Implement additional message request improvements.
This commit is contained in:
parent
81c7887d47
commit
1faf196f82
43 changed files with 1523 additions and 361 deletions
|
@ -42,16 +42,11 @@ import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
|||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider;
|
||||
import org.thoughtcrime.securesms.gcm.FcmJobService;
|
||||
import org.thoughtcrime.securesms.insights.InsightsOptOut;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob;
|
||||
import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
|
||||
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
|
||||
import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.megaphone.MegaphoneRepository;
|
||||
import org.thoughtcrime.securesms.logging.AndroidLogger;
|
||||
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
@ -63,6 +58,7 @@ import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
|||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.ringrtc.RingRtcLogger;
|
||||
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
|
||||
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
||||
|
@ -73,10 +69,8 @@ import org.thoughtcrime.securesms.revealable.ViewOnceMessageManager;
|
|||
import org.thoughtcrime.securesms.service.RotateSenderCertificateListener;
|
||||
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
|
||||
import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
|
||||
import org.thoughtcrime.securesms.stickers.BlessedPacks;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
|
||||
import org.webrtc.voiceengine.WebRtcAudioManager;
|
||||
|
@ -153,7 +147,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
|||
public void onStart(@NonNull LifecycleOwner owner) {
|
||||
isAppVisible = true;
|
||||
Log.i(TAG, "App is now visible.");
|
||||
FeatureFlags.refresh();
|
||||
FeatureFlags.refreshIfNecessary();
|
||||
ApplicationDependencies.getRecipientCache().warmUp();
|
||||
executePendingContactSync();
|
||||
KeyCachingService.onAppForegrounded(this);
|
||||
|
|
|
@ -11,7 +11,6 @@ import com.annimon.stream.Stream;
|
|||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
|
|
@ -70,7 +70,6 @@ import androidx.appcompat.widget.Toolbar;
|
|||
import androidx.core.content.pm.ShortcutInfoCompat;
|
||||
import androidx.core.content.pm.ShortcutManagerCompat;
|
||||
import androidx.core.graphics.drawable.IconCompat;
|
||||
import androidx.core.text.HtmlCompat;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
|
@ -152,6 +151,7 @@ import org.thoughtcrime.securesms.giph.ui.GiphyActivity;
|
|||
import org.thoughtcrime.securesms.insights.InsightsLauncher;
|
||||
import org.thoughtcrime.securesms.invites.InviteReminderModel;
|
||||
import org.thoughtcrime.securesms.invites.InviteReminderRepository;
|
||||
import org.thoughtcrime.securesms.jobs.LeaveGroupJob;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
||||
import org.thoughtcrime.securesms.jobs.ServiceOutageDetectionJob;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
|
@ -216,7 +216,6 @@ import org.thoughtcrime.securesms.util.DynamicLanguage;
|
|||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.HtmlUtil;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.MessageUtil;
|
||||
|
@ -1120,7 +1119,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||
Optional<OutgoingGroupMediaMessage> leaveMessage = GroupUtil.createGroupLeaveMessage(this, groupRecipient);
|
||||
|
||||
if (threadId != -1 && leaveMessage.isPresent()) {
|
||||
MessageSender.send(this, leaveMessage.get(), threadId, false, null);
|
||||
ApplicationDependencies.getJobManager().add(LeaveGroupJob.create(groupRecipient));
|
||||
|
||||
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(this);
|
||||
String groupId = groupRecipient.requireGroupId();
|
||||
|
@ -2043,7 +2042,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||
}
|
||||
|
||||
private void setBlockedUserState(Recipient recipient, boolean isSecureText, boolean isDefaultSms) {
|
||||
if (recipient.isBlocked()) {
|
||||
if (recipient.isBlocked() && !FeatureFlags.messageRequests()) {
|
||||
unblockButton.setVisibility(View.VISIBLE);
|
||||
composePanel.setVisibility(View.GONE);
|
||||
makeDefaultSmsButton.setVisibility(View.GONE);
|
||||
|
@ -2067,7 +2066,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||
}
|
||||
|
||||
private void setGroupShareProfileReminder(@NonNull Recipient recipient) {
|
||||
if (!shouldDisplayMessageRequestUi && recipient.isPushGroup() && !recipient.isProfileSharing()) {
|
||||
if (FeatureFlags.messageRequests()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (recipient.isPushGroup() && !recipient.isProfileSharing()) {
|
||||
groupShareProfileView.get().setRecipient(recipient);
|
||||
groupShareProfileView.get().setVisibility(View.VISIBLE);
|
||||
} else if (groupShareProfileView.resolved()) {
|
||||
|
@ -2777,14 +2780,14 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||
|
||||
@Override
|
||||
public void onMessageRequest(@NonNull MessageRequestViewModel viewModel) {
|
||||
|
||||
messageRequestBottomView.setAcceptOnClickListener(v -> viewModel.accept());
|
||||
messageRequestBottomView.setDeleteOnClickListener(v -> viewModel.delete());
|
||||
messageRequestBottomView.setBlockOnClickListener(v -> viewModel.block());
|
||||
messageRequestBottomView.setAcceptOnClickListener(v -> viewModel.onAccept());
|
||||
messageRequestBottomView.setDeleteOnClickListener(v -> onMessageRequestDeleteClicked(viewModel));
|
||||
messageRequestBottomView.setBlockOnClickListener(v -> onMessageRequestBlockClicked(viewModel));
|
||||
messageRequestBottomView.setUnblockOnClickListener(v -> onMessageRequestUnblockClicked(viewModel));
|
||||
|
||||
viewModel.getRecipient().observe(this, this::presentMessageRequestBottomViewTo);
|
||||
viewModel.getShouldDisplayMessageRequest().observe(this, this::handleShouldDisplayMessageRequest);
|
||||
viewModel.getMesasgeRequestStatus().observe(this, status -> {
|
||||
viewModel.getMessageRequestDisplayState().observe(this, this::presentMessageRequestDisplayState);
|
||||
viewModel.getMessageRequestStatus().observe(this, status -> {
|
||||
switch (status) {
|
||||
case ACCEPTED:
|
||||
messageRequestBottomView.setVisibility(View.GONE);
|
||||
|
@ -2909,14 +2912,104 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||
updateLinkPreviewState();
|
||||
}
|
||||
|
||||
private void handleShouldDisplayMessageRequest(boolean shouldDisplayMessageRequest) {
|
||||
shouldDisplayMessageRequestUi = shouldDisplayMessageRequest;
|
||||
setGroupShareProfileReminder(recipient.get());
|
||||
private void onMessageRequestDeleteClicked(@NonNull MessageRequestViewModel requestModel) {
|
||||
Recipient recipient = requestModel.getRecipient().getValue();
|
||||
if (recipient == null) {
|
||||
Log.w(TAG, "[onMessageRequestDeleteClicked] No recipient!");
|
||||
return;
|
||||
}
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this)
|
||||
.setNeutralButton(R.string.ConversationActivity_cancel, (d, w) -> d.dismiss());
|
||||
|
||||
if (recipient.isGroup() && recipient.isBlocked()) {
|
||||
builder.setTitle(R.string.ConversationActivity_delete_conversation);
|
||||
builder.setMessage(R.string.ConversationActivity_this_conversation_will_be_deleted_from_all_of_your_devices);
|
||||
builder.setPositiveButton(R.string.ConversationActivity_delete, (d, w) -> requestModel.onDelete());
|
||||
} else if (recipient.isGroup()) {
|
||||
builder.setTitle(R.string.ConversationActivity_delete_and_leave_group);
|
||||
builder.setMessage(R.string.ConversationActivity_you_will_leave_this_group_and_it_will_be_deleted_from_all_of_your_devices);
|
||||
builder.setNegativeButton(R.string.ConversationActivity_delete_and_leave, (d, w) -> requestModel.onDelete());
|
||||
} else {
|
||||
builder.setTitle(R.string.ConversationActivity_delete_conversation);
|
||||
builder.setMessage(R.string.ConversationActivity_this_conversation_will_be_deleted_from_all_of_your_devices);
|
||||
builder.setNegativeButton(R.string.ConversationActivity_delete, (d, w) -> requestModel.onDelete());
|
||||
}
|
||||
|
||||
builder.show();
|
||||
}
|
||||
|
||||
private void onMessageRequestBlockClicked(@NonNull MessageRequestViewModel requestModel) {
|
||||
Recipient recipient = requestModel.getRecipient().getValue();
|
||||
if (recipient == null) {
|
||||
Log.w(TAG, "[onMessageRequestBlockClicked] No recipient!");
|
||||
return;
|
||||
}
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this)
|
||||
.setNeutralButton(R.string.ConversationActivity_cancel, (d, w) -> d.dismiss())
|
||||
.setPositiveButton(R.string.ConversationActivity_block_and_delete, (d, w) -> requestModel.onBlockAndDelete())
|
||||
.setNegativeButton(R.string.ConversationActivity_block, (d, w) -> requestModel.onBlock());
|
||||
|
||||
if (recipient.isGroup()) {
|
||||
builder.setTitle(getString(R.string.ConversationActivity_block_and_leave_s, recipient.getDisplayName(this)));
|
||||
builder.setMessage(R.string.ConversationActivity_you_will_leave_this_group_and_no_longer_receive_messages_or_updates);
|
||||
} else {
|
||||
builder.setTitle(getString(R.string.ConversationActivity_block_s, recipient.getDisplayName(this)));
|
||||
builder.setMessage(R.string.ConversationActivity_blocked_people_will_not_be_able_to_call_you_or_send_you_messages);
|
||||
}
|
||||
|
||||
builder.show();
|
||||
}
|
||||
|
||||
private void onMessageRequestUnblockClicked(@NonNull MessageRequestViewModel requestModel) {
|
||||
Recipient recipient = requestModel.getRecipient().getValue();
|
||||
if (recipient == null) {
|
||||
Log.w(TAG, "[onMessageRequestUnblockClicked] No recipient!");
|
||||
return;
|
||||
}
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this)
|
||||
.setTitle(getString(R.string.ConversationActivity_unblock_s, recipient.getDisplayName(this)))
|
||||
.setNeutralButton(R.string.ConversationActivity_cancel, (d, w) -> d.dismiss())
|
||||
.setNegativeButton(R.string.ConversationActivity_unblock, (d, w) -> requestModel.onUnblock());
|
||||
|
||||
if (recipient.isGroup()) {
|
||||
builder.setMessage(R.string.ConversationActivity_group_members_will_be_able_to_add_you_to_this_group_again);
|
||||
} else {
|
||||
builder.setMessage(R.string.ConversationActivity_you_will_be_able_to_message_and_call_each_other);
|
||||
}
|
||||
|
||||
builder.show();
|
||||
}
|
||||
|
||||
private void presentMessageRequestDisplayState(@NonNull MessageRequestViewModel.DisplayState displayState) {
|
||||
if (getIntent().hasExtra(TEXT_EXTRA) || getIntent().hasExtra(MEDIA_EXTRA) || getIntent().hasExtra(STICKER_EXTRA) || (isPushGroupConversation() && !isActiveGroup())) {
|
||||
Log.d(TAG, "[presentMessageRequestDisplayState] Have extra, so ignoring provided state.");
|
||||
messageRequestBottomView.setVisibility(View.GONE);
|
||||
} else {
|
||||
messageRequestBottomView.setVisibility(shouldDisplayMessageRequest ? View.VISIBLE : View.GONE);
|
||||
Log.d(TAG, "[presentMessageRequestDisplayState] " + displayState);
|
||||
switch (displayState) {
|
||||
case DISPLAY_MESSAGE_REQUEST:
|
||||
messageRequestBottomView.setVisibility(View.VISIBLE);
|
||||
if (groupShareProfileView.resolved()) {
|
||||
groupShareProfileView.get().setVisibility(View.GONE);
|
||||
}
|
||||
break;
|
||||
case DISPLAY_LEGACY:
|
||||
if (recipient.get().isGroup()) {
|
||||
groupShareProfileView.get().setRecipient(recipient.get());
|
||||
groupShareProfileView.get().setVisibility(View.VISIBLE);
|
||||
}
|
||||
messageRequestBottomView.setVisibility(View.GONE);
|
||||
break;
|
||||
case DISPLAY_NONE:
|
||||
messageRequestBottomView.setVisibility(View.GONE);
|
||||
if (groupShareProfileView.resolved()) {
|
||||
groupShareProfileView.get().setVisibility(View.GONE);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
invalidateOptionsMenu();
|
||||
|
@ -3019,6 +3112,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||
private void presentMessageRequestBottomViewTo(@Nullable Recipient recipient) {
|
||||
if (recipient == null) return;
|
||||
|
||||
messageRequestBottomView.setQuestionText(HtmlCompat.fromHtml(getString(R.string.MessageRequestBottomView_do_you_want_to_let, HtmlUtil.bold(recipient.getDisplayName(this))), 0));
|
||||
messageRequestBottomView.setRecipient(recipient);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -65,6 +65,7 @@ import org.thoughtcrime.securesms.ApplicationContext;
|
|||
import org.thoughtcrime.securesms.MessageDetailsActivity;
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.sharing.ShareActivity;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.components.ConversationTypingView;
|
||||
import org.thoughtcrime.securesms.components.TooltipPopup;
|
||||
|
@ -155,7 +156,6 @@ public class ConversationFragment extends Fragment
|
|||
private int activeOffset;
|
||||
private boolean firstLoad;
|
||||
private boolean isReacting;
|
||||
private boolean shouldDisplayMessageRequest;
|
||||
private ActionMode actionMode;
|
||||
private Locale locale;
|
||||
private RecyclerView list;
|
||||
|
@ -206,7 +206,7 @@ public class ConversationFragment extends Fragment
|
|||
|
||||
new ConversationItemSwipeCallback(
|
||||
messageRecord -> actionMode == null &&
|
||||
canReplyToMessage(isActionMessage(messageRecord), messageRecord, shouldDisplayMessageRequest),
|
||||
canReplyToMessage(isActionMessage(messageRecord), messageRecord, messageRequestViewModel.shouldShowMessageRequest()),
|
||||
this::handleReplyMessage
|
||||
).attachToRecyclerView(list);
|
||||
|
||||
|
@ -334,12 +334,6 @@ public class ConversationFragment extends Fragment
|
|||
presentMessageRequestProfileView(requireContext(), recipientInfo, conversationBanner);
|
||||
presentMessageRequestProfileView(requireContext(), recipientInfo, emptyConversationBanner);
|
||||
});
|
||||
|
||||
messageRequestViewModel.getShouldDisplayMessageRequest().observe(getViewLifecycleOwner(), this::handleShouldDisplayMessageRequest);
|
||||
}
|
||||
|
||||
private void handleShouldDisplayMessageRequest(boolean shouldDisplayMessageRequest) {
|
||||
this.shouldDisplayMessageRequest = shouldDisplayMessageRequest;
|
||||
}
|
||||
|
||||
private static void presentMessageRequestProfileView(@NonNull Context context, @NonNull MessageRequestViewModel.RecipientInfo recipientInfo, @Nullable ConversationBannerView conversationBanner) {
|
||||
|
@ -558,7 +552,7 @@ public class ConversationFragment extends Fragment
|
|||
|
||||
menu.findItem(R.id.menu_context_forward).setVisible(!actionMessage && !sharedContact && !viewOnce);
|
||||
menu.findItem(R.id.menu_context_details).setVisible(!actionMessage);
|
||||
menu.findItem(R.id.menu_context_reply).setVisible(canReplyToMessage(actionMessage, messageRecord, shouldDisplayMessageRequest));
|
||||
menu.findItem(R.id.menu_context_reply).setVisible(canReplyToMessage(actionMessage, messageRecord, messageRequestViewModel.shouldShowMessageRequest()));
|
||||
}
|
||||
menu.findItem(R.id.menu_context_copy).setVisible(!actionMessage && hasText);
|
||||
}
|
||||
|
@ -840,7 +834,7 @@ public class ConversationFragment extends Fragment
|
|||
@Override
|
||||
public void onLoadFinished(@NonNull Loader<Cursor> cursorLoader, Cursor cursor) {
|
||||
int count = cursor.getCount();
|
||||
ConversationLoader loader = (ConversationLoader)cursorLoader;
|
||||
ConversationLoader loader = (ConversationLoader) cursorLoader;
|
||||
|
||||
ConversationAdapter adapter = getListAdapter();
|
||||
if (adapter == null) {
|
||||
|
@ -859,7 +853,7 @@ public class ConversationFragment extends Fragment
|
|||
setLastSeen(loader.getLastSeen());
|
||||
}
|
||||
|
||||
if (FeatureFlags.messageRequests()) {
|
||||
if (FeatureFlags.messageRequests() && !loader.hasPreMessageRequestMessages()) {
|
||||
clearHeaderIfNotTyping(adapter);
|
||||
} else {
|
||||
if (!loader.hasSent() && !recipient.get().isSystemContact() && !recipient.get().isGroup() && recipient.get().getRegistered() == RecipientDatabase.RegisteredState.REGISTERED) {
|
||||
|
@ -1142,7 +1136,7 @@ public class ConversationFragment extends Fragment
|
|||
if (messageRecord.isSecure() &&
|
||||
!messageRecord.isUpdate() &&
|
||||
!recipient.get().isBlocked() &&
|
||||
!shouldDisplayMessageRequest &&
|
||||
!messageRequestViewModel.shouldShowMessageRequest() &&
|
||||
((ConversationAdapter) list.getAdapter()).getSelectedItems().isEmpty())
|
||||
{
|
||||
isReacting = true;
|
||||
|
|
|
@ -49,7 +49,6 @@ import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
|
|||
import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.SearchUtil;
|
||||
import org.thoughtcrime.securesms.util.SpanUtil;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
|
|
|
@ -15,7 +15,6 @@ import com.annimon.stream.Stream;
|
|||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||
|
|
|
@ -282,16 +282,31 @@ public class MmsDatabase extends MessagingDatabase {
|
|||
|
||||
public int getMessageCountForThread(long threadId) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
Cursor cursor = null;
|
||||
|
||||
try {
|
||||
cursor = db.query(TABLE_NAME, new String[] {"COUNT(*)"}, THREAD_ID + " = ?", new String[] {threadId+""}, null, null, null);
|
||||
String[] cols = new String[] {"COUNT(*)"};
|
||||
String query = THREAD_ID + " = ?";
|
||||
String[] args = new String[]{String.valueOf(threadId)};
|
||||
|
||||
if (cursor != null && cursor.moveToFirst())
|
||||
try (Cursor cursor = db.query(TABLE_NAME, cols, query, args, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return cursor.getInt(0);
|
||||
} finally {
|
||||
if (cursor != null)
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public int getMessageCountForThread(long threadId, long beforeTime) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
|
||||
String[] cols = new String[] {"COUNT(*)"};
|
||||
String query = THREAD_ID + " = ? AND " + DATE_RECEIVED + " < ?";
|
||||
String[] args = new String[]{String.valueOf(threadId), String.valueOf(beforeTime)};
|
||||
|
||||
try (Cursor cursor = db.query(TABLE_NAME, cols, query, args, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return cursor.getInt(0);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
|
|
@ -22,8 +22,6 @@ import android.database.Cursor;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.sqlcipher.database.SQLiteQueryBuilder;
|
||||
|
||||
|
@ -35,7 +33,6 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
|
|||
import org.whispersystems.libsignal.util.Pair;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class MmsSmsDatabase extends Database {
|
||||
|
@ -88,24 +85,27 @@ public class MmsSmsDatabase extends Database {
|
|||
super(context, databaseHelper);
|
||||
}
|
||||
|
||||
public @Nullable RecipientId getRecipientIdForLatestAdd(long threadId) {
|
||||
/**
|
||||
* @return The user that added you to the group, otherwise null.
|
||||
*/
|
||||
public @Nullable RecipientId getGroupAddedBy(long threadId) {
|
||||
long lastQuitChecked = System.currentTimeMillis();
|
||||
Pair<RecipientId, Long> pair;
|
||||
|
||||
do {
|
||||
pair = getRecipientIdForLatestAdd(threadId, lastQuitChecked);
|
||||
pair = getGroupAddedBy(threadId, lastQuitChecked);
|
||||
if (pair.first() != null) {
|
||||
return pair.first();
|
||||
} else {
|
||||
lastQuitChecked = pair.second();
|
||||
}
|
||||
|
||||
} while (pair.second() != -1L);
|
||||
} while (pair.second() != -1);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private @NonNull Pair<RecipientId, Long> getRecipientIdForLatestAdd(long threadId, long lastQuitChecked) {
|
||||
private @NonNull Pair<RecipientId, Long> getGroupAddedBy(long threadId, long lastQuitChecked) {
|
||||
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
|
||||
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
|
||||
long latestQuit = mmsDatabase.getLatestGroupQuitTimestamp(threadId, lastQuitChecked);
|
||||
|
@ -225,6 +225,11 @@ public class MmsSmsDatabase extends Database {
|
|||
return count;
|
||||
}
|
||||
|
||||
public int getConversationCount(long threadId, long beforeTime) {
|
||||
return DatabaseFactory.getSmsDatabase(context).getMessageCountForThread(threadId, beforeTime) +
|
||||
DatabaseFactory.getMmsDatabase(context).getMessageCountForThread(threadId, beforeTime);
|
||||
}
|
||||
|
||||
public int getInsecureSentCount(long threadId) {
|
||||
int count = DatabaseFactory.getSmsDatabase(context).getInsecureMessagesSentForThread(threadId);
|
||||
count += DatabaseFactory.getMmsDatabase(context).getInsecureMessagesSentForThread(threadId);
|
||||
|
|
|
@ -1222,6 +1222,7 @@ public class RecipientDatabase extends Database {
|
|||
|
||||
ContentValues setBlocked = new ContentValues();
|
||||
setBlocked.put(BLOCKED, 1);
|
||||
setBlocked.put(PROFILE_SHARING, 0);
|
||||
|
||||
for (String e164 : blockedE164) {
|
||||
db.update(TABLE_NAME, setBlocked, PHONE + " = ?", new String[] { e164 });
|
||||
|
|
|
@ -216,17 +216,31 @@ public class SmsDatabase extends MessagingDatabase {
|
|||
|
||||
public int getMessageCountForThread(long threadId) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
Cursor cursor = null;
|
||||
|
||||
try {
|
||||
cursor = db.query(TABLE_NAME, new String[] {"COUNT(*)"}, THREAD_ID + " = ?",
|
||||
new String[] {threadId+""}, null, null, null);
|
||||
String[] cols = new String[] {"COUNT(*)"};
|
||||
String query = THREAD_ID + " = ?";
|
||||
String[] args = new String[]{String.valueOf(threadId)};
|
||||
|
||||
if (cursor != null && cursor.moveToFirst())
|
||||
try (Cursor cursor = db.query(TABLE_NAME, cols, query, args, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return cursor.getInt(0);
|
||||
} finally {
|
||||
if (cursor != null)
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public int getMessageCountForThread(long threadId, long beforeTime) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
|
||||
String[] cols = new String[] {"COUNT(*)"};
|
||||
String query = THREAD_ID + " = ? AND " + DATE_RECEIVED + " < ?";
|
||||
String[] args = new String[]{String.valueOf(threadId), String.valueOf(beforeTime)};
|
||||
|
||||
try (Cursor cursor = db.query(TABLE_NAME, cols, query, args, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return cursor.getInt(0);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
|
|
@ -705,13 +705,13 @@ public class ThreadDatabase extends Database {
|
|||
}
|
||||
|
||||
private @Nullable Extra getExtrasFor(MessageRecord record) {
|
||||
boolean messageRequestAccepted = RecipientUtil.isThreadMessageRequestAccepted(context, record.getThreadId());
|
||||
boolean messageRequestAccepted = RecipientUtil.isMessageRequestAccepted(context, record.getThreadId());
|
||||
RecipientId threadRecipientId = getRecipientIdForThreadId(record.getThreadId());
|
||||
|
||||
if (!messageRequestAccepted && threadRecipientId != null) {
|
||||
boolean isPushGroup = Recipient.resolved(threadRecipientId).isPushGroup();
|
||||
if (isPushGroup) {
|
||||
RecipientId recipientId = DatabaseFactory.getMmsSmsDatabase(context).getRecipientIdForLatestAdd(record.getThreadId());
|
||||
RecipientId recipientId = DatabaseFactory.getMmsSmsDatabase(context).getGroupAddedBy(record.getThreadId());
|
||||
|
||||
if (recipientId != null) {
|
||||
return Extra.forGroupMessageRequest(recipientId);
|
||||
|
|
|
@ -15,6 +15,7 @@ public class ConversationLoader extends AbstractCursorLoader {
|
|||
private long lastSeen;
|
||||
private boolean hasSent;
|
||||
private boolean isMessageRequestAccepted;
|
||||
private boolean hasPreMessageRequestMessages;
|
||||
|
||||
public ConversationLoader(Context context, long threadId, int offset, int limit, long lastSeen) {
|
||||
super(context);
|
||||
|
@ -49,6 +50,10 @@ public class ConversationLoader extends AbstractCursorLoader {
|
|||
return isMessageRequestAccepted;
|
||||
}
|
||||
|
||||
public boolean hasPreMessageRequestMessages() {
|
||||
return hasPreMessageRequestMessages;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor getCursor() {
|
||||
Pair<Long, Boolean> lastSeenAndHasSent = DatabaseFactory.getThreadDatabase(context).getLastSeenAndHasSent(threadId);
|
||||
|
@ -59,7 +64,8 @@ public class ConversationLoader extends AbstractCursorLoader {
|
|||
this.lastSeen = lastSeenAndHasSent.first();
|
||||
}
|
||||
|
||||
this.isMessageRequestAccepted = RecipientUtil.isThreadMessageRequestAccepted(context, threadId);
|
||||
this.isMessageRequestAccepted = RecipientUtil.isMessageRequestAccepted(context, threadId);
|
||||
this.hasPreMessageRequestMessages = RecipientUtil.isPreMessageRequestThread(context, threadId);
|
||||
|
||||
return DatabaseFactory.getMmsSmsDatabase(context).getConversation(threadId, offset, limit);
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
|||
import org.thoughtcrime.securesms.sms.IncomingGroupMessage;
|
||||
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
|
@ -101,6 +102,13 @@ public class GroupMessageProcessor {
|
|||
database.create(id, group.getName().orNull(), members,
|
||||
avatar != null && avatar.isPointer() ? avatar.asPointer() : null, null);
|
||||
|
||||
Recipient sender = Recipient.externalPush(context, content.getSender());
|
||||
|
||||
if (FeatureFlags.messageRequests() && (sender.isSystemContact() || sender.isProfileSharing())) {
|
||||
Log.i(TAG, "Auto-enabling profile sharing because 'adder' is trusted. contact: " + sender.isSystemContact() + ", profileSharing: " + sender.isProfileSharing());
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.external(context, id).getId(), true);
|
||||
}
|
||||
|
||||
return storeMessage(context, content, group, builder.build(), outgoing);
|
||||
}
|
||||
|
||||
|
@ -283,11 +291,11 @@ public class GroupMessageProcessor {
|
|||
GroupContext.Member.Builder member = GroupContext.Member.newBuilder();
|
||||
|
||||
if (address.getUuid().isPresent()) {
|
||||
member = member.setUuid(address.getUuid().get().toString());
|
||||
member.setUuid(address.getUuid().get().toString());
|
||||
}
|
||||
|
||||
if (address.getNumber().isPresent()) {
|
||||
member = member.setE164(address.getNumber().get());
|
||||
member.setE164(address.getNumber().get());
|
||||
}
|
||||
|
||||
return member.build();
|
||||
|
|
|
@ -51,6 +51,7 @@ public final class JobManagerFactories {
|
|||
put(CreateSignedPreKeyJob.KEY, new CreateSignedPreKeyJob.Factory());
|
||||
put(DirectoryRefreshJob.KEY, new DirectoryRefreshJob.Factory());
|
||||
put(FcmRefreshJob.KEY, new FcmRefreshJob.Factory());
|
||||
put(LeaveGroupJob.KEY, new LeaveGroupJob.Factory());
|
||||
put(LocalBackupJob.KEY, new LocalBackupJob.Factory());
|
||||
put(MmsDownloadJob.KEY, new MmsDownloadJob.Factory());
|
||||
put(MmsReceiveJob.KEY, new MmsReceiveJob.Factory());
|
||||
|
@ -60,6 +61,7 @@ public final class JobManagerFactories {
|
|||
put(MultiDeviceContactUpdateJob.KEY, new MultiDeviceContactUpdateJob.Factory());
|
||||
put(MultiDeviceGroupUpdateJob.KEY, new MultiDeviceGroupUpdateJob.Factory());
|
||||
put(MultiDeviceKeysUpdateJob.KEY, new MultiDeviceKeysUpdateJob.Factory());
|
||||
put(MultiDeviceMessageRequestResponseJob.KEY, new MultiDeviceMessageRequestResponseJob.Factory());
|
||||
put(MultiDeviceProfileContentUpdateJob.KEY, new MultiDeviceProfileContentUpdateJob.Factory());
|
||||
put(MultiDeviceProfileKeyUpdateJob.KEY, new MultiDeviceProfileKeyUpdateJob.Factory());
|
||||
put(MultiDeviceReadUpdateJob.KEY, new MultiDeviceReadUpdateJob.Factory());
|
||||
|
|
|
@ -0,0 +1,179 @@
|
|||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.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.thoughtcrime.securesms.util.GroupUtil;
|
||||
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.crypto.UntrustedIdentityException;
|
||||
import org.whispersystems.signalservice.api.messages.SendMessageResult;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Normally, we can do group leaves via {@link PushGroupSendJob}. However, that job relies on a
|
||||
* message being present in the database, which is not true if the user selects a message request
|
||||
* option that deletes and leaves at the same time.
|
||||
*
|
||||
* This job tracks all send state within the job and does not require a message in the database to
|
||||
* work.
|
||||
*/
|
||||
public class LeaveGroupJob extends BaseJob {
|
||||
|
||||
public static final String KEY = "LeaveGroupJob";
|
||||
|
||||
private static final String TAG = Log.tag(LeaveGroupJob.class);
|
||||
|
||||
private static final String KEY_GROUP_ID = "group_id";
|
||||
private static final String KEY_GROUP_NAME = "name";
|
||||
private static final String KEY_MEMBERS = "members";
|
||||
private static final String KEY_RECIPIENTS = "recipients";
|
||||
|
||||
private final byte[] groupId;
|
||||
private final String name;
|
||||
private final List<RecipientId> members;
|
||||
private final List<RecipientId> recipients;
|
||||
|
||||
public static @NonNull LeaveGroupJob create(@NonNull Recipient group) {
|
||||
List<RecipientId> members = Stream.of(group.resolve().getParticipants()).map(Recipient::getId).toList();
|
||||
members.remove(Recipient.self().getId());
|
||||
|
||||
return new LeaveGroupJob(GroupUtil.getDecodedIdOrThrow(group.getGroupId().get()),
|
||||
group.resolve().getDisplayName(ApplicationDependencies.getApplication()),
|
||||
members,
|
||||
members,
|
||||
new Parameters.Builder()
|
||||
.setQueue(group.getId().toQueueKey())
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
.setMaxAttempts(Parameters.UNLIMITED)
|
||||
.build());
|
||||
}
|
||||
|
||||
private LeaveGroupJob(@NonNull byte[] groupId,
|
||||
@NonNull String name,
|
||||
@NonNull List<RecipientId> members,
|
||||
@NonNull List<RecipientId> recipients,
|
||||
@NonNull Parameters parameters)
|
||||
{
|
||||
super(parameters);
|
||||
this.groupId = groupId;
|
||||
this.name = name;
|
||||
this.members = Collections.unmodifiableList(members);
|
||||
this.recipients = recipients;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Data serialize() {
|
||||
return new Data.Builder().putString(KEY_GROUP_ID, Base64.encodeBytes(groupId))
|
||||
.putString(KEY_GROUP_NAME, name)
|
||||
.putString(KEY_MEMBERS, RecipientId.toSerializedList(members))
|
||||
.putString(KEY_RECIPIENTS, RecipientId.toSerializedList(recipients))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRun() throws Exception {
|
||||
List<Recipient> completions = deliver(context, groupId, name, members, recipients);
|
||||
|
||||
for (Recipient completion : completions) {
|
||||
recipients.remove(completion.getId());
|
||||
}
|
||||
|
||||
Log.i(TAG, "Completed now: " + completions.size() + ", Remaining: " + recipients.size());
|
||||
|
||||
if (!recipients.isEmpty()) {
|
||||
Log.w(TAG, "Still need to send to " + recipients.size() + " recipients. Retrying.");
|
||||
throw new RetryLaterException();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onShouldRetry(@NonNull Exception e) {
|
||||
return e instanceof IOException || e instanceof RetryLaterException;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure() {
|
||||
}
|
||||
|
||||
private static @NonNull List<Recipient> deliver(@NonNull Context context,
|
||||
@NonNull byte[] groupId,
|
||||
@NonNull String name,
|
||||
@NonNull List<RecipientId> members,
|
||||
@NonNull List<RecipientId> destinations)
|
||||
throws IOException, UntrustedIdentityException
|
||||
{
|
||||
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
|
||||
List<SignalServiceAddress> addresses = Stream.of(destinations).map(Recipient::resolved).map(t -> RecipientUtil.toSignalServiceAddress(context, t)).toList();
|
||||
List<SignalServiceAddress> memberAddresses = Stream.of(members).map(Recipient::resolved).map(t -> RecipientUtil.toSignalServiceAddress(context, t)).toList();
|
||||
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess = Stream.of(destinations).map(Recipient::resolved).map(recipient -> UnidentifiedAccessUtil.getAccessFor(context, recipient)).toList();
|
||||
SignalServiceGroup serviceGroup = new SignalServiceGroup(SignalServiceGroup.Type.QUIT, groupId, name, memberAddresses, null);
|
||||
SignalServiceDataMessage.Builder dataMessage = SignalServiceDataMessage.newBuilder()
|
||||
.withTimestamp(System.currentTimeMillis())
|
||||
.asGroupMessage(serviceGroup);
|
||||
|
||||
|
||||
List<SendMessageResult> results = messageSender.sendMessage(addresses, unidentifiedAccess, false, dataMessage.build());
|
||||
|
||||
Stream.of(results)
|
||||
.filter(r -> r.getIdentityFailure() != null)
|
||||
.map(SendMessageResult::getAddress)
|
||||
.map(a -> Recipient.externalPush(context, a))
|
||||
.forEach(r -> Log.w(TAG, "Identity failure for " + r.getId()));
|
||||
|
||||
Stream.of(results)
|
||||
.filter(SendMessageResult::isUnregisteredFailure)
|
||||
.map(SendMessageResult::getAddress)
|
||||
.map(a -> Recipient.externalPush(context, a))
|
||||
.forEach(r -> Log.w(TAG, "Unregistered failure for " + r.getId()));
|
||||
|
||||
|
||||
return Stream.of(results)
|
||||
.filter(r -> r.getSuccess() != null || r.getIdentityFailure() != null || r.isUnregisteredFailure())
|
||||
.map(SendMessageResult::getAddress)
|
||||
.map(a -> Recipient.externalPush(context, a))
|
||||
.toList();
|
||||
}
|
||||
|
||||
public static class Factory implements Job.Factory<LeaveGroupJob> {
|
||||
@Override
|
||||
public @NonNull LeaveGroupJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
return new LeaveGroupJob(Base64.decodeOrThrow(data.getString(KEY_GROUP_ID)),
|
||||
data.getString(KEY_GROUP_NAME),
|
||||
RecipientId.fromSerializedList(data.getString(KEY_MEMBERS)),
|
||||
RecipientId.fromSerializedList(data.getString(KEY_RECIPIENTS)),
|
||||
parameters);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,164 @@
|
|||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.KeysMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.MessageRequestResponseMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class MultiDeviceMessageRequestResponseJob extends BaseJob {
|
||||
|
||||
public static final String KEY = "MultiDeviceMessageRequestResponseJob";
|
||||
|
||||
private static final String TAG = MultiDeviceMessageRequestResponseJob.class.getSimpleName();
|
||||
|
||||
private static final String KEY_THREAD_RECIPIENT = "thread_recipient";
|
||||
private static final String KEY_TYPE = "type";
|
||||
|
||||
private final RecipientId threadRecipient;
|
||||
private final Type type;
|
||||
|
||||
public static @NonNull MultiDeviceMessageRequestResponseJob forAccept(@NonNull RecipientId threadRecipient) {
|
||||
return new MultiDeviceMessageRequestResponseJob(threadRecipient, Type.ACCEPT);
|
||||
}
|
||||
|
||||
public static @NonNull MultiDeviceMessageRequestResponseJob forDelete(@NonNull RecipientId threadRecipient) {
|
||||
return new MultiDeviceMessageRequestResponseJob(threadRecipient, Type.DELETE);
|
||||
}
|
||||
|
||||
public static @NonNull MultiDeviceMessageRequestResponseJob forBlock(@NonNull RecipientId threadRecipient) {
|
||||
return new MultiDeviceMessageRequestResponseJob(threadRecipient, Type.BLOCK);
|
||||
}
|
||||
|
||||
public static @NonNull MultiDeviceMessageRequestResponseJob forBlockAndDelete(@NonNull RecipientId threadRecipient) {
|
||||
return new MultiDeviceMessageRequestResponseJob(threadRecipient, Type.BLOCK_AND_DELETE);
|
||||
}
|
||||
|
||||
private MultiDeviceMessageRequestResponseJob(@NonNull RecipientId threadRecipient, @NonNull Type type) {
|
||||
this(new Parameters.Builder()
|
||||
.setQueue("MultiDeviceMessageRequestResponseJob")
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setMaxAttempts(Parameters.UNLIMITED)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
.build(), threadRecipient, type);
|
||||
|
||||
}
|
||||
|
||||
private MultiDeviceMessageRequestResponseJob(@NonNull Parameters parameters,
|
||||
@NonNull RecipientId threadRecipient,
|
||||
@NonNull Type type)
|
||||
{
|
||||
super(parameters);
|
||||
this.threadRecipient = threadRecipient;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Data serialize() {
|
||||
return new Data.Builder().putString(KEY_THREAD_RECIPIENT, threadRecipient.serialize())
|
||||
.putInt(KEY_TYPE, type.serialize())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRun() throws IOException, UntrustedIdentityException {
|
||||
if (!TextSecurePreferences.isMultiDevice(context)) {
|
||||
Log.i(TAG, "Not multi device, aborting...");
|
||||
return;
|
||||
}
|
||||
|
||||
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
|
||||
Recipient recipient = Recipient.resolved(threadRecipient);
|
||||
|
||||
|
||||
MessageRequestResponseMessage response;
|
||||
|
||||
if (recipient.isGroup()) {
|
||||
response = MessageRequestResponseMessage.forGroup(GroupUtil.getDecodedId(recipient.getGroupId().get()), localToRemoteType(type));
|
||||
} else {
|
||||
response = MessageRequestResponseMessage.forIndividual(RecipientUtil.toSignalServiceAddress(context, recipient), localToRemoteType(type));
|
||||
}
|
||||
|
||||
messageSender.sendMessage(SignalServiceSyncMessage.forMessageRequestResponse(response),
|
||||
UnidentifiedAccessUtil.getAccessForSync(context));
|
||||
}
|
||||
|
||||
private static MessageRequestResponseMessage.Type localToRemoteType(@NonNull Type type) {
|
||||
switch (type) {
|
||||
case ACCEPT: return MessageRequestResponseMessage.Type.ACCEPT;
|
||||
case DELETE: return MessageRequestResponseMessage.Type.DELETE;
|
||||
case BLOCK: return MessageRequestResponseMessage.Type.BLOCK;
|
||||
case BLOCK_AND_DELETE: return MessageRequestResponseMessage.Type.BLOCK_AND_DELETE;
|
||||
default: return MessageRequestResponseMessage.Type.UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onShouldRetry(@NonNull Exception e) {
|
||||
return e instanceof PushNetworkException;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure() {
|
||||
}
|
||||
|
||||
private enum Type {
|
||||
UNKNOWN(0), ACCEPT(1), DELETE(2), BLOCK(3), BLOCK_AND_DELETE(4);
|
||||
|
||||
private final int value;
|
||||
|
||||
Type(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
int serialize() {
|
||||
return value;
|
||||
}
|
||||
|
||||
static @NonNull Type deserialize(int value) {
|
||||
for (Type type : Type.values()) {
|
||||
if (type.value == value) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
throw new AssertionError("Unknown type: " + value);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Factory implements Job.Factory<MultiDeviceMessageRequestResponseJob> {
|
||||
@Override
|
||||
public @NonNull
|
||||
MultiDeviceMessageRequestResponseJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
RecipientId threadRecipient = RecipientId.from(data.getString(KEY_THREAD_RECIPIENT));
|
||||
Type type = Type.deserialize(data.getInt(KEY_TYPE));
|
||||
|
||||
return new MultiDeviceMessageRequestResponseJob(parameters, threadRecipient, type);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -153,17 +153,19 @@ public class PushGroupSendJob extends PushSendJob {
|
|||
try {
|
||||
log(TAG, "Sending message: " + messageId);
|
||||
|
||||
if (FeatureFlags.messageRequests() && !message.getRecipient().resolve().isProfileSharing() && !database.isGroupQuitMessage(messageId)) {
|
||||
if (!message.getRecipient().resolve().isProfileSharing() && !database.isGroupQuitMessage(messageId)) {
|
||||
RecipientUtil.shareProfileIfFirstSecureMessage(context, message.getRecipient());
|
||||
}
|
||||
|
||||
List<RecipientId> target;
|
||||
|
||||
Recipient groupRecipient = message.getRecipient().fresh();
|
||||
|
||||
if (filterRecipient != null) target = Collections.singletonList(Recipient.resolved(filterRecipient).getId());
|
||||
else if (!existingNetworkFailures.isEmpty()) target = Stream.of(existingNetworkFailures).map(nf -> nf.getRecipientId(context)).toList();
|
||||
else target = getGroupMessageRecipients(message.getRecipient().requireGroupId(), messageId);
|
||||
else target = getGroupMessageRecipients(groupRecipient.requireGroupId(), messageId);
|
||||
|
||||
List<SendMessageResult> results = deliver(message, target);
|
||||
List<SendMessageResult> results = deliver(message, groupRecipient, target);
|
||||
List<NetworkFailure> networkFailures = Stream.of(results).filter(SendMessageResult::isNetworkFailure).map(result -> new NetworkFailure(Recipient.externalPush(context, result.getAddress()).getId())).toList();
|
||||
List<IdentityKeyMismatch> identityMismatches = Stream.of(results).filter(result -> result.getIdentityFailure() != null).map(result -> new IdentityKeyMismatch(Recipient.externalPush(context, result.getAddress()).getId(), result.getIdentityFailure().getIdentityKey())).toList();
|
||||
Set<RecipientId> successIds = Stream.of(results).filter(result -> result.getSuccess() != null).map(SendMessageResult::getAddress).map(a -> Recipient.externalPush(context, a).getId()).collect(Collectors.toSet());
|
||||
|
@ -235,13 +237,13 @@ public class PushGroupSendJob extends PushSendJob {
|
|||
DatabaseFactory.getMmsDatabase(context).markAsSentFailed(messageId);
|
||||
}
|
||||
|
||||
private List<SendMessageResult> deliver(OutgoingMediaMessage message, @NonNull List<RecipientId> destinations)
|
||||
private List<SendMessageResult> deliver(OutgoingMediaMessage message, @NonNull Recipient groupRecipient, @NonNull List<RecipientId> destinations)
|
||||
throws IOException, UntrustedIdentityException, UndeliverableMessageException {
|
||||
rotateSenderCertificateIfNecessary();
|
||||
|
||||
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
|
||||
String groupId = message.getRecipient().requireGroupId();
|
||||
Optional<byte[]> profileKey = getProfileKey(message.getRecipient());
|
||||
String groupId = groupRecipient.requireGroupId();
|
||||
Optional<byte[]> profileKey = getProfileKey(groupRecipient);
|
||||
Optional<Quote> quote = getQuoteFor(message);
|
||||
Optional<SignalServiceDataMessage.Sticker> sticker = getStickerFor(message);
|
||||
List<SharedContact> sharedContacts = getSharedContactsFor(message);
|
||||
|
@ -267,7 +269,7 @@ public class PushGroupSendJob extends PushSendJob {
|
|||
SignalServiceGroup group = new SignalServiceGroup(type, GroupUtil.getDecodedId(groupId), groupContext.getName(), members, avatar);
|
||||
SignalServiceDataMessage groupDataMessage = SignalServiceDataMessage.newBuilder()
|
||||
.withTimestamp(message.getSentTimeMillis())
|
||||
.withExpiration(message.getRecipient().getExpireMessages())
|
||||
.withExpiration(groupRecipient.getExpireMessages())
|
||||
.asGroupMessage(group)
|
||||
.build();
|
||||
|
||||
|
|
|
@ -120,7 +120,7 @@ public class PushMediaSendJob extends PushSendJob {
|
|||
|
||||
RecipientUtil.shareProfileIfFirstSecureMessage(context, message.getRecipient());
|
||||
|
||||
Recipient recipient = message.getRecipient().resolve();
|
||||
Recipient recipient = message.getRecipient().fresh();
|
||||
byte[] profileKey = recipient.getProfileKey();
|
||||
UnidentifiedAccessMode accessMode = recipient.getUnidentifiedAccessMode();
|
||||
|
||||
|
@ -193,11 +193,12 @@ public class PushMediaSendJob extends PushSendJob {
|
|||
try {
|
||||
rotateSenderCertificateIfNecessary();
|
||||
|
||||
Recipient messageRecipient = message.getRecipient().fresh();
|
||||
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
|
||||
SignalServiceAddress address = getPushAddress(message.getRecipient());
|
||||
SignalServiceAddress address = getPushAddress(messageRecipient);
|
||||
List<Attachment> attachments = Stream.of(message.getAttachments()).filterNot(Attachment::isSticker).toList();
|
||||
List<SignalServiceAttachment> serviceAttachments = getAttachmentPointersFor(attachments);
|
||||
Optional<byte[]> profileKey = getProfileKey(message.getRecipient());
|
||||
Optional<byte[]> profileKey = getProfileKey(messageRecipient);
|
||||
Optional<SignalServiceDataMessage.Quote> quote = getQuoteFor(message);
|
||||
Optional<SignalServiceDataMessage.Sticker> sticker = getStickerFor(message);
|
||||
List<SharedContact> sharedContacts = getSharedContactsFor(message);
|
||||
|
@ -223,7 +224,7 @@ public class PushMediaSendJob extends PushSendJob {
|
|||
messageSender.sendMessage(syncMessage, syncAccess);
|
||||
return syncAccess.isPresent();
|
||||
} else {
|
||||
return messageSender.sendMessage(address, UnidentifiedAccessUtil.getAccessFor(context, message.getRecipient()), mediaMessage).getSuccess().isUnidentified();
|
||||
return messageSender.sendMessage(address, UnidentifiedAccessUtil.getAccessFor(context, messageRecipient), mediaMessage).getSuccess().isUnidentified();
|
||||
}
|
||||
} catch (UnregisteredUserException e) {
|
||||
warn(TAG, e);
|
||||
|
|
|
@ -95,6 +95,7 @@ import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
|
|||
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.ConfigurationMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.MessageRequestResponseMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
|
||||
|
@ -295,6 +296,7 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
else if (syncMessage.getConfiguration().isPresent()) handleSynchronizeConfigurationMessage(syncMessage.getConfiguration().get());
|
||||
else if (syncMessage.getBlockedList().isPresent()) handleSynchronizeBlockedListMessage(syncMessage.getBlockedList().get());
|
||||
else if (syncMessage.getFetchType().isPresent()) handleSynchronizeFetchMessage(syncMessage.getFetchType().get());
|
||||
else if (syncMessage.getMessageRequestResponse().isPresent()) handleSynchronizeMessageRequestResponse(syncMessage.getMessageRequestResponse().get());
|
||||
else Log.w(TAG, "Contains no known sync types...");
|
||||
} else if (content.getCallMessage().isPresent()) {
|
||||
Log.i(TAG, "Got call message...");
|
||||
|
@ -659,6 +661,48 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
}
|
||||
}
|
||||
|
||||
private void handleSynchronizeMessageRequestResponse(@NonNull MessageRequestResponseMessage response) {
|
||||
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
||||
|
||||
Recipient recipient;
|
||||
|
||||
if (response.getPerson().isPresent()) {
|
||||
recipient = Recipient.externalPush(context, response.getPerson().get());
|
||||
} else if (response.getGroupId().isPresent()) {
|
||||
String groupId = GroupUtil.getEncodedId(response.getGroupId().get(), false);
|
||||
recipient = Recipient.externalGroup(context, groupId);
|
||||
} else {
|
||||
Log.w(TAG, "Message request response was missing a thread recipient! Skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
long threadId = threadDatabase.getThreadIdFor(recipient);
|
||||
|
||||
switch (response.getType()) {
|
||||
case ACCEPT:
|
||||
recipientDatabase.setProfileSharing(recipient.getId(), true);
|
||||
recipientDatabase.setBlocked(recipient.getId(), false);
|
||||
break;
|
||||
case DELETE:
|
||||
recipientDatabase.setProfileSharing(recipient.getId(), false);
|
||||
if (threadId > 0) threadDatabase.deleteConversation(threadId);
|
||||
break;
|
||||
case BLOCK:
|
||||
recipientDatabase.setBlocked(recipient.getId(), true);
|
||||
recipientDatabase.setProfileSharing(recipient.getId(), false);
|
||||
break;
|
||||
case BLOCK_AND_DELETE:
|
||||
recipientDatabase.setBlocked(recipient.getId(), true);
|
||||
recipientDatabase.setProfileSharing(recipient.getId(), false);
|
||||
if (threadId > 0) threadDatabase.deleteConversation(threadId);
|
||||
break;
|
||||
default:
|
||||
Log.w(TAG, "Got an unknown response type! Skipping");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void handleSynchronizeSentMessage(@NonNull SignalServiceContent content,
|
||||
@NonNull SentTranscriptMessage message)
|
||||
throws StorageFailedException
|
||||
|
|
|
@ -83,7 +83,7 @@ public class PushTextSendJob extends PushSendJob {
|
|||
|
||||
RecipientUtil.shareProfileIfFirstSecureMessage(context, record.getRecipient());
|
||||
|
||||
Recipient recipient = record.getRecipient().resolve();
|
||||
Recipient recipient = record.getRecipient().fresh();
|
||||
byte[] profileKey = recipient.getProfileKey();
|
||||
UnidentifiedAccessMode accessMode = recipient.getUnidentifiedAccessMode();
|
||||
|
||||
|
@ -154,10 +154,11 @@ public class PushTextSendJob extends PushSendJob {
|
|||
try {
|
||||
rotateSenderCertificateIfNecessary();
|
||||
|
||||
Recipient messageRecipient = message.getIndividualRecipient().fresh();
|
||||
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
|
||||
SignalServiceAddress address = getPushAddress(message.getIndividualRecipient());
|
||||
Optional<byte[]> profileKey = getProfileKey(message.getIndividualRecipient());
|
||||
Optional<UnidentifiedAccessPair> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, message.getIndividualRecipient());
|
||||
SignalServiceAddress address = getPushAddress(messageRecipient);
|
||||
Optional<byte[]> profileKey = getProfileKey(messageRecipient);
|
||||
Optional<UnidentifiedAccessPair> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, messageRecipient);
|
||||
|
||||
log(TAG, "Have access key to use: " + unidentifiedAccess.isPresent());
|
||||
|
||||
|
|
|
@ -43,7 +43,6 @@ public class RemoteConfigRefreshJob extends BaseJob {
|
|||
protected void onRun() throws Exception {
|
||||
Map<String, Boolean> config = ApplicationDependencies.getSignalServiceAccountManager().getRemoteConfig();
|
||||
FeatureFlags.update(config);
|
||||
SignalStore.setRemoteConfigLastFetchTime(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -92,7 +92,7 @@ public class SendReadReceiptJob extends BaseJob {
|
|||
public void onRun() throws IOException, UntrustedIdentityException {
|
||||
if (!TextSecurePreferences.isReadReceiptsEnabled(context) || messageIds.isEmpty()) return;
|
||||
|
||||
if (!RecipientUtil.isThreadMessageRequestAccepted(context, threadId)) {
|
||||
if (!RecipientUtil.isMessageRequestAccepted(context, threadId)) {
|
||||
Log.w(TAG, "Refusing to send receipts to untrusted recipient");
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
package org.thoughtcrime.securesms.keyvalue;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
public final class RemoteConfigValues {
|
||||
|
||||
private static final String TAG = Log.tag(RemoteConfigValues.class);
|
||||
|
||||
private static final String CURRENT_CONFIG = "remote_config";
|
||||
private static final String PENDING_CONFIG = "pending_remote_config";
|
||||
private static final String LAST_FETCH_TIME = "remote_config_last_fetch_time";
|
||||
|
||||
private final KeyValueStore store;
|
||||
|
||||
RemoteConfigValues(KeyValueStore store) {
|
||||
this.store = store;
|
||||
}
|
||||
|
||||
public String getCurrentConfig() {
|
||||
return store.getString(CURRENT_CONFIG, null);
|
||||
}
|
||||
|
||||
public void setCurrentConfig(String value) {
|
||||
store.beginWrite().putString(CURRENT_CONFIG, value).apply();
|
||||
}
|
||||
|
||||
public String getPendingConfig() {
|
||||
return store.getString(PENDING_CONFIG, getCurrentConfig());
|
||||
}
|
||||
|
||||
public void setPendingConfig(String value) {
|
||||
store.beginWrite().putString(PENDING_CONFIG, value).apply();
|
||||
}
|
||||
|
||||
public long getLastFetchTime() {
|
||||
return store.getLong(LAST_FETCH_TIME, 0);
|
||||
}
|
||||
|
||||
public void setLastFetchTime(long time) {
|
||||
store.beginWrite().putLong(LAST_FETCH_TIME, time).apply();
|
||||
}
|
||||
}
|
|
@ -1,7 +1,5 @@
|
|||
package org.thoughtcrime.securesms.keyvalue;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
|
@ -12,9 +10,8 @@ import org.thoughtcrime.securesms.logging.SignalUncaughtExceptionHandler;
|
|||
*/
|
||||
public final class SignalStore {
|
||||
|
||||
private static final String REMOTE_CONFIG = "remote_config";
|
||||
private static final String REMOTE_CONFIG_LAST_FETCH_TIME = "remote_config_last_fetch_time";
|
||||
private static final String LAST_PREKEY_REFRESH_TIME = "last_prekey_refresh_time";
|
||||
private static final String MESSAGE_REQUEST_ENABLE_TIME = "message_request_enable_time";
|
||||
|
||||
private SignalStore() {}
|
||||
|
||||
|
@ -30,20 +27,8 @@ public final class SignalStore {
|
|||
return new PinValues(getStore());
|
||||
}
|
||||
|
||||
public static String getRemoteConfig() {
|
||||
return getStore().getString(REMOTE_CONFIG, null);
|
||||
}
|
||||
|
||||
public static void setRemoteConfig(String value) {
|
||||
putString(REMOTE_CONFIG, value);
|
||||
}
|
||||
|
||||
public static long getRemoteConfigLastFetchTime() {
|
||||
return getStore().getLong(REMOTE_CONFIG_LAST_FETCH_TIME, 0);
|
||||
}
|
||||
|
||||
public static void setRemoteConfigLastFetchTime(long time) {
|
||||
putLong(REMOTE_CONFIG_LAST_FETCH_TIME, time);
|
||||
public static @NonNull RemoteConfigValues remoteConfigValues() {
|
||||
return new RemoteConfigValues(getStore());
|
||||
}
|
||||
|
||||
public static long getLastPrekeyRefreshTime() {
|
||||
|
@ -54,6 +39,14 @@ public final class SignalStore {
|
|||
putLong(LAST_PREKEY_REFRESH_TIME, time);
|
||||
}
|
||||
|
||||
public static long getMessageRequestEnableTime() {
|
||||
return getStore().getLong(MESSAGE_REQUEST_ENABLE_TIME, 0);
|
||||
}
|
||||
|
||||
public static void setMessageRequestEnableTime(long time) {
|
||||
putLong(MESSAGE_REQUEST_ENABLE_TIME, time);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures any pending writes are finished. Only intended to be called by
|
||||
* {@link SignalUncaughtExceptionHandler}.
|
||||
|
|
|
@ -24,7 +24,6 @@ import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
|
|||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender.PreUploadResult;
|
||||
import org.thoughtcrime.securesms.util.DiffHelper;
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package org.thoughtcrime.securesms.messagerequests;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.util.Consumer;
|
||||
|
@ -9,56 +8,62 @@ import androidx.core.util.Consumer;
|
|||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceMessageRequestResponseJob;
|
||||
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
|
||||
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
public class MessageRequestRepository {
|
||||
|
||||
private final Context context;
|
||||
private final Executor executor;
|
||||
|
||||
public MessageRequestRepository(@NonNull Context context) {
|
||||
MessageRequestRepository(@NonNull Context context) {
|
||||
this.context = context.getApplicationContext();
|
||||
this.executor = SignalExecutors.BOUNDED;
|
||||
}
|
||||
|
||||
public LiveRecipient getLiveRecipient(@NonNull RecipientId recipientId) {
|
||||
return Recipient.live(recipientId);
|
||||
}
|
||||
|
||||
public void getGroups(@NonNull RecipientId recipientId, @NonNull Consumer<List<String>> onGroupsLoaded) {
|
||||
SimpleTask.run(() -> {
|
||||
void getGroups(@NonNull RecipientId recipientId, @NonNull Consumer<List<String>> onGroupsLoaded) {
|
||||
executor.execute(() -> {
|
||||
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||
return groupDatabase.getGroupNamesContainingMember(recipientId);
|
||||
}, onGroupsLoaded::accept);
|
||||
onGroupsLoaded.accept(groupDatabase.getGroupNamesContainingMember(recipientId));
|
||||
});
|
||||
}
|
||||
|
||||
public void getMemberCount(@NonNull RecipientId recipientId, @NonNull Consumer<Integer> onMemberCountLoaded) {
|
||||
SimpleTask.run(() -> {
|
||||
void getMemberCount(@NonNull RecipientId recipientId, @NonNull Consumer<Integer> onMemberCountLoaded) {
|
||||
executor.execute(() -> {
|
||||
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||
Optional<GroupDatabase.GroupRecord> groupRecord = groupDatabase.getGroup(recipientId);
|
||||
return groupRecord.transform(record -> record.getMembers().size()).or(0);
|
||||
}, onMemberCountLoaded::accept);
|
||||
onMemberCountLoaded.accept(groupRecord.transform(record -> record.getMembers().size()).or(0));
|
||||
});
|
||||
}
|
||||
|
||||
public void getMessageRequestAccepted(long threadId, @NonNull Consumer<Boolean> recipientRequestAccepted) {
|
||||
SimpleTask.run(() -> RecipientUtil.isThreadMessageRequestAccepted(context, threadId),
|
||||
recipientRequestAccepted::accept);
|
||||
void getMessageRequestState(@NonNull Recipient recipient, long threadId, @NonNull Consumer<MessageRequestState> state) {
|
||||
executor.execute(() -> {
|
||||
if (!RecipientUtil.isMessageRequestAccepted(context, threadId)) {
|
||||
state.accept(MessageRequestState.UNACCEPTED);
|
||||
} else if (RecipientUtil.isPreMessageRequestThread(context, threadId) && !RecipientUtil.isLegacyProfileSharingAccepted(recipient)) {
|
||||
state.accept(MessageRequestState.LEGACY);
|
||||
} else {
|
||||
state.accept(MessageRequestState.ACCEPTED);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void acceptMessageRequest(@NonNull LiveRecipient liveRecipient, long threadId, @NonNull Runnable onMessageRequestAccepted) {
|
||||
SimpleTask.run(() -> {
|
||||
void acceptMessageRequest(@NonNull LiveRecipient liveRecipient, long threadId, @NonNull Runnable onMessageRequestAccepted) {
|
||||
executor.execute(()-> {
|
||||
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||
recipientDatabase.setProfileSharing(liveRecipient.getId(), true);
|
||||
liveRecipient.refresh();
|
||||
|
@ -68,24 +73,84 @@ public class MessageRequestRepository {
|
|||
MessageNotifier.updateNotification(context);
|
||||
MarkReadReceiver.process(context, messageIds);
|
||||
|
||||
return null;
|
||||
}, v -> onMessageRequestAccepted.run());
|
||||
if (TextSecurePreferences.isMultiDevice(context)) {
|
||||
ApplicationDependencies.getJobManager().add(MultiDeviceMessageRequestResponseJob.forAccept(liveRecipient.getId()));
|
||||
}
|
||||
|
||||
public void deleteMessageRequest(long threadId, @NonNull Runnable onMessageRequestDeleted) {
|
||||
SimpleTask.run(() -> {
|
||||
onMessageRequestAccepted.run();
|
||||
});
|
||||
}
|
||||
|
||||
void deleteMessageRequest(@NonNull LiveRecipient recipient, long threadId, @NonNull Runnable onMessageRequestDeleted) {
|
||||
executor.execute(() -> {
|
||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
||||
threadDatabase.deleteConversation(threadId);
|
||||
return null;
|
||||
}, v -> onMessageRequestDeleted.run());
|
||||
|
||||
if (recipient.resolve().isGroup()) {
|
||||
RecipientUtil.leaveGroup(context, recipient.get());
|
||||
}
|
||||
|
||||
public void blockMessageRequest(@NonNull LiveRecipient liveRecipient, @NonNull Runnable onMessageRequestBlocked) {
|
||||
SimpleTask.run(() -> {
|
||||
if (TextSecurePreferences.isMultiDevice(context)) {
|
||||
ApplicationDependencies.getJobManager().add(MultiDeviceMessageRequestResponseJob.forDelete(recipient.getId()));
|
||||
}
|
||||
|
||||
onMessageRequestDeleted.run();
|
||||
});
|
||||
}
|
||||
|
||||
void blockMessageRequest(@NonNull LiveRecipient liveRecipient, @NonNull Runnable onMessageRequestBlocked) {
|
||||
executor.execute(() -> {
|
||||
Recipient recipient = liveRecipient.resolve();
|
||||
RecipientUtil.block(context, recipient);
|
||||
liveRecipient.refresh();
|
||||
return null;
|
||||
}, v -> onMessageRequestBlocked.run());
|
||||
|
||||
if (TextSecurePreferences.isMultiDevice(context)) {
|
||||
ApplicationDependencies.getJobManager().add(MultiDeviceMessageRequestResponseJob.forBlock(liveRecipient.getId()));
|
||||
}
|
||||
|
||||
onMessageRequestBlocked.run();
|
||||
});
|
||||
}
|
||||
|
||||
void blockAndDeleteMessageRequest(@NonNull LiveRecipient liveRecipient, long threadId, @NonNull Runnable onMessageRequestBlocked) {
|
||||
executor.execute(() -> {
|
||||
Recipient recipient = liveRecipient.resolve();
|
||||
RecipientUtil.block(context, recipient);
|
||||
liveRecipient.refresh();
|
||||
|
||||
DatabaseFactory.getThreadDatabase(context).deleteConversation(threadId);
|
||||
|
||||
if (TextSecurePreferences.isMultiDevice(context)) {
|
||||
ApplicationDependencies.getJobManager().add(MultiDeviceMessageRequestResponseJob.forBlockAndDelete(liveRecipient.getId()));
|
||||
}
|
||||
|
||||
onMessageRequestBlocked.run();
|
||||
});
|
||||
}
|
||||
|
||||
void unblockAndAccept(@NonNull LiveRecipient liveRecipient, long threadId, @NonNull Runnable onMessageRequestUnblocked) {
|
||||
executor.execute(() -> {
|
||||
Recipient recipient = liveRecipient.resolve();
|
||||
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||
|
||||
RecipientUtil.unblock(context, recipient);
|
||||
recipientDatabase.setProfileSharing(liveRecipient.getId(), true);
|
||||
liveRecipient.refresh();
|
||||
|
||||
List<MessagingDatabase.MarkedMessageInfo> messageIds = DatabaseFactory.getThreadDatabase(context)
|
||||
.setEntireThreadRead(threadId);
|
||||
MessageNotifier.updateNotification(context);
|
||||
MarkReadReceiver.process(context, messageIds);
|
||||
|
||||
if (TextSecurePreferences.isMultiDevice(context)) {
|
||||
ApplicationDependencies.getJobManager().add(MultiDeviceMessageRequestResponseJob.forAccept(liveRecipient.getId()));
|
||||
}
|
||||
|
||||
onMessageRequestUnblocked.run();
|
||||
});
|
||||
}
|
||||
|
||||
enum MessageRequestState {
|
||||
ACCEPTED, UNACCEPTED, LEGACY
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
|||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataTriple;
|
||||
|
@ -28,7 +29,7 @@ public class MessageRequestViewModel extends ViewModel {
|
|||
private final MutableLiveData<Recipient> recipient = new MutableLiveData<>();
|
||||
private final MutableLiveData<List<String>> groups = new MutableLiveData<>(Collections.emptyList());
|
||||
private final MutableLiveData<Integer> memberCount = new MutableLiveData<>(0);
|
||||
private final MutableLiveData<Boolean> shouldDisplayMessageRequest = new MutableLiveData<>();
|
||||
private final MutableLiveData<DisplayState> displayState = new MutableLiveData<>();
|
||||
private final LiveData<RecipientInfo> recipientInfo = Transformations.map(new LiveDataTriple<>(recipient, memberCount, groups),
|
||||
triple -> new RecipientInfo(triple.first(), triple.second(), triple.third()));
|
||||
|
||||
|
@ -39,11 +40,7 @@ public class MessageRequestViewModel extends ViewModel {
|
|||
|
||||
@SuppressWarnings("CodeBlock2Expr")
|
||||
private final RecipientForeverObserver recipientObserver = recipient -> {
|
||||
if (Recipient.self().equals(recipient) || recipient.isBlocked() || recipient.isForceSmsSelection() || !recipient.isRegistered()) {
|
||||
shouldDisplayMessageRequest.setValue(false);
|
||||
} else {
|
||||
loadMessageRequestAccepted();
|
||||
}
|
||||
loadMessageRequestAccepted(recipient);
|
||||
this.recipient.setValue(recipient);
|
||||
};
|
||||
|
||||
|
@ -71,8 +68,8 @@ public class MessageRequestViewModel extends ViewModel {
|
|||
}
|
||||
}
|
||||
|
||||
public LiveData<Boolean> getShouldDisplayMessageRequest() {
|
||||
return shouldDisplayMessageRequest;
|
||||
public LiveData<DisplayState> getMessageRequestDisplayState() {
|
||||
return displayState;
|
||||
}
|
||||
|
||||
public LiveData<Recipient> getRecipient() {
|
||||
|
@ -83,28 +80,46 @@ public class MessageRequestViewModel extends ViewModel {
|
|||
return recipientInfo;
|
||||
}
|
||||
|
||||
public LiveData<Status> getMesasgeRequestStatus() {
|
||||
public LiveData<Status> getMessageRequestStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public boolean shouldShowMessageRequest() {
|
||||
return displayState.getValue() == DisplayState.DISPLAY_MESSAGE_REQUEST;
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public void accept() {
|
||||
public void onAccept() {
|
||||
repository.acceptMessageRequest(liveRecipient, threadId, () -> {
|
||||
status.setValue(Status.ACCEPTED);
|
||||
status.postValue(Status.ACCEPTED);
|
||||
});
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public void delete() {
|
||||
repository.deleteMessageRequest(threadId, () -> {
|
||||
status.setValue(Status.DELETED);
|
||||
public void onDelete() {
|
||||
repository.deleteMessageRequest(liveRecipient, threadId, () -> {
|
||||
status.postValue(Status.DELETED);
|
||||
});
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public void block() {
|
||||
public void onBlock() {
|
||||
repository.blockMessageRequest(liveRecipient, () -> {
|
||||
status.setValue(Status.BLOCKED);
|
||||
status.postValue(Status.BLOCKED);
|
||||
});
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public void onUnblock() {
|
||||
repository.unblockAndAccept(liveRecipient, threadId, () -> {
|
||||
status.postValue(Status.ACCEPTED);
|
||||
});
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public void onBlockAndDelete() {
|
||||
repository.blockAndDeleteMessageRequest(liveRecipient, threadId, () -> {
|
||||
status.postValue(Status.BLOCKED);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -114,33 +129,35 @@ public class MessageRequestViewModel extends ViewModel {
|
|||
}
|
||||
|
||||
private void loadGroups() {
|
||||
repository.getGroups(liveRecipient.getId(), this.groups::setValue);
|
||||
repository.getGroups(liveRecipient.getId(), this.groups::postValue);
|
||||
}
|
||||
|
||||
private void loadMemberCount() {
|
||||
repository.getMemberCount(liveRecipient.getId(), memberCount -> {
|
||||
this.memberCount.setValue(memberCount == null ? 0 : memberCount);
|
||||
this.memberCount.postValue(memberCount == null ? 0 : memberCount);
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
private void loadMessageRequestAccepted() {
|
||||
repository.getMessageRequestAccepted(threadId, accepted -> shouldDisplayMessageRequest.setValue(!accepted));
|
||||
private void loadMessageRequestAccepted(@NonNull Recipient recipient) {
|
||||
if (FeatureFlags.messageRequests() && recipient.isBlocked()) {
|
||||
displayState.postValue(DisplayState.DISPLAY_MESSAGE_REQUEST);
|
||||
return;
|
||||
}
|
||||
|
||||
public static class Factory implements ViewModelProvider.Factory {
|
||||
|
||||
private final Context context;
|
||||
|
||||
public Factory(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
return (T) new MessageRequestViewModel(new MessageRequestRepository(context.getApplicationContext()));
|
||||
repository.getMessageRequestState(recipient, threadId, accepted -> {
|
||||
switch (accepted) {
|
||||
case ACCEPTED:
|
||||
displayState.postValue(DisplayState.DISPLAY_NONE);
|
||||
break;
|
||||
case UNACCEPTED:
|
||||
displayState.postValue(DisplayState.DISPLAY_MESSAGE_REQUEST);
|
||||
break;
|
||||
case LEGACY:
|
||||
displayState.postValue(DisplayState.DISPLAY_LEGACY);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static class RecipientInfo {
|
||||
|
@ -174,4 +191,24 @@ public class MessageRequestViewModel extends ViewModel {
|
|||
DELETED,
|
||||
ACCEPTED
|
||||
}
|
||||
|
||||
public enum DisplayState {
|
||||
DISPLAY_MESSAGE_REQUEST, DISPLAY_LEGACY, DISPLAY_NONE
|
||||
}
|
||||
|
||||
public static class Factory implements ViewModelProvider.Factory {
|
||||
|
||||
private final Context context;
|
||||
|
||||
public Factory(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
//noinspection unchecked
|
||||
return (T) new MessageRequestViewModel(new MessageRequestRepository(context.getApplicationContext()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -5,9 +5,14 @@ import android.util.AttributeSet;
|
|||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.constraintlayout.widget.Group;
|
||||
import androidx.core.text.HtmlCompat;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.HtmlUtil;
|
||||
|
||||
public class MessageRequestsBottomView extends ConstraintLayout {
|
||||
|
||||
|
@ -15,6 +20,11 @@ public class MessageRequestsBottomView extends ConstraintLayout {
|
|||
private View accept;
|
||||
private View block;
|
||||
private View delete;
|
||||
private View bigDelete;
|
||||
private View bigUnblock;
|
||||
|
||||
private Group normalButtons;
|
||||
private Group blockedButtons;
|
||||
|
||||
public MessageRequestsBottomView(Context context) {
|
||||
super(context);
|
||||
|
@ -38,10 +48,30 @@ public class MessageRequestsBottomView extends ConstraintLayout {
|
|||
accept = findViewById(R.id.message_request_accept);
|
||||
block = findViewById(R.id.message_request_block);
|
||||
delete = findViewById(R.id.message_request_delete);
|
||||
bigDelete = findViewById(R.id.message_request_big_delete);
|
||||
bigUnblock = findViewById(R.id.message_request_big_unblock);
|
||||
normalButtons = findViewById(R.id.message_request_normal_buttons);
|
||||
blockedButtons = findViewById(R.id.message_request_blocked_buttons);
|
||||
}
|
||||
|
||||
public void setQuestionText(CharSequence questionText) {
|
||||
question.setText(questionText);
|
||||
public void setRecipient(@NonNull Recipient recipient) {
|
||||
if (recipient.isBlocked()) {
|
||||
if (recipient.isGroup()) {
|
||||
question.setText(R.string.MessageRequestBottomView_unblock_to_allow_group_members_to_add_you_to_this_group_again);
|
||||
} else {
|
||||
question.setText(HtmlCompat.fromHtml(getContext().getString(R.string.MessageRequestBottomView_unblock_s_to_message_and_call_each_other, HtmlUtil.bold(recipient.getDisplayName(getContext()))), 0));
|
||||
}
|
||||
normalButtons.setVisibility(GONE);
|
||||
blockedButtons.setVisibility(VISIBLE);
|
||||
} else {
|
||||
if (recipient.isGroup()) {
|
||||
question.setText(HtmlCompat.fromHtml(getContext().getString(R.string.MessageRequestBottomView_do_you_want_to_join_the_group_s_they_wont_know_youve_seen_their_messages_until_you_accept, HtmlUtil.bold(recipient.getDisplayName(getContext()))), 0));
|
||||
} else {
|
||||
question.setText(HtmlCompat.fromHtml(getContext().getString(R.string.MessageRequestBottomView_do_you_want_to_let_s_message_you_they_wont_know_youve_seen_their_messages_until_you_accept, HtmlUtil.bold(recipient.getDisplayName(getContext()))), 0));
|
||||
}
|
||||
normalButtons.setVisibility(VISIBLE);
|
||||
blockedButtons.setVisibility(GONE);
|
||||
}
|
||||
}
|
||||
|
||||
public void setAcceptOnClickListener(OnClickListener acceptOnClickListener) {
|
||||
|
@ -50,9 +80,14 @@ public class MessageRequestsBottomView extends ConstraintLayout {
|
|||
|
||||
public void setDeleteOnClickListener(OnClickListener deleteOnClickListener) {
|
||||
delete.setOnClickListener(deleteOnClickListener);
|
||||
bigDelete.setOnClickListener(deleteOnClickListener);
|
||||
}
|
||||
|
||||
public void setBlockOnClickListener(OnClickListener blockOnClickListener) {
|
||||
block.setOnClickListener(blockOnClickListener);
|
||||
}
|
||||
|
||||
public void setUnblockOnClickListener(OnClickListener unblockOnClickListener) {
|
||||
bigUnblock.setOnClickListener(unblockOnClickListener);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -368,7 +368,7 @@ public class MessageNotifier {
|
|||
long timestamp = notifications.get(0).getTimestamp();
|
||||
if (timestamp != 0) builder.setWhen(timestamp);
|
||||
|
||||
if (!KeyCachingService.isLocked(context) && RecipientUtil.isRecipientMessageRequestAccepted(context, recipient.resolve())) {
|
||||
if (!KeyCachingService.isLocked(context) && RecipientUtil.isMessageRequestAccepted(context, recipient.resolve())) {
|
||||
ReplyMethod replyMethod = ReplyMethod.forRecipient(context, recipient);
|
||||
|
||||
builder.addActions(notificationState.getMarkAsReadIntent(context, notificationId),
|
||||
|
|
|
@ -12,7 +12,6 @@ import org.thoughtcrime.securesms.R;
|
|||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.reactions.ReactionsLoader.Reaction;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.AvatarUtil;
|
||||
|
||||
import java.util.Collections;
|
||||
|
|
|
@ -230,6 +230,19 @@ public class Recipient {
|
|||
return Recipient.resolved(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* A version of {@link #external(Context, String)} that should be used when you know the
|
||||
* identifier is a groupId.
|
||||
*/
|
||||
@WorkerThread
|
||||
public static @NonNull Recipient externalGroup(@NonNull Context context, @NonNull String groupId) {
|
||||
if (!GroupUtil.isEncodedGroup(groupId)) {
|
||||
throw new IllegalArgumentException("Invalid groupId!");
|
||||
}
|
||||
|
||||
return Recipient.resolved(DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a fully-populated {@link Recipient} based off of a string identifier, creating one in
|
||||
* the database if necessary. The identifier may be a uuid, phone number, email,
|
||||
|
@ -706,7 +719,11 @@ public class Recipient {
|
|||
return contactUri != null;
|
||||
}
|
||||
|
||||
public Recipient resolve() {
|
||||
/**
|
||||
* If this recipient is missing crucial data, this will return a populated copy. Otherwise it
|
||||
* returns itself.
|
||||
*/
|
||||
public @NonNull Recipient resolve() {
|
||||
if (resolving) {
|
||||
return live().resolve();
|
||||
} else {
|
||||
|
@ -718,6 +735,13 @@ public class Recipient {
|
|||
return resolving;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces retrieving a fresh copy of the recipient, regardless of its state.
|
||||
*/
|
||||
public @NonNull Recipient fresh() {
|
||||
return live().resolve();
|
||||
}
|
||||
|
||||
public @NonNull LiveRecipient live() {
|
||||
return ApplicationDependencies.getRecipientCache().getLive(id);
|
||||
}
|
||||
|
|
|
@ -11,18 +11,18 @@ import org.thoughtcrime.securesms.R;
|
|||
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
|
||||
import org.thoughtcrime.securesms.jobs.LeaveGroupJob;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceMessageRequestResponseJob;
|
||||
import org.thoughtcrime.securesms.jobs.RotateProfileKeyJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
@ -76,25 +76,13 @@ public class RecipientUtil {
|
|||
|
||||
DatabaseFactory.getRecipientDatabase(context).setBlocked(resolved.getId(), true);
|
||||
|
||||
if (resolved.isGroup() && DatabaseFactory.getGroupDatabase(context).isActive(resolved.requireGroupId())) {
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(resolved);
|
||||
Optional<OutgoingGroupMediaMessage> leaveMessage = GroupUtil.createGroupLeaveMessage(context, resolved);
|
||||
|
||||
if (threadId != -1 && leaveMessage.isPresent()) {
|
||||
MessageSender.send(context, leaveMessage.get(), threadId, false, null);
|
||||
|
||||
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||
String groupId = resolved.requireGroupId();
|
||||
groupDatabase.setActive(groupId, false);
|
||||
groupDatabase.remove(groupId, Recipient.self().getId());
|
||||
} else {
|
||||
Log.w(TAG, "Failed to leave group. Can't block.");
|
||||
Toast.makeText(context, R.string.RecipientPreferenceActivity_error_leaving_group, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
if (resolved.isGroup()) {
|
||||
leaveGroup(context, recipient);
|
||||
}
|
||||
|
||||
if (resolved.isSystemContact() || resolved.isProfileSharing()) {
|
||||
ApplicationDependencies.getJobManager().add(new RotateProfileKeyJob());
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(resolved.getId(), false);
|
||||
}
|
||||
|
||||
ApplicationDependencies.getJobManager().add(new MultiDeviceBlockedUpdateJob());
|
||||
|
@ -108,43 +96,87 @@ public class RecipientUtil {
|
|||
|
||||
DatabaseFactory.getRecipientDatabase(context).setBlocked(recipient.getId(), false);
|
||||
ApplicationDependencies.getJobManager().add(new MultiDeviceBlockedUpdateJob());
|
||||
|
||||
if (FeatureFlags.messageRequests()) {
|
||||
ApplicationDependencies.getJobManager().add(MultiDeviceMessageRequestResponseJob.forAccept(recipient.getId()));
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static boolean isThreadMessageRequestAccepted(@NonNull Context context, long threadId) {
|
||||
if (!FeatureFlags.messageRequests()) {
|
||||
public static void leaveGroup(@NonNull Context context, @NonNull Recipient recipient) {
|
||||
Recipient resolved = recipient.resolve();
|
||||
|
||||
if (!resolved.isGroup()) {
|
||||
throw new AssertionError("Not a group!");
|
||||
}
|
||||
|
||||
if (DatabaseFactory.getGroupDatabase(context).isActive(resolved.requireGroupId())) {
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(resolved);
|
||||
Optional<OutgoingGroupMediaMessage> leaveMessage = GroupUtil.createGroupLeaveMessage(context, resolved);
|
||||
|
||||
if (threadId != -1 && leaveMessage.isPresent()) {
|
||||
ApplicationDependencies.getJobManager().add(LeaveGroupJob.create(recipient));
|
||||
|
||||
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||
String groupId = resolved.requireGroupId();
|
||||
groupDatabase.setActive(groupId, false);
|
||||
groupDatabase.remove(groupId, Recipient.self().getId());
|
||||
} else {
|
||||
Log.w(TAG, "Failed to leave group.");
|
||||
Toast.makeText(context, R.string.RecipientPreferenceActivity_error_leaving_group, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
} else {
|
||||
Log.i(TAG, "Group was already inactive. Skipping.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If true, the new message request UI does not need to be shown, and it's safe to send read
|
||||
* receipts.
|
||||
*
|
||||
* Note that this does not imply that a user has explicitly accepted a message request -- it could
|
||||
* also be the case that the thread in question is for a system contact or something of the like.
|
||||
*/
|
||||
@WorkerThread
|
||||
public static boolean isMessageRequestAccepted(@NonNull Context context, long threadId) {
|
||||
if (!FeatureFlags.messageRequests() || threadId < 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
||||
Recipient recipient = threadDatabase.getRecipientForThreadId(threadId);
|
||||
boolean hasSentSecureMessage = DatabaseFactory.getMmsSmsDatabase(context)
|
||||
.getOutgoingSecureConversationCount(threadId) != 0;
|
||||
boolean noSecureMessagesInThread = DatabaseFactory.getMmsSmsDatabase(context)
|
||||
.getSecureConversationCount(threadId) == 0;
|
||||
Recipient threadRecipient = threadDatabase.getRecipientForThreadId(threadId);
|
||||
|
||||
if (recipient == null || hasSentSecureMessage || noSecureMessagesInThread) {
|
||||
if (threadRecipient == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Recipient resolved = recipient.resolve();
|
||||
|
||||
return resolved.isProfileSharing() || resolved.isSystemContact();
|
||||
return isMessageRequestAccepted(context, threadId, threadRecipient);
|
||||
}
|
||||
|
||||
/**
|
||||
* See {@link #isMessageRequestAccepted(Context, long)}.
|
||||
*/
|
||||
@WorkerThread
|
||||
public static boolean isRecipientMessageRequestAccepted(@NonNull Context context, @Nullable Recipient recipient) {
|
||||
if (recipient == null || !FeatureFlags.messageRequests()) return true;
|
||||
public static boolean isMessageRequestAccepted(@NonNull Context context, @Nullable Recipient threadRecipient) {
|
||||
if (!FeatureFlags.messageRequests() || threadRecipient == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Recipient resolved = recipient.resolve();
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(threadRecipient);
|
||||
return isMessageRequestAccepted(context, threadId, threadRecipient);
|
||||
}
|
||||
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(resolved);
|
||||
boolean hasSentMessage = DatabaseFactory.getMmsSmsDatabase(context)
|
||||
.getOutgoingSecureConversationCount(threadId) != 0;
|
||||
boolean noSecureMessagesInThread = DatabaseFactory.getMmsSmsDatabase(context)
|
||||
.getSecureConversationCount(threadId) == 0;
|
||||
/**
|
||||
* @return True if a conversation existed before we enabled message requests, otherwise false.
|
||||
*/
|
||||
@WorkerThread
|
||||
public static boolean isPreMessageRequestThread(@NonNull Context context, long threadId) {
|
||||
if (!FeatureFlags.messageRequests()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return noSecureMessagesInThread || hasSentMessage || resolved.isProfileSharing() || resolved.isSystemContact();
|
||||
long beforeTime = SignalStore.getMessageRequestEnableTime();
|
||||
return DatabaseFactory.getMmsSmsDatabase(context).getConversationCount(threadId, beforeTime) > 0;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
|
@ -154,11 +186,44 @@ public class RecipientUtil {
|
|||
}
|
||||
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(recipient);
|
||||
boolean firstMessage = DatabaseFactory.getMmsSmsDatabase(context)
|
||||
.getOutgoingSecureConversationCount(threadId) == 0;
|
||||
|
||||
if (isPreMessageRequestThread(context, threadId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean firstMessage = DatabaseFactory.getMmsSmsDatabase(context).getOutgoingSecureConversationCount(threadId) == 0;
|
||||
|
||||
if (firstMessage) {
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient.getId(), true);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isLegacyProfileSharingAccepted(@NonNull Recipient threadRecipient) {
|
||||
return threadRecipient.isLocalNumber() ||
|
||||
threadRecipient.isProfileSharing() ||
|
||||
threadRecipient.isSystemContact() ||
|
||||
!threadRecipient.isRegistered();
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private static boolean isMessageRequestAccepted(@NonNull Context context, long threadId, @NonNull Recipient threadRecipient) {
|
||||
return threadRecipient.isLocalNumber() ||
|
||||
threadRecipient.isProfileSharing() ||
|
||||
threadRecipient.isSystemContact() ||
|
||||
threadRecipient.isForceSmsSelection() ||
|
||||
!threadRecipient.isRegistered() ||
|
||||
hasSentMessageInThread(context, threadId) ||
|
||||
noSecureMessagesInThread(context, threadId) ||
|
||||
isPreMessageRequestThread(context, threadId);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private static boolean hasSentMessageInThread(@NonNull Context context, long threadId) {
|
||||
return DatabaseFactory.getMmsSmsDatabase(context).getOutgoingSecureConversationCount(threadId) != 0;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private static boolean noSecureMessagesInThread(@NonNull Context context, long threadId) {
|
||||
return DatabaseFactory.getMmsSmsDatabase(context).getSecureConversationCount(threadId) == 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ import java.util.concurrent.TimeUnit;
|
|||
* are not yet ready to be activated.
|
||||
*
|
||||
* When creating a new flag:
|
||||
* - Create a new string constant using {@link #generateKey(String)})
|
||||
* - Create a new string constant. This should almost certainly be prefixed with "android."
|
||||
* - Add a method to retrieve the value using {@link #getValue(String, boolean)}. You can also add
|
||||
* other checks here, like requiring other flags.
|
||||
* - If you want to be able to change a flag remotely, place it in {@link #REMOTE_CAPABLE}.
|
||||
|
@ -38,22 +38,22 @@ import java.util.concurrent.TimeUnit;
|
|||
* Other interesting things you can do:
|
||||
* - Make a flag {@link #HOT_SWAPPABLE}
|
||||
* - Make a flag {@link #STICKY}
|
||||
* - Register a listener for flag changes in {@link #FLAG_CHANGE_LISTENERS}
|
||||
*/
|
||||
public final class FeatureFlags {
|
||||
|
||||
private static final String TAG = Log.tag(FeatureFlags.class);
|
||||
|
||||
private static final String PREFIX = "android.";
|
||||
private static final long FETCH_INTERVAL = TimeUnit.HOURS.toMillis(2);
|
||||
|
||||
private static final String UUIDS = generateKey("uuids");
|
||||
private static final String MESSAGE_REQUESTS = generateKey("messageRequests");
|
||||
private static final String USERNAMES = generateKey("usernames");
|
||||
private static final String STORAGE_SERVICE = generateKey("storageService");
|
||||
private static final String PINS_FOR_ALL = generateKey("pinsForAll");
|
||||
private static final String PINS_MEGAPHONE_KILL_SWITCH = generateKey("pinsMegaphoneKillSwitch");
|
||||
private static final String PROFILE_NAMES_MEGAPHONE = generateKey("profileNamesMegaphone");
|
||||
private static final String VIDEO_TRIMMING = generateKey("videoTrimming");
|
||||
private static final String UUIDS = "android.uuids";
|
||||
private static final String MESSAGE_REQUESTS = "android.messageRequests";
|
||||
private static final String USERNAMES = "android.usernames";
|
||||
private static final String STORAGE_SERVICE = "android.storageService";
|
||||
private static final String PINS_FOR_ALL = "android.pinsForAll";
|
||||
private static final String PINS_MEGAPHONE_KILL_SWITCH = "android.pinsMegaphoneKillSwitch";
|
||||
private static final String PROFILE_NAMES_MEGAPHONE = "android.profileNamesMegaphone";
|
||||
private static final String VIDEO_TRIMMING = "android.videoTrimming";
|
||||
|
||||
/**
|
||||
* We will only store remote values for flags in this set. If you want a flag to be controllable
|
||||
|
@ -97,17 +97,39 @@ public final class FeatureFlags {
|
|||
PINS_FOR_ALL
|
||||
);
|
||||
|
||||
/**
|
||||
* Listeners that are called when the value in {@link #REMOTE_VALUES} changes. That means that
|
||||
* hot-swappable flags will have this invoked as soon as we know about that change, but otherwise
|
||||
* these will only run during initialization.
|
||||
*
|
||||
* These can be called on any thread, including the main thread, so be careful!
|
||||
*
|
||||
* Also note that this doesn't play well with {@link #FORCED_VALUES} -- changes there will not
|
||||
* trigger changes in this map, so you'll have to do some manually hacking to get yourself in the
|
||||
* desired test state.
|
||||
*/
|
||||
private static final Map<String, OnFlagChange> FLAG_CHANGE_LISTENERS = new HashMap<String, OnFlagChange>() {{
|
||||
put(MESSAGE_REQUESTS, (change) -> SignalStore.setMessageRequestEnableTime(change == Change.ENABLED ? System.currentTimeMillis() : 0));
|
||||
}};
|
||||
|
||||
private static final Map<String, Boolean> REMOTE_VALUES = new TreeMap<>();
|
||||
|
||||
private FeatureFlags() {}
|
||||
|
||||
public static synchronized void init() {
|
||||
REMOTE_VALUES.putAll(parseStoredConfig());
|
||||
Map<String, Boolean> current = parseStoredConfig(SignalStore.remoteConfigValues().getCurrentConfig());
|
||||
Map<String, Boolean> pending = parseStoredConfig(SignalStore.remoteConfigValues().getPendingConfig());
|
||||
Map<String, Change> changes = computeChanges(current, pending);
|
||||
|
||||
SignalStore.remoteConfigValues().setCurrentConfig(mapToJson(pending));
|
||||
REMOTE_VALUES.putAll(pending);
|
||||
triggerFlagChangeListeners(changes);
|
||||
|
||||
Log.i(TAG, "init() " + REMOTE_VALUES.toString());
|
||||
}
|
||||
|
||||
public static synchronized void refresh() {
|
||||
long timeSinceLastFetch = System.currentTimeMillis() - SignalStore.getRemoteConfigLastFetchTime();
|
||||
public static synchronized void refreshIfNecessary() {
|
||||
long timeSinceLastFetch = System.currentTimeMillis() - SignalStore.remoteConfigValues().getLastFetchTime();
|
||||
|
||||
if (timeSinceLastFetch > FETCH_INTERVAL) {
|
||||
Log.i(TAG, "Scheduling remote config refresh.");
|
||||
|
@ -119,12 +141,15 @@ public final class FeatureFlags {
|
|||
|
||||
public static synchronized void update(@NonNull Map<String, Boolean> config) {
|
||||
Map<String, Boolean> memory = REMOTE_VALUES;
|
||||
Map<String, Boolean> disk = parseStoredConfig();
|
||||
Map<String, Boolean> disk = parseStoredConfig(SignalStore.remoteConfigValues().getPendingConfig());
|
||||
UpdateResult result = updateInternal(config, memory, disk, REMOTE_CAPABLE, HOT_SWAPPABLE, STICKY);
|
||||
|
||||
SignalStore.setRemoteConfig(mapToJson(result.getDisk()).toString());
|
||||
SignalStore.remoteConfigValues().setPendingConfig(mapToJson(result.getDisk()));
|
||||
REMOTE_VALUES.clear();
|
||||
REMOTE_VALUES.putAll(result.getMemory());
|
||||
triggerFlagChangeListeners(result.getChanges());
|
||||
|
||||
SignalStore.remoteConfigValues().setLastFetchTime(System.currentTimeMillis());
|
||||
|
||||
Log.i(TAG, "[Memory] Before: " + memory.toString());
|
||||
Log.i(TAG, "[Memory] After : " + result.getMemory().toString());
|
||||
|
@ -189,7 +214,7 @@ public final class FeatureFlags {
|
|||
|
||||
/** Only for rendering debug info. */
|
||||
public static synchronized @NonNull Map<String, Boolean> getDiskValues() {
|
||||
return new TreeMap<>(parseStoredConfig());
|
||||
return new TreeMap<>(parseStoredConfig(SignalStore.remoteConfigValues().getCurrentConfig()));
|
||||
}
|
||||
|
||||
/** Only for rendering debug info. */
|
||||
|
@ -239,11 +264,31 @@ public final class FeatureFlags {
|
|||
}
|
||||
});
|
||||
|
||||
return new UpdateResult(newMemory, newDisk);
|
||||
return new UpdateResult(newMemory, newDisk, computeChanges(localMemory, newMemory));
|
||||
}
|
||||
|
||||
private static @NonNull String generateKey(@NonNull String key) {
|
||||
return PREFIX + key;
|
||||
@VisibleForTesting
|
||||
static @NonNull Map<String, Change> computeChanges(@NonNull Map<String, Boolean> oldMap, @NonNull Map<String, Boolean> newMap) {
|
||||
Map<String, Change> changes = new HashMap<>();
|
||||
Set<String> allKeys = new HashSet<>();
|
||||
|
||||
allKeys.addAll(oldMap.keySet());
|
||||
allKeys.addAll(newMap.keySet());
|
||||
|
||||
for (String key : allKeys) {
|
||||
Boolean oldValue = oldMap.get(key);
|
||||
Boolean newValue = newMap.get(key);
|
||||
|
||||
if (oldValue == null && newValue == null) {
|
||||
throw new AssertionError("Should not be possible.");
|
||||
} else if (oldValue != null && newValue == null) {
|
||||
changes.put(key, Change.REMOVED);
|
||||
} else if (newValue != oldValue) {
|
||||
changes.put(key, newValue ? Change.ENABLED : Change.DISABLED);
|
||||
}
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
private static boolean getValue(@NonNull String key, boolean defaultValue) {
|
||||
|
@ -260,9 +305,8 @@ public final class FeatureFlags {
|
|||
return defaultValue;
|
||||
}
|
||||
|
||||
private static Map<String, Boolean> parseStoredConfig() {
|
||||
private static Map<String, Boolean> parseStoredConfig(String stored) {
|
||||
Map<String, Boolean> parsed = new HashMap<>();
|
||||
String stored = SignalStore.getRemoteConfig();
|
||||
|
||||
if (TextUtils.isEmpty(stored)) {
|
||||
Log.i(TAG, "No remote config stored. Skipping.");
|
||||
|
@ -278,14 +322,13 @@ public final class FeatureFlags {
|
|||
parsed.put(key, root.getBoolean(key));
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
SignalStore.setRemoteConfig(null);
|
||||
throw new AssertionError("Failed to parse! Cleared storage.");
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
private static JSONObject mapToJson(@NonNull Map<String, Boolean> map) {
|
||||
private static @NonNull String mapToJson(@NonNull Map<String, Boolean> map) {
|
||||
try {
|
||||
JSONObject json = new JSONObject();
|
||||
|
||||
|
@ -293,12 +336,23 @@ public final class FeatureFlags {
|
|||
json.put(entry.getKey(), (boolean) entry.getValue());
|
||||
}
|
||||
|
||||
return json;
|
||||
return json.toString();
|
||||
} catch (JSONException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void triggerFlagChangeListeners(Map<String, Change> changes) {
|
||||
for (Map.Entry<String, Change> change : changes.entrySet()) {
|
||||
OnFlagChange listener = FLAG_CHANGE_LISTENERS.get(change.getKey());
|
||||
|
||||
if (listener != null) {
|
||||
Log.i(TAG, "Triggering change listener for: " + change.getKey());
|
||||
listener.onFlagChange(change.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class MissingFlagRequirementError extends Error {
|
||||
}
|
||||
|
||||
|
@ -306,10 +360,12 @@ public final class FeatureFlags {
|
|||
static final class UpdateResult {
|
||||
private final Map<String, Boolean> memory;
|
||||
private final Map<String, Boolean> disk;
|
||||
private final Map<String, Change> changes;
|
||||
|
||||
UpdateResult(@NonNull Map<String, Boolean> memory, @NonNull Map<String, Boolean> disk) {
|
||||
UpdateResult(@NonNull Map<String, Boolean> memory, @NonNull Map<String, Boolean> disk, @NonNull Map<String, Change> changes) {
|
||||
this.memory = memory;
|
||||
this.disk = disk;
|
||||
this.changes = changes;
|
||||
}
|
||||
|
||||
public @NonNull Map<String, Boolean> getMemory() {
|
||||
|
@ -319,6 +375,19 @@ public final class FeatureFlags {
|
|||
public @NonNull Map<String, Boolean> getDisk() {
|
||||
return disk;
|
||||
}
|
||||
|
||||
public @NonNull Map<String, Change> getChanges() {
|
||||
return changes;
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
interface OnFlagChange {
|
||||
void onFlagChange(@NonNull Change change);
|
||||
}
|
||||
|
||||
enum Change {
|
||||
ENABLED, DISABLED, REMOVED
|
||||
}
|
||||
|
||||
/** Read and write versioned profile information. */
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
@ -45,6 +44,14 @@ public class GroupUtil {
|
|||
return Hex.fromStringCondensed(groupId.split("!", 2)[1]);
|
||||
}
|
||||
|
||||
public static byte[] getDecodedIdOrThrow(String groupId) {
|
||||
try {
|
||||
return getDecodedId(groupId);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isEncodedGroup(@NonNull String groupId) {
|
||||
return groupId.startsWith(ENCODED_SIGNAL_GROUP_PREFIX) || groupId.startsWith(ENCODED_MMS_GROUP_PREFIX);
|
||||
}
|
||||
|
|
|
@ -13,17 +13,19 @@
|
|||
android:layout_marginBottom="11dp"
|
||||
android:textAppearance="@style/Signal.Text.MessageRequest.Description"
|
||||
android:paddingTop="16dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/message_request_block"
|
||||
app:layout_constraintBottom_toTopOf="@id/message_request_button_barrier"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
tools:text="Do you want to let Cayce Pollard message you? They won't know you've seen their message until you accept." />
|
||||
tools:text="Do you want to let J. Jonah Jameson message you? They won't know you've seen their message until you accept." />
|
||||
|
||||
<Button
|
||||
android:id="@+id/message_request_block"
|
||||
style="@style/Signal.MessageRequest.Button.Deny"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
style="@style/Signal.MessageRequest.Button.Deny"
|
||||
android:text="@string/MessageRequestBottomView_block"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/message_request_delete"
|
||||
|
@ -32,8 +34,10 @@
|
|||
|
||||
<Button
|
||||
android:id="@+id/message_request_delete"
|
||||
style="@style/Signal.MessageRequest.Button.Deny"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
style="@style/Signal.MessageRequest.Button.Deny"
|
||||
android:text="@string/MessageRequestBottomView_delete"
|
||||
app:layout_constraintBottom_toBottomOf="@id/message_request_block"
|
||||
app:layout_constraintEnd_toStartOf="@+id/message_request_accept"
|
||||
|
@ -43,12 +47,61 @@
|
|||
|
||||
<Button
|
||||
android:id="@+id/message_request_accept"
|
||||
style="@style/Signal.MessageRequest.Button.Accept"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
style="@style/Signal.MessageRequest.Button.Accept"
|
||||
android:text="@string/MessageRequestBottomView_accept"
|
||||
app:layout_constraintBottom_toBottomOf="@id/message_request_block"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toEndOf="@+id/message_request_delete"
|
||||
app:layout_constraintTop_toTopOf="@id/message_request_block" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/message_request_big_delete"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
style="@style/Signal.MessageRequest.Button.Deny"
|
||||
android:text="@string/MessageRequestBottomView_delete"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/message_request_big_unblock"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/message_request_big_unblock"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
style="@style/Signal.MessageRequest.Button.Accept"
|
||||
android:text="@string/MessageRequestBottomView_unblock"
|
||||
app:layout_constraintTop_toTopOf="@id/message_request_big_delete"
|
||||
app:layout_constraintStart_toEndOf="@id/message_request_big_delete"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="@id/message_request_big_delete"/>
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/message_request_button_barrier"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:barrierDirection="top"
|
||||
app:constraint_referenced_ids="message_request_block,message_request_big_delete"/>
|
||||
|
||||
<androidx.constraintlayout.widget.Group
|
||||
android:id="@+id/message_request_normal_buttons"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:visibility="gone"
|
||||
app:constraint_referenced_ids="message_request_accept,message_request_delete,message_request_block" />
|
||||
|
||||
<androidx.constraintlayout.widget.Group
|
||||
android:id="@+id/message_request_blocked_buttons"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
app:constraint_referenced_ids="message_request_big_delete,message_request_big_unblock" />
|
||||
</merge>
|
|
@ -239,6 +239,23 @@
|
|||
<string name="ConversationActivity_sticker_pack_installed">Sticker pack installed</string>
|
||||
<string name="ConversationActivity_new_say_it_with_stickers">New! Say it with stickers</string>
|
||||
|
||||
<string name="ConversationActivity_block_s">Block %1$s?</string>
|
||||
<string name="ConversationActivity_block_and_leave_s">Block and leave %1$s?</string>
|
||||
<string name="ConversationActivity_unblock_s">Unblock %1$s?</string>
|
||||
<string name="ConversationActivity_you_will_be_able_to_message_and_call_each_other">You will be able to message and call each other.</string>
|
||||
<string name="ConversationActivity_group_members_will_be_able_to_add_you_to_this_group_again">Group members will be able to add you to this group again.</string>
|
||||
<string name="ConversationActivity_blocked_people_will_not_be_able_to_call_you_or_send_you_messages">Blocked people will not be able to call you or send you messages.</string>
|
||||
<string name="ConversationActivity_you_will_leave_this_group_and_no_longer_receive_messages_or_updates">You will leave this group and no longer receive messages or updates.</string>
|
||||
<string name="ConversationActivity_block">Block</string>
|
||||
<string name="ConversationActivity_block_and_delete">Block and delete</string>
|
||||
<string name="ConversationActivity_cancel">Cancel</string>
|
||||
<string name="ConversationActivity_delete_conversation">Delete conversation?</string>
|
||||
<string name="ConversationActivity_delete_and_leave_group">Delete and leave group?</string>
|
||||
<string name="ConversationActivity_this_conversation_will_be_deleted_from_all_of_your_devices">This conversation will be deleted from all of your devices.</string>
|
||||
<string name="ConversationActivity_you_will_leave_this_group_and_it_will_be_deleted_from_all_of_your_devices">You will leave this group, and it will be deleted from all your devices.</string>
|
||||
<string name="ConversationActivity_delete">Delete</string>
|
||||
<string name="ConversationActivity_delete_and_leave">Delete and leave</string>
|
||||
|
||||
<!-- ConversationAdapter -->
|
||||
<plurals name="ConversationAdapter_n_unread_messages">
|
||||
<item quantity="one">%d unread message</item>
|
||||
|
@ -615,6 +632,27 @@
|
|||
<string name="MessageRecord_you_marked_your_safety_number_with_s_unverified">You marked your safety number with %s unverified</string>
|
||||
<string name="MessageRecord_you_marked_your_safety_number_with_s_unverified_from_another_device">You marked your safety number with %s unverified from another device</string>
|
||||
|
||||
<!-- MessageRequestBottomView -->
|
||||
<string name="MessageRequestBottomView_accept">Accept</string>
|
||||
<string name="MessageRequestBottomView_delete">Delete</string>
|
||||
<string name="MessageRequestBottomView_block">Block</string>
|
||||
<string name="MessageRequestBottomView_unblock">Unblock</string>
|
||||
<string name="MessageRequestBottomView_do_you_want_to_let_s_message_you_they_wont_know_youve_seen_their_messages_until_you_accept">Do you want to let %1$s message you? They won\'t know you\'ve seen their messages until you accept.</string>
|
||||
<string name="MessageRequestBottomView_do_you_want_to_join_the_group_s_they_wont_know_youve_seen_their_messages_until_you_accept">Do you want to join the group %1$s? They won\'t know you\'ve seen their messages until you accept.</string>
|
||||
<string name="MessageRequestBottomView_unblock_s_to_message_and_call_each_other">Unblock %1$s to message and call each other.</string>
|
||||
<string name="MessageRequestBottomView_unblock_to_allow_group_members_to_add_you_to_this_group_again">Unblock to allow group members to add you to this group again.</string>
|
||||
<string name="MessageRequestProfileView_member_of_one_group">Member of %1$s</string>
|
||||
<string name="MessageRequestProfileView_member_of_two_groups">Member of %1$s and %2$s</string>
|
||||
<string name="MessageRequestProfileView_member_of_many_groups">Member of %1$s, %2$s, and %3$s</string>
|
||||
<plurals name="MessageRequestProfileView_members">
|
||||
<item quantity="one">%1$d member</item>
|
||||
<item quantity="other">%1$d members</item>
|
||||
</plurals>
|
||||
<plurals name="MessageRequestProfileView_member_of_others">
|
||||
<item quantity="one">%d other</item>
|
||||
<item quantity="other">%d others</item>
|
||||
</plurals>
|
||||
|
||||
<!-- PassphraseChangeActivity -->
|
||||
<string name="PassphraseChangeActivity_passphrases_dont_match_exclamation">Passphrases don\'t match!</string>
|
||||
<string name="PassphraseChangeActivity_incorrect_old_passphrase_exclamation">Incorrect old passphrase!</string>
|
||||
|
@ -1932,21 +1970,6 @@
|
|||
<string name="RegistrationLockDialog_reminder">Reminder:</string>
|
||||
<string name="recipient_preferences__about">About</string>
|
||||
<string name="Recipient_unknown">Unknown</string>
|
||||
<string name="MessageRequestBottomView_accept">Accept</string>
|
||||
<string name="MessageRequestBottomView_delete">Delete</string>
|
||||
<string name="MessageRequestBottomView_block">Block</string>
|
||||
<string name="MessageRequestBottomView_do_you_want_to_let">Do you want to receive messages from %1$s?</string>
|
||||
<string name="MessageRequestProfileView_member_of_one_group">Member of %1$s</string>
|
||||
<string name="MessageRequestProfileView_member_of_two_groups">Member of %1$s and %2$s</string>
|
||||
<string name="MessageRequestProfileView_member_of_many_groups">Member of %1$s, %2$s, and %3$s</string>
|
||||
<plurals name="MessageRequestProfileView_members">
|
||||
<item quantity="one">%1$d member</item>
|
||||
<item quantity="other">%1$d members</item>
|
||||
</plurals>
|
||||
<plurals name="MessageRequestProfileView_member_of_others">
|
||||
<item quantity="one">%d other</item>
|
||||
<item quantity="other">%d others</item>
|
||||
</plurals>
|
||||
<!-- EOF -->
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.recipients;
|
|||
import android.content.Context;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.powermock.core.classloader.annotations.PrepareForTest;
|
||||
|
@ -11,6 +12,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
|
|||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
|
@ -53,7 +55,7 @@ public class RecipientUtilTest {
|
|||
when(FeatureFlags.messageRequests()).thenReturn(false);
|
||||
|
||||
// WHEN
|
||||
boolean result = RecipientUtil.isThreadMessageRequestAccepted(context, 1);
|
||||
boolean result = RecipientUtil.isMessageRequestAccepted(context, 1);
|
||||
|
||||
// THEN
|
||||
assertTrue(result);
|
||||
|
@ -62,7 +64,7 @@ public class RecipientUtilTest {
|
|||
@Test
|
||||
public void givenThreadIsNegativeOne_whenIsThreadMessageRequestAccepted_thenIExpectTrue() {
|
||||
// WHEN
|
||||
boolean result = RecipientUtil.isThreadMessageRequestAccepted(context, -1L);
|
||||
boolean result = RecipientUtil.isMessageRequestAccepted(context, -1L);
|
||||
|
||||
// THEN
|
||||
assertTrue(result);
|
||||
|
@ -71,7 +73,7 @@ public class RecipientUtilTest {
|
|||
@Test
|
||||
public void givenRecipientIsNullForThreadId_whenIsThreadMessageRequestAccepted_thenIExpectTrue() {
|
||||
// WHEN
|
||||
boolean result = RecipientUtil.isThreadMessageRequestAccepted(context, 1L);
|
||||
boolean result = RecipientUtil.isMessageRequestAccepted(context, 1L);
|
||||
|
||||
// THEN
|
||||
assertTrue(result);
|
||||
|
@ -84,7 +86,7 @@ public class RecipientUtilTest {
|
|||
when(mockMmsSmsDatabase.getOutgoingSecureConversationCount(1L)).thenReturn(5);
|
||||
|
||||
// WHEN
|
||||
boolean result = RecipientUtil.isThreadMessageRequestAccepted(context, 1L);
|
||||
boolean result = RecipientUtil.isMessageRequestAccepted(context, 1L);
|
||||
|
||||
// THEN
|
||||
assertTrue(result);
|
||||
|
@ -98,7 +100,7 @@ public class RecipientUtilTest {
|
|||
when(mockMmsSmsDatabase.getOutgoingSecureConversationCount(1L)).thenReturn(0);
|
||||
|
||||
// WHEN
|
||||
boolean result = RecipientUtil.isThreadMessageRequestAccepted(context, 1L);
|
||||
boolean result = RecipientUtil.isMessageRequestAccepted(context, 1L);
|
||||
|
||||
// THEN
|
||||
assertTrue(result);
|
||||
|
@ -112,12 +114,13 @@ public class RecipientUtilTest {
|
|||
when(mockMmsSmsDatabase.getOutgoingSecureConversationCount(1L)).thenReturn(0);
|
||||
|
||||
// WHEN
|
||||
boolean result = RecipientUtil.isThreadMessageRequestAccepted(context, 1L);
|
||||
boolean result = RecipientUtil.isMessageRequestAccepted(context, 1L);
|
||||
|
||||
// THEN
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Ignore
|
||||
@Test
|
||||
public void givenIHaveReceivedASecureMessageIHaveNotSentASecureMessageAndRecipientIsNotSystemContactAndNotProfileSharing_whenIsThreadMessageRequestAccepted_thenIExpectFalse() {
|
||||
// GIVEN
|
||||
|
@ -126,7 +129,7 @@ public class RecipientUtilTest {
|
|||
when(mockMmsSmsDatabase.getSecureConversationCount(1L)).thenReturn(5);
|
||||
|
||||
// WHEN
|
||||
boolean result = RecipientUtil.isThreadMessageRequestAccepted(context, 1L);
|
||||
boolean result = RecipientUtil.isMessageRequestAccepted(context, 1L);
|
||||
|
||||
// THEN
|
||||
assertFalse(result);
|
||||
|
@ -140,7 +143,7 @@ public class RecipientUtilTest {
|
|||
when(mockMmsSmsDatabase.getSecureConversationCount(1L)).thenReturn(0);
|
||||
|
||||
// WHEN
|
||||
boolean result = RecipientUtil.isThreadMessageRequestAccepted(context, 1L);
|
||||
boolean result = RecipientUtil.isMessageRequestAccepted(context, 1L);
|
||||
|
||||
// THEN
|
||||
assertTrue(result);
|
||||
|
@ -149,7 +152,7 @@ public class RecipientUtilTest {
|
|||
@Test
|
||||
public void givenRecipientIsNull_whenIsRecipientMessageRequestAccepted_thenIExpectTrue() {
|
||||
// WHEN
|
||||
boolean result = RecipientUtil.isRecipientMessageRequestAccepted(context, null);
|
||||
boolean result = RecipientUtil.isMessageRequestAccepted(context, null);
|
||||
|
||||
// THEN
|
||||
assertTrue(result);
|
||||
|
@ -161,7 +164,7 @@ public class RecipientUtilTest {
|
|||
when(FeatureFlags.messageRequests()).thenReturn(false);
|
||||
|
||||
// WHEN
|
||||
boolean result = RecipientUtil.isRecipientMessageRequestAccepted(context, recipient);
|
||||
boolean result = RecipientUtil.isMessageRequestAccepted(context, recipient);
|
||||
|
||||
// THEN
|
||||
assertTrue(result);
|
||||
|
@ -173,7 +176,7 @@ public class RecipientUtilTest {
|
|||
when(mockMmsSmsDatabase.getOutgoingSecureConversationCount(anyLong())).thenReturn(1);
|
||||
|
||||
// WHEN
|
||||
boolean result = RecipientUtil.isRecipientMessageRequestAccepted(context, recipient);
|
||||
boolean result = RecipientUtil.isMessageRequestAccepted(context, recipient);
|
||||
|
||||
// THEN
|
||||
assertTrue(result);
|
||||
|
@ -185,7 +188,7 @@ public class RecipientUtilTest {
|
|||
when(recipient.isProfileSharing()).thenReturn(true);
|
||||
|
||||
// WHEN
|
||||
boolean result = RecipientUtil.isRecipientMessageRequestAccepted(context, recipient);
|
||||
boolean result = RecipientUtil.isMessageRequestAccepted(context, recipient);
|
||||
|
||||
// THEN
|
||||
assertTrue(result);
|
||||
|
@ -197,19 +200,21 @@ public class RecipientUtilTest {
|
|||
when(recipient.isSystemContact()).thenReturn(true);
|
||||
|
||||
// WHEN
|
||||
boolean result = RecipientUtil.isRecipientMessageRequestAccepted(context, recipient);
|
||||
boolean result = RecipientUtil.isMessageRequestAccepted(context, recipient);
|
||||
|
||||
// THEN
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Ignore
|
||||
@Test
|
||||
public void givenNoSecureMessagesSentSomeSecureMessagesReceivedNotSharingAndNotSystemContact_whenIsRecipientMessageRequestAccepted_thenIExpectFalse() {
|
||||
// GIVEN
|
||||
when(recipient.isRegistered()).thenReturn(true);
|
||||
when(mockMmsSmsDatabase.getSecureConversationCount(anyLong())).thenReturn(5);
|
||||
|
||||
// WHEN
|
||||
boolean result = RecipientUtil.isRecipientMessageRequestAccepted(context, recipient);
|
||||
boolean result = RecipientUtil.isMessageRequestAccepted(context, recipient);
|
||||
|
||||
// THEN
|
||||
assertFalse(result);
|
||||
|
@ -221,12 +226,13 @@ public class RecipientUtilTest {
|
|||
when(mockMmsSmsDatabase.getSecureConversationCount(anyLong())).thenReturn(0);
|
||||
|
||||
// WHEN
|
||||
boolean result = RecipientUtil.isRecipientMessageRequestAccepted(context, recipient);
|
||||
boolean result = RecipientUtil.isMessageRequestAccepted(context, recipient);
|
||||
|
||||
// THEN
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Ignore
|
||||
@Test
|
||||
public void givenNoSecureMessagesSent_whenIShareProfileIfFirstSecureMessage_thenIShareProfile() {
|
||||
// GIVEN
|
||||
|
@ -239,6 +245,7 @@ public class RecipientUtilTest {
|
|||
verify(mockRecipientDatabase).setProfileSharing(recipient.getId(), true);
|
||||
}
|
||||
|
||||
@Ignore
|
||||
@Test
|
||||
public void givenSecureMessagesSent_whenIShareProfileIfFirstSecureMessage_thenIShareProfile() {
|
||||
// GIVEN
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags.Change;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags.UpdateResult;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
@ -9,12 +10,14 @@ import java.util.HashSet;
|
|||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static junit.framework.TestCase.assertTrue;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
|
||||
public class FeatureFlagsTest {
|
||||
|
||||
private static final String A = key("a");
|
||||
private static final String B = key("b");
|
||||
private static final String A = "A";
|
||||
private static final String B = "B";
|
||||
|
||||
@Test
|
||||
public void updateInternal_newValue_ignoreNotInRemoteCapable() {
|
||||
|
@ -28,6 +31,7 @@ public class FeatureFlagsTest {
|
|||
|
||||
assertEquals(mapOf(), result.getMemory());
|
||||
assertEquals(mapOf("A", true), result.getDisk());
|
||||
assertTrue(result.getChanges().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -41,6 +45,7 @@ public class FeatureFlagsTest {
|
|||
|
||||
assertEquals(mapOf(), result.getMemory());
|
||||
assertEquals(mapOf(A, true), result.getDisk());
|
||||
assertTrue(result.getChanges().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -54,6 +59,7 @@ public class FeatureFlagsTest {
|
|||
|
||||
assertEquals(mapOf(A, true), result.getMemory());
|
||||
assertEquals(mapOf(A, true), result.getDisk());
|
||||
assertEquals(Change.ENABLED, result.getChanges().get(A));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -67,6 +73,7 @@ public class FeatureFlagsTest {
|
|||
|
||||
assertEquals(mapOf(), result.getMemory());
|
||||
assertEquals(mapOf(A, true), result.getDisk());
|
||||
assertTrue(result.getChanges().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -80,6 +87,7 @@ public class FeatureFlagsTest {
|
|||
|
||||
assertEquals(mapOf(A, true), result.getMemory());
|
||||
assertEquals(mapOf(A, true), result.getDisk());
|
||||
assertEquals(Change.ENABLED, result.getChanges().get(A));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -93,6 +101,7 @@ public class FeatureFlagsTest {
|
|||
|
||||
assertEquals(mapOf(A, false), result.getMemory());
|
||||
assertEquals(mapOf(A, true), result.getDisk());
|
||||
assertTrue(result.getChanges().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -106,6 +115,7 @@ public class FeatureFlagsTest {
|
|||
|
||||
assertEquals(mapOf(A, true), result.getMemory());
|
||||
assertEquals(mapOf(A, true), result.getDisk());
|
||||
assertEquals(Change.ENABLED, result.getChanges().get(A));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -119,6 +129,7 @@ public class FeatureFlagsTest {
|
|||
|
||||
assertEquals(mapOf(A, true), result.getMemory());
|
||||
assertEquals(mapOf(A, true), result.getDisk());
|
||||
assertEquals(Change.ENABLED, result.getChanges().get(A));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -132,6 +143,7 @@ public class FeatureFlagsTest {
|
|||
|
||||
assertEquals(mapOf(A, true), result.getMemory());
|
||||
assertEquals(mapOf(A, true), result.getDisk());
|
||||
assertTrue(result.getChanges().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -145,6 +157,7 @@ public class FeatureFlagsTest {
|
|||
|
||||
assertEquals(mapOf(A, true), result.getMemory());
|
||||
assertEquals(mapOf(), result.getDisk());
|
||||
assertTrue(result.getChanges().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -158,6 +171,7 @@ public class FeatureFlagsTest {
|
|||
|
||||
assertEquals(mapOf(), result.getMemory());
|
||||
assertEquals(mapOf(), result.getDisk());
|
||||
assertEquals(Change.REMOVED, result.getChanges().get(A));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -171,6 +185,7 @@ public class FeatureFlagsTest {
|
|||
|
||||
assertEquals(mapOf(A, true), result.getMemory());
|
||||
assertEquals(mapOf(A, true), result.getDisk());
|
||||
assertTrue(result.getChanges().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -184,6 +199,7 @@ public class FeatureFlagsTest {
|
|||
|
||||
assertEquals(mapOf(A, false), result.getMemory());
|
||||
assertEquals(mapOf(), result.getDisk());
|
||||
assertTrue(result.getChanges().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -197,6 +213,7 @@ public class FeatureFlagsTest {
|
|||
|
||||
assertEquals(mapOf(A, true), result.getMemory());
|
||||
assertEquals(mapOf(A, true), result.getDisk());
|
||||
assertTrue(result.getChanges().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -210,6 +227,7 @@ public class FeatureFlagsTest {
|
|||
|
||||
assertEquals(mapOf(), result.getMemory());
|
||||
assertEquals(mapOf(), result.getDisk());
|
||||
assertEquals(Change.REMOVED, result.getChanges().get(A));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -224,6 +242,7 @@ public class FeatureFlagsTest {
|
|||
|
||||
assertEquals(mapOf(), result.getMemory());
|
||||
assertEquals(mapOf(A, true, B, false), result.getDisk());
|
||||
assertTrue(result.getChanges().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -240,10 +259,34 @@ public class FeatureFlagsTest {
|
|||
|
||||
assertEquals(mapOf(A, true, B, true), result.getMemory());
|
||||
assertEquals(mapOf(A, true, B, false), result.getDisk());
|
||||
assertTrue(result.getChanges().isEmpty());
|
||||
}
|
||||
|
||||
private static String key(String s) {
|
||||
return "android." + s;
|
||||
@Test
|
||||
public void computeChanges_generic() {
|
||||
Map<String, Boolean> oldMap = new HashMap<String, Boolean>() {{
|
||||
put("a", true);
|
||||
put("b", false);
|
||||
put("c", true);
|
||||
put("d", false);
|
||||
}};
|
||||
|
||||
Map<String, Boolean> newMap = new HashMap<String, Boolean>() {{
|
||||
put("a", true);
|
||||
put("b", true);
|
||||
put("c", false);
|
||||
put("e", true);
|
||||
put("f", false);
|
||||
}};
|
||||
|
||||
Map<String, Change> changes = FeatureFlags.computeChanges(oldMap, newMap);
|
||||
|
||||
assertFalse(changes.containsKey("a"));
|
||||
assertEquals(Change.ENABLED, changes.get("b"));
|
||||
assertEquals(Change.DISABLED, changes.get("c"));
|
||||
assertEquals(Change.REMOVED, changes.get("d"));
|
||||
assertEquals(Change.ENABLED, changes.get("e"));
|
||||
assertEquals(Change.DISABLED, changes.get("f"));
|
||||
}
|
||||
|
||||
private static <V> Set<V> setOf(V... values) {
|
||||
|
|
|
@ -35,6 +35,7 @@ import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
|
|||
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.ConfigurationMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.MessageRequestResponseMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
|
||||
|
@ -302,6 +303,8 @@ public class SignalServiceMessageSender {
|
|||
content = createMultiDeviceStickerPackOperationContent(message.getStickerPackOperations().get());
|
||||
} else if (message.getFetchType().isPresent()) {
|
||||
content = createMultiDeviceFetchTypeContent(message.getFetchType().get());
|
||||
} else if (message.getMessageRequestResponse().isPresent()) {
|
||||
content = createMultiDeviceMessageRequestResponseContent(message.getMessageRequestResponse().get());
|
||||
} else if (message.getVerified().isPresent()) {
|
||||
sendMessage(message.getVerified().get(), unidentifiedAccess);
|
||||
return;
|
||||
|
@ -818,6 +821,48 @@ public class SignalServiceMessageSender {
|
|||
return container.setSyncMessage(syncMessage.setFetchLatest(fetchMessage)).build().toByteArray();
|
||||
}
|
||||
|
||||
private byte[] createMultiDeviceMessageRequestResponseContent(MessageRequestResponseMessage message) {
|
||||
Content.Builder container = Content.newBuilder();
|
||||
SyncMessage.Builder syncMessage = createSyncMessageBuilder();
|
||||
SyncMessage.MessageRequestResponse.Builder responseMessage = SyncMessage.MessageRequestResponse.newBuilder();
|
||||
|
||||
if (message.getGroupId().isPresent()) {
|
||||
responseMessage.setGroupId(ByteString.copyFrom(message.getGroupId().get()));
|
||||
}
|
||||
|
||||
if (message.getPerson().isPresent()) {
|
||||
if (message.getPerson().get().getNumber().isPresent()) {
|
||||
responseMessage.setThreadE164(message.getPerson().get().getNumber().get());
|
||||
}
|
||||
if (message.getPerson().get().getUuid().isPresent()) {
|
||||
responseMessage.setThreadUuid(message.getPerson().get().getUuid().get().toString());
|
||||
}
|
||||
}
|
||||
|
||||
switch (message.getType()) {
|
||||
case ACCEPT:
|
||||
responseMessage.setType(SyncMessage.MessageRequestResponse.Type.ACCEPT);
|
||||
break;
|
||||
case DELETE:
|
||||
responseMessage.setType(SyncMessage.MessageRequestResponse.Type.DELETE);
|
||||
break;
|
||||
case BLOCK:
|
||||
responseMessage.setType(SyncMessage.MessageRequestResponse.Type.BLOCK);
|
||||
break;
|
||||
case BLOCK_AND_DELETE:
|
||||
responseMessage.setType(SyncMessage.MessageRequestResponse.Type.BLOCK_AND_DELETE);
|
||||
break;
|
||||
default:
|
||||
Log.w(TAG, "Unknown type!");
|
||||
responseMessage.setType(SyncMessage.MessageRequestResponse.Type.UNKNOWN);
|
||||
break;
|
||||
}
|
||||
|
||||
syncMessage.setMessageRequestResponse(responseMessage);
|
||||
|
||||
return container.setSyncMessage(syncMessage).build().toByteArray();
|
||||
}
|
||||
|
||||
private byte[] createMultiDeviceVerifiedContent(VerifiedMessage verifiedMessage, byte[] nullMessage) {
|
||||
Content.Builder container = Content.newBuilder();
|
||||
SyncMessage.Builder syncMessage = createSyncMessageBuilder();
|
||||
|
|
|
@ -24,6 +24,7 @@ import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
|
|||
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.ConfigurationMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.MessageRequestResponseMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
|
||||
|
@ -454,6 +455,44 @@ public final class SignalServiceContent {
|
|||
}
|
||||
}
|
||||
|
||||
if (content.hasMessageRequestResponse()) {
|
||||
MessageRequestResponseMessage.Type type;
|
||||
|
||||
switch (content.getMessageRequestResponse().getType()) {
|
||||
case ACCEPT:
|
||||
type = MessageRequestResponseMessage.Type.ACCEPT;
|
||||
break;
|
||||
case DELETE:
|
||||
type = MessageRequestResponseMessage.Type.DELETE;
|
||||
break;
|
||||
case BLOCK:
|
||||
type = MessageRequestResponseMessage.Type.BLOCK;
|
||||
break;
|
||||
case BLOCK_AND_DELETE:
|
||||
type = MessageRequestResponseMessage.Type.BLOCK_AND_DELETE;
|
||||
break;
|
||||
default:
|
||||
type = MessageRequestResponseMessage.Type.UNKNOWN;
|
||||
break;
|
||||
}
|
||||
|
||||
MessageRequestResponseMessage responseMessage;
|
||||
|
||||
if (content.getMessageRequestResponse().hasGroupId()) {
|
||||
responseMessage = MessageRequestResponseMessage.forGroup(content.getMessageRequestResponse().getGroupId().toByteArray(), type);
|
||||
} else {
|
||||
Optional<SignalServiceAddress> address = SignalServiceAddress.fromRaw(content.getMessageRequestResponse().getThreadUuid(), content.getMessageRequestResponse().getThreadE164());
|
||||
|
||||
if (address.isPresent()) {
|
||||
responseMessage = MessageRequestResponseMessage.forIndividual(address.get(), type);
|
||||
} else {
|
||||
throw new ProtocolInvalidMessageException(new InvalidMessageException("Message request response has an invalid thread identifier!"), null, 0);
|
||||
}
|
||||
}
|
||||
|
||||
return SignalServiceSyncMessage.forMessageRequestResponse(responseMessage);
|
||||
}
|
||||
|
||||
return SignalServiceSyncMessage.empty();
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
package org.whispersystems.signalservice.api.messages.multidevice;
|
||||
|
||||
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
||||
public class MessageRequestResponseMessage {
|
||||
|
||||
private final Optional<SignalServiceAddress> person;
|
||||
private final Optional<byte[]> groupId;
|
||||
private final Type type;
|
||||
|
||||
public static MessageRequestResponseMessage forIndividual(SignalServiceAddress address, Type type) {
|
||||
return new MessageRequestResponseMessage(Optional.of(address), Optional.<byte[]>absent(), type);
|
||||
}
|
||||
|
||||
public static MessageRequestResponseMessage forGroup(byte[] groupId, Type type) {
|
||||
return new MessageRequestResponseMessage(Optional.<SignalServiceAddress>absent(), Optional.of(groupId), type);
|
||||
}
|
||||
|
||||
private MessageRequestResponseMessage(Optional<SignalServiceAddress> person,
|
||||
Optional<byte[]> groupId,
|
||||
Type type)
|
||||
{
|
||||
this.person = person;
|
||||
this.groupId = groupId;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public Optional<SignalServiceAddress> getPerson() {
|
||||
return person;
|
||||
}
|
||||
|
||||
public Optional<byte[]> getGroupId() {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
UNKNOWN, ACCEPT, DELETE, BLOCK, BLOCK_AND_DELETE, UNBLOCK_AND_ACCEPT
|
||||
}
|
||||
}
|
|
@ -26,6 +26,7 @@ public class SignalServiceSyncMessage {
|
|||
private final Optional<List<StickerPackOperationMessage>> stickerPackOperations;
|
||||
private final Optional<FetchType> fetchType;
|
||||
private final Optional<KeysMessage> keys;
|
||||
private final Optional<MessageRequestResponseMessage> messageRequestResponse;
|
||||
|
||||
private SignalServiceSyncMessage(Optional<SentTranscriptMessage> sent,
|
||||
Optional<ContactsMessage> contacts,
|
||||
|
@ -38,7 +39,8 @@ public class SignalServiceSyncMessage {
|
|||
Optional<ConfigurationMessage> configuration,
|
||||
Optional<List<StickerPackOperationMessage>> stickerPackOperations,
|
||||
Optional<FetchType> fetchType,
|
||||
Optional<KeysMessage> keys)
|
||||
Optional<KeysMessage> keys,
|
||||
Optional<MessageRequestResponseMessage> messageRequestResponse)
|
||||
{
|
||||
this.sent = sent;
|
||||
this.contacts = contacts;
|
||||
|
@ -52,6 +54,7 @@ public class SignalServiceSyncMessage {
|
|||
this.stickerPackOperations = stickerPackOperations;
|
||||
this.fetchType = fetchType;
|
||||
this.keys = keys;
|
||||
this.messageRequestResponse = messageRequestResponse;
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage forSentTranscript(SentTranscriptMessage sent) {
|
||||
|
@ -66,7 +69,8 @@ public class SignalServiceSyncMessage {
|
|||
Optional.<ConfigurationMessage>absent(),
|
||||
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||
Optional.<FetchType>absent(),
|
||||
Optional.<KeysMessage>absent());
|
||||
Optional.<KeysMessage>absent(),
|
||||
Optional.<MessageRequestResponseMessage>absent());
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage forContacts(ContactsMessage contacts) {
|
||||
|
@ -81,7 +85,8 @@ public class SignalServiceSyncMessage {
|
|||
Optional.<ConfigurationMessage>absent(),
|
||||
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||
Optional.<FetchType>absent(),
|
||||
Optional.<KeysMessage>absent());
|
||||
Optional.<KeysMessage>absent(),
|
||||
Optional.<MessageRequestResponseMessage>absent());
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage forGroups(SignalServiceAttachment groups) {
|
||||
|
@ -96,7 +101,8 @@ public class SignalServiceSyncMessage {
|
|||
Optional.<ConfigurationMessage>absent(),
|
||||
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||
Optional.<FetchType>absent(),
|
||||
Optional.<KeysMessage>absent());
|
||||
Optional.<KeysMessage>absent(),
|
||||
Optional.<MessageRequestResponseMessage>absent());
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage forRequest(RequestMessage request) {
|
||||
|
@ -111,7 +117,8 @@ public class SignalServiceSyncMessage {
|
|||
Optional.<ConfigurationMessage>absent(),
|
||||
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||
Optional.<FetchType>absent(),
|
||||
Optional.<KeysMessage>absent());
|
||||
Optional.<KeysMessage>absent(),
|
||||
Optional.<MessageRequestResponseMessage>absent());
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage forRead(List<ReadMessage> reads) {
|
||||
|
@ -126,7 +133,8 @@ public class SignalServiceSyncMessage {
|
|||
Optional.<ConfigurationMessage>absent(),
|
||||
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||
Optional.<FetchType>absent(),
|
||||
Optional.<KeysMessage>absent());
|
||||
Optional.<KeysMessage>absent(),
|
||||
Optional.<MessageRequestResponseMessage>absent());
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage forViewOnceOpen(ViewOnceOpenMessage timerRead) {
|
||||
|
@ -141,7 +149,8 @@ public class SignalServiceSyncMessage {
|
|||
Optional.<ConfigurationMessage>absent(),
|
||||
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||
Optional.<FetchType>absent(),
|
||||
Optional.<KeysMessage>absent());
|
||||
Optional.<KeysMessage>absent(),
|
||||
Optional.<MessageRequestResponseMessage>absent());
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage forRead(ReadMessage read) {
|
||||
|
@ -159,7 +168,8 @@ public class SignalServiceSyncMessage {
|
|||
Optional.<ConfigurationMessage>absent(),
|
||||
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||
Optional.<FetchType>absent(),
|
||||
Optional.<KeysMessage>absent());
|
||||
Optional.<KeysMessage>absent(),
|
||||
Optional.<MessageRequestResponseMessage>absent());
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage forVerified(VerifiedMessage verifiedMessage) {
|
||||
|
@ -174,7 +184,8 @@ public class SignalServiceSyncMessage {
|
|||
Optional.<ConfigurationMessage>absent(),
|
||||
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||
Optional.<FetchType>absent(),
|
||||
Optional.<KeysMessage>absent());
|
||||
Optional.<KeysMessage>absent(),
|
||||
Optional.<MessageRequestResponseMessage>absent());
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage forBlocked(BlockedListMessage blocked) {
|
||||
|
@ -189,7 +200,8 @@ public class SignalServiceSyncMessage {
|
|||
Optional.<ConfigurationMessage>absent(),
|
||||
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||
Optional.<FetchType>absent(),
|
||||
Optional.<KeysMessage>absent());
|
||||
Optional.<KeysMessage>absent(),
|
||||
Optional.<MessageRequestResponseMessage>absent());
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage forConfiguration(ConfigurationMessage configuration) {
|
||||
|
@ -204,7 +216,8 @@ public class SignalServiceSyncMessage {
|
|||
Optional.of(configuration),
|
||||
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||
Optional.<FetchType>absent(),
|
||||
Optional.<KeysMessage>absent());
|
||||
Optional.<KeysMessage>absent(),
|
||||
Optional.<MessageRequestResponseMessage>absent());
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage forStickerPackOperations(List<StickerPackOperationMessage> stickerPackOperations) {
|
||||
|
@ -219,7 +232,8 @@ public class SignalServiceSyncMessage {
|
|||
Optional.<ConfigurationMessage>absent(),
|
||||
Optional.of(stickerPackOperations),
|
||||
Optional.<FetchType>absent(),
|
||||
Optional.<KeysMessage>absent());
|
||||
Optional.<KeysMessage>absent(),
|
||||
Optional.<MessageRequestResponseMessage>absent());
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage forFetchLatest(FetchType fetchType) {
|
||||
|
@ -234,7 +248,8 @@ public class SignalServiceSyncMessage {
|
|||
Optional.<ConfigurationMessage>absent(),
|
||||
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||
Optional.of(fetchType),
|
||||
Optional.<KeysMessage>absent());
|
||||
Optional.<KeysMessage>absent(),
|
||||
Optional.<MessageRequestResponseMessage>absent());
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage forKeys(KeysMessage keys) {
|
||||
|
@ -249,7 +264,24 @@ public class SignalServiceSyncMessage {
|
|||
Optional.<ConfigurationMessage>absent(),
|
||||
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||
Optional.<FetchType>absent(),
|
||||
Optional.of(keys));
|
||||
Optional.of(keys),
|
||||
Optional.<MessageRequestResponseMessage>absent());
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage forMessageRequestResponse(MessageRequestResponseMessage messageRequestResponse) {
|
||||
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
|
||||
Optional.<ContactsMessage>absent(),
|
||||
Optional.<SignalServiceAttachment>absent(),
|
||||
Optional.<BlockedListMessage>absent(),
|
||||
Optional.<RequestMessage>absent(),
|
||||
Optional.<List<ReadMessage>>absent(),
|
||||
Optional.<ViewOnceOpenMessage>absent(),
|
||||
Optional.<VerifiedMessage>absent(),
|
||||
Optional.<ConfigurationMessage>absent(),
|
||||
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||
Optional.<FetchType>absent(),
|
||||
Optional.<KeysMessage>absent(),
|
||||
Optional.of(messageRequestResponse));
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage empty() {
|
||||
|
@ -264,7 +296,8 @@ public class SignalServiceSyncMessage {
|
|||
Optional.<ConfigurationMessage>absent(),
|
||||
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||
Optional.<FetchType>absent(),
|
||||
Optional.<KeysMessage>absent());
|
||||
Optional.<KeysMessage>absent(),
|
||||
Optional.<MessageRequestResponseMessage>absent());
|
||||
}
|
||||
|
||||
public Optional<SentTranscriptMessage> getSent() {
|
||||
|
@ -315,6 +348,10 @@ public class SignalServiceSyncMessage {
|
|||
return keys;
|
||||
}
|
||||
|
||||
public Optional<MessageRequestResponseMessage> getMessageRequestResponse() {
|
||||
return messageRequestResponse;
|
||||
}
|
||||
|
||||
public enum FetchType {
|
||||
LOCAL_PROFILE,
|
||||
STORAGE_MANIFEST
|
||||
|
|
|
@ -340,6 +340,21 @@ message SyncMessage {
|
|||
optional bytes storageService = 1;
|
||||
}
|
||||
|
||||
message MessageRequestResponse {
|
||||
enum Type {
|
||||
UNKNOWN = 0;
|
||||
ACCEPT = 1;
|
||||
DELETE = 2;
|
||||
BLOCK = 3;
|
||||
BLOCK_AND_DELETE = 4;
|
||||
}
|
||||
|
||||
optional string threadE164 = 1;
|
||||
optional string threadUuid = 2;
|
||||
optional bytes groupId = 3;
|
||||
optional Type type = 4;
|
||||
}
|
||||
|
||||
optional Sent sent = 1;
|
||||
optional Contacts contacts = 2;
|
||||
optional Groups groups = 3;
|
||||
|
@ -353,6 +368,7 @@ message SyncMessage {
|
|||
optional ViewOnceOpen viewOnceOpen = 11;
|
||||
optional FetchLatest fetchLatest = 12;
|
||||
optional Keys keys = 13;
|
||||
optional MessageRequestResponse messageRequestResponse = 14;
|
||||
}
|
||||
|
||||
message AttachmentPointer {
|
||||
|
|
Loading…
Add table
Reference in a new issue