Implement additional message request improvements.

This commit is contained in:
Greyson Parrelli 2020-02-21 13:52:27 -05:00
parent 81c7887d47
commit 1faf196f82
43 changed files with 1523 additions and 361 deletions

View file

@ -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);

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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);

View file

@ -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 });

View file

@ -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;

View file

@ -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);

View file

@ -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);
}

View file

@ -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();

View file

@ -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());

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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();

View file

@ -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);

View file

@ -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

View file

@ -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());

View file

@ -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

View file

@ -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;
}

View file

@ -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();
}
}

View file

@ -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}.

View file

@ -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;

View file

@ -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
}
}

View file

@ -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()));
}
}
}

View file

@ -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);
}
}

View file

@ -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),

View file

@ -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;

View file

@ -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);
}

View file

@ -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;
}
}

View file

@ -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. */

View file

@ -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);
}

View file

@ -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>

View file

@ -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>

View file

@ -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

View file

@ -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) {

View file

@ -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();

View file

@ -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();
}

View file

@ -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
}
}

View file

@ -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

View file

@ -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 {