diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 45571e0080..87b25d3842 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -267,6 +267,10 @@
android:windowSoftInputMode="stateAlwaysHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
+
+
{
+public final class RecipientSettingsCoordinatorLayoutBehavior extends CoordinatorLayout.Behavior {
private static final Interpolator INTERPOLATOR = new DecelerateInterpolator();
- private final ViewRef avatarTargetRef = new ViewRef(R.id.avatar_target);
- private final ViewRef groupNameRef = new ViewRef(R.id.group_name);
- private final ViewRef groupNameTargetRef = new ViewRef(R.id.group_name_target);
- private final Rect targetRect = new Rect();
- private final Rect childRect = new Rect();
+ private final ViewReference avatarTargetRef = new ViewReference(R.id.avatar_target);
+ private final ViewReference nameRef = new ViewReference(R.id.name);
+ private final ViewReference nameTargetRef = new ViewReference(R.id.name_target);
+ private final Rect targetRect = new Rect();
+ private final Rect childRect = new Rect();
- public GroupSettingsCoordinatorLayoutBehavior(@NonNull Context context, @Nullable AttributeSet attrs) {
+ public RecipientSettingsCoordinatorLayoutBehavior(@NonNull Context context, @Nullable AttributeSet attrs) {
}
@Override
@@ -71,8 +71,8 @@ public final class GroupSettingsCoordinatorLayoutBehavior extends CoordinatorLay
}
private void updateNamePosition(@NonNull CoordinatorLayout parent, float factor) {
- TextView child = (TextView) groupNameRef.require(parent);
- View target = groupNameTargetRef.require(parent);
+ TextView child = (TextView) nameRef.require(parent);
+ View target = nameTargetRef.require(parent);
targetRect.set(target.getLeft(), target.getTop(), target.getRight(), target.getBottom());
childRect.set(child.getLeft(), child.getTop(), child.getRight(), child.getBottom());
@@ -95,13 +95,13 @@ public final class GroupSettingsCoordinatorLayoutBehavior extends CoordinatorLay
return parent.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR ? rect.left : rect.right;
}
- private static final class ViewRef {
+ private static final class ViewReference {
private WeakReference ref = new WeakReference<>(null);
private final @IdRes int idRes;
- private ViewRef(@IdRes int idRes) {
+ private ViewReference(@IdRes int idRes) {
this.idRes = idRes;
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientActivity.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientActivity.java
new file mode 100644
index 0000000000..7f73abf278
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientActivity.java
@@ -0,0 +1,60 @@
+package org.thoughtcrime.securesms.recipients.ui.managerecipient;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.app.ActivityOptionsCompat;
+
+import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.recipients.RecipientId;
+import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
+import org.thoughtcrime.securesms.util.DynamicTheme;
+
+public class ManageRecipientActivity extends PassphraseRequiredActionBarActivity {
+
+ private static final String RECIPIENT_ID = "RECIPIENT_ID";
+
+ private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
+
+ public static Intent newIntent(@NonNull Context context, @NonNull RecipientId recipientId) {
+ Intent intent = new Intent(context, ManageRecipientActivity.class);
+ intent.putExtra(RECIPIENT_ID, recipientId);
+ return intent;
+ }
+
+ public static @Nullable Bundle createTransitionBundle(@NonNull Context activityContext, @NonNull View from) {
+ if (activityContext instanceof Activity) {
+ return ActivityOptionsCompat.makeSceneTransitionAnimation((Activity) activityContext, from, "avatar").toBundle();
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ protected void onPreCreate() {
+ dynamicTheme.onCreate(this);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState, boolean ready) {
+ super.onCreate(savedInstanceState, ready);
+ setContentView(R.layout.recipient_manage_activity);
+ if (savedInstanceState == null) {
+ getSupportFragmentManager().beginTransaction()
+ .replace(R.id.container, ManageRecipientFragment.newInstance(getIntent().getParcelableExtra(RECIPIENT_ID)))
+ .commitNow();
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ dynamicTheme.onResume(this);
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientFragment.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientFragment.java
new file mode 100644
index 0000000000..371237389f
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientFragment.java
@@ -0,0 +1,349 @@
+package org.thoughtcrime.securesms.recipients.ui.managerecipient;
+
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.SwitchCompat;
+import androidx.appcompat.widget.Toolbar;
+import androidx.core.content.ContextCompat;
+import androidx.core.view.ViewCompat;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.ViewModelProviders;
+
+import com.takisoft.colorpicker.ColorPickerDialog;
+import com.takisoft.colorpicker.ColorStateDrawable;
+
+import org.thoughtcrime.securesms.AvatarPreviewActivity;
+import org.thoughtcrime.securesms.MediaPreviewActivity;
+import org.thoughtcrime.securesms.MuteDialog;
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.color.MaterialColor;
+import org.thoughtcrime.securesms.color.MaterialColors;
+import org.thoughtcrime.securesms.components.AvatarImageView;
+import org.thoughtcrime.securesms.components.ThreadPhotoRailView;
+import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
+import org.thoughtcrime.securesms.contacts.avatars.FallbackPhoto80dp;
+import org.thoughtcrime.securesms.groups.ui.GroupMemberListView;
+import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity;
+import org.thoughtcrime.securesms.mms.GlideApp;
+import org.thoughtcrime.securesms.notifications.NotificationChannels;
+import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
+import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.recipients.RecipientId;
+import org.thoughtcrime.securesms.recipients.ui.notifications.CustomNotificationsDialogFragment;
+import org.thoughtcrime.securesms.util.DateUtils;
+import org.thoughtcrime.securesms.util.LifecycleCursorWrapper;
+import org.thoughtcrime.securesms.util.ServiceUtil;
+import org.thoughtcrime.securesms.util.Util;
+
+import java.util.Locale;
+import java.util.Objects;
+
+public class ManageRecipientFragment extends Fragment {
+ private static final String RECIPIENT_ID = "RECIPIENT_ID";
+
+ private static final int RETURN_FROM_MEDIA = 405;
+
+ private ManageRecipientViewModel viewModel;
+ private GroupMemberListView sharedGroupList;
+ private Toolbar toolbar;
+ private TextView name;
+ private TextView usernameNumber;
+ private AvatarImageView avatar;
+ private ThreadPhotoRailView threadPhotoRailView;
+ private View mediaCard;
+ private ManageRecipientViewModel.CursorFactory cursorFactory;
+ private View sharedMediaRow;
+ private View disappearingMessagesCard;
+ private View disappearingMessagesRow;
+ private TextView disappearingMessages;
+ private View colorRow;
+ private ImageView colorChip;
+ private TextView block;
+ private TextView unblock;
+ private TextView addToAGroup;
+ private SwitchCompat muteNotificationsSwitch;
+ private View muteNotificationsRow;
+ private TextView muteNotificationsUntilLabel;
+ private TextView customNotificationsButton;
+ private View customNotificationsRow;
+ private View toggleAllGroups;
+ private View viewSafetyNumber;
+ private TextView groupsInCommonCount;
+ private View messageButton;
+ private View secureCallButton;
+ private View secureVideoCallButton;
+
+ static ManageRecipientFragment newInstance(@NonNull RecipientId recipientId) {
+ ManageRecipientFragment fragment = new ManageRecipientFragment();
+ Bundle args = new Bundle();
+
+ args.putParcelable(RECIPIENT_ID, recipientId);
+ fragment.setArguments(args);
+
+ return fragment;
+ }
+
+ @Override
+ public @Nullable View onCreateView(@NonNull LayoutInflater inflater,
+ @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState)
+ {
+ View view = inflater.inflate(R.layout.recipient_manage_fragment, container, false);
+
+ avatar = view.findViewById(R.id.recipient_avatar);
+ toolbar = view.findViewById(R.id.toolbar);
+ name = view.findViewById(R.id.name);
+ usernameNumber = view.findViewById(R.id.username_number);
+ sharedGroupList = view.findViewById(R.id.shared_group_list);
+ groupsInCommonCount = view.findViewById(R.id.groups_in_common_count);
+ threadPhotoRailView = view.findViewById(R.id.recent_photos);
+ mediaCard = view.findViewById(R.id.recipient_media_card);
+ sharedMediaRow = view.findViewById(R.id.shared_media_row);
+ disappearingMessagesCard = view.findViewById(R.id.recipient_disappearing_messages_card);
+ disappearingMessagesRow = view.findViewById(R.id.disappearing_messages_row);
+ disappearingMessages = view.findViewById(R.id.disappearing_messages);
+ colorRow = view.findViewById(R.id.color_row);
+ colorChip = view.findViewById(R.id.color_chip);
+ block = view.findViewById(R.id.block);
+ unblock = view.findViewById(R.id.unblock);
+ viewSafetyNumber = view.findViewById(R.id.view_safety_number);
+ addToAGroup = view.findViewById(R.id.add_to_a_group);
+ muteNotificationsUntilLabel = view.findViewById(R.id.recipient_mute_notifications_until);
+ muteNotificationsSwitch = view.findViewById(R.id.recipient_mute_notifications_switch);
+ muteNotificationsRow = view.findViewById(R.id.recipient_mute_notifications_row);
+ customNotificationsButton = view.findViewById(R.id.recipient_custom_notifications_button);
+ customNotificationsRow = view.findViewById(R.id.recipient_custom_notifications_row);
+ toggleAllGroups = view.findViewById(R.id.toggle_all_groups);
+ messageButton = view.findViewById(R.id.recipient_message);
+ secureCallButton = view.findViewById(R.id.recipient_voice_call);
+ secureVideoCallButton = view.findViewById(R.id.recipient_video_call);
+
+ return view;
+ }
+
+ @Override
+ public void onActivityCreated(@Nullable Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ RecipientId recipientId = Objects.requireNonNull(requireArguments().getParcelable(RECIPIENT_ID));
+ ManageRecipientViewModel.Factory factory = new ManageRecipientViewModel.Factory(recipientId);
+
+ viewModel = ViewModelProviders.of(requireActivity(), factory).get(ManageRecipientViewModel.class);
+
+ viewModel.getVisibleSharedGroups().observe(getViewLifecycleOwner(), members -> sharedGroupList.setMembers(members));
+ viewModel.getSharedGroupsCountSummary().observe(getViewLifecycleOwner(), members -> groupsInCommonCount.setText(members));
+
+ viewModel.getCanCollapseMemberList().observe(getViewLifecycleOwner(), canCollapseMemberList -> {
+ if (canCollapseMemberList) {
+ toggleAllGroups.setVisibility(View.VISIBLE);
+ toggleAllGroups.setOnClickListener(v -> viewModel.revealCollapsedMembers());
+ } else {
+ toggleAllGroups.setVisibility(View.GONE);
+ }
+ });
+
+ viewModel.getIdentity().observe(getViewLifecycleOwner(), identityRecord -> {
+ viewSafetyNumber.setVisibility(identityRecord != null ? View.VISIBLE : View.GONE);
+
+ if (identityRecord != null) {
+ viewSafetyNumber.setOnClickListener(view -> viewModel.onViewSafetyNumberClicked(requireActivity(), identityRecord));
+ }
+ });
+
+ toolbar.setNavigationOnClickListener(v -> requireActivity().onBackPressed());
+ toolbar.setOnMenuItemClickListener(this::onMenuItemSelected);
+ toolbar.inflateMenu(R.menu.manage_recipient_fragment);
+
+ if (recipientId.equals(Recipient.self().getId())) {
+ toolbar.getMenu().findItem(R.id.action_edit).setVisible(true);
+ }
+
+ viewModel.getName().observe(getViewLifecycleOwner(), name::setText);
+ viewModel.getDisappearingMessageTimer().observe(getViewLifecycleOwner(), string -> disappearingMessages.setText(string));
+ viewModel.getRecipient().observe(getViewLifecycleOwner(), this::presentRecipient);
+ viewModel.getMediaCursor().observe(getViewLifecycleOwner(), this::presentMediaCursor);
+ viewModel.getMuteState().observe(getViewLifecycleOwner(), this::presentMuteState);
+
+ disappearingMessagesRow.setOnClickListener(v -> viewModel.handleExpirationSelection(requireContext()));
+ block.setOnClickListener(v -> viewModel.onBlockClicked(requireActivity()));
+ unblock.setOnClickListener(v -> viewModel.onUnblockClicked(requireActivity()));
+
+ addToAGroup.setOnClickListener(v -> viewModel.onAddToGroupButton(requireActivity()));
+
+ sharedGroupList.setRecipientClickListener(recipient -> viewModel.onGroupClicked(requireActivity(), recipient));
+ sharedGroupList.setOverScrollMode(View.OVER_SCROLL_NEVER);
+
+ muteNotificationsRow.setOnClickListener(v -> {
+ if (muteNotificationsSwitch.isEnabled()) {
+ muteNotificationsSwitch.toggle();
+ }
+ });
+
+ customNotificationsRow.setVisibility(View.VISIBLE);
+ customNotificationsRow.setOnClickListener(v -> CustomNotificationsDialogFragment.create(recipientId)
+ .show(requireFragmentManager(), "CUSTOM_NOTIFICATIONS"));
+
+ //noinspection CodeBlock2Expr
+ if (NotificationChannels.supported()) {
+ viewModel.hasCustomNotifications().observe(getViewLifecycleOwner(), hasCustomNotifications -> {
+ customNotificationsButton.setText(hasCustomNotifications ? R.string.ManageRecipientActivity_on
+ : R.string.ManageRecipientActivity_off);
+ });
+ }
+
+ viewModel.getCanBlock().observe(getViewLifecycleOwner(), canBlock -> {
+ block.setVisibility(canBlock ? View.VISIBLE : View.GONE);
+ unblock.setVisibility(canBlock ? View.GONE : View.VISIBLE);
+ });
+
+ messageButton.setOnClickListener(v -> viewModel.onMessage(requireActivity()));
+ secureCallButton.setOnClickListener(v -> viewModel.onSecureCall(requireActivity()));
+ secureVideoCallButton.setOnClickListener(v -> viewModel.onSecureVideoCall(requireActivity()));
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == RETURN_FROM_MEDIA) {
+ applyMediaCursorFactory();
+ }
+ }
+
+ private void presentRecipient(@NonNull Recipient recipient) {
+ disappearingMessagesCard.setVisibility(recipient.isRegistered() ? View.VISIBLE : View.GONE);
+ addToAGroup.setVisibility(recipient.isRegistered() ? View.VISIBLE : View.GONE);
+
+ MaterialColor recipientColor = recipient.getColor();
+ avatar.setFallbackPhotoProvider(new Recipient.FallbackPhotoProvider() {
+ @Override
+ public @NonNull FallbackContactPhoto getPhotoForRecipientWithoutName() {
+ return new FallbackPhoto80dp(R.drawable.ic_profile_80, recipientColor);
+ }
+ });
+ avatar.setRecipient(recipient);
+ avatar.setOnClickListener(v -> {
+ FragmentActivity activity = requireActivity();
+ activity.startActivity(AvatarPreviewActivity.intentFromRecipientId(activity, recipient.getId()),
+ AvatarPreviewActivity.createTransitionBundle(activity, avatar));
+ });
+
+ @ColorInt int color = recipientColor.toActionBarColor(requireContext());
+ Drawable[] colorDrawable = new Drawable[]{ContextCompat.getDrawable(requireContext(), R.drawable.colorpickerpreference_pref_swatch)};
+ colorChip.setImageDrawable(new ColorStateDrawable(colorDrawable, color));
+ colorRow.setOnClickListener(v -> handleColorSelection(color));
+
+ String usernameNumberString = String.format("%s %s", recipient.getUsername().or(""), recipient.getSmsAddress().or(""))
+ .trim();
+ usernameNumber.setText(usernameNumberString);
+ usernameNumber.setVisibility(TextUtils.isEmpty(usernameNumberString) ? View.GONE : View.VISIBLE);
+ usernameNumber.setOnLongClickListener(v -> {
+ Util.copyToClipboard(v.getContext(), usernameNumber.getText().toString());
+ ServiceUtil.getVibrator(v.getContext()).vibrate(250);
+ Toast.makeText(v.getContext(), R.string.RecipientBottomSheet_copied_to_clipboard, Toast.LENGTH_SHORT).show();
+ return true;
+ });
+
+ secureCallButton.setVisibility(recipient.isRegistered() ? View.VISIBLE : View.GONE);
+ secureVideoCallButton.setVisibility(recipient.isRegistered() ? View.VISIBLE : View.GONE);
+ }
+
+ private void presentMediaCursor(ManageRecipientViewModel.MediaCursor mediaCursor) {
+ if (mediaCursor == null) return;
+ sharedMediaRow.setOnClickListener(v -> startActivity(MediaOverviewActivity.forThread(requireContext(), mediaCursor.getThreadId())));
+
+ setMediaCursorFactory(mediaCursor.getMediaCursorFactory());
+
+ threadPhotoRailView.setListener(mediaRecord ->
+ startActivityForResult(MediaPreviewActivity.intentFromMediaRecord(requireContext(),
+ mediaRecord,
+ ViewCompat.getLayoutDirection(threadPhotoRailView) == ViewCompat.LAYOUT_DIRECTION_LTR),
+ RETURN_FROM_MEDIA));
+ }
+
+ private void presentMuteState(@NonNull ManageRecipientViewModel.MuteState muteState) {
+ if (muteNotificationsSwitch.isChecked() != muteState.isMuted()) {
+ muteNotificationsSwitch.setOnCheckedChangeListener(null);
+ muteNotificationsSwitch.setChecked(muteState.isMuted());
+ }
+
+ muteNotificationsSwitch.setEnabled(true);
+ muteNotificationsSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ if (isChecked) {
+ MuteDialog.show(requireContext(), viewModel::setMuteUntil, () -> muteNotificationsSwitch.setChecked(false));
+ } else {
+ viewModel.clearMuteUntil();
+ }
+ });
+ muteNotificationsUntilLabel.setVisibility(muteState.isMuted() ? View.VISIBLE : View.GONE);
+
+ if (muteState.isMuted()) {
+ muteNotificationsUntilLabel.setText(getString(R.string.ManageRecipientActivity_until_s,
+ DateUtils.getTimeString(requireContext(),
+ Locale.getDefault(),
+ muteState.getMutedUntil())));
+ }
+ }
+
+ private void handleColorSelection(@ColorInt int currentColor) {
+ @ColorInt int[] colors = MaterialColors.CONVERSATION_PALETTE.asConversationColorArray(requireContext());
+
+ ColorPickerDialog.Params params = new ColorPickerDialog.Params.Builder(requireContext())
+ .setSelectedColor(currentColor)
+ .setColors(colors)
+ .setSize(ColorPickerDialog.SIZE_SMALL)
+ .setSortColors(false)
+ .setColumns(3)
+ .build();
+
+ ColorPickerDialog dialog = new ColorPickerDialog(requireActivity(), color -> viewModel.onSelectColor(color), params);
+ dialog.setTitle(R.string.ManageRecipientActivity_chat_color);
+ dialog.show();
+ }
+
+ public boolean onMenuItemSelected(@NonNull MenuItem item) {
+ if (item.getItemId() == R.id.action_edit) {
+ startActivity(EditProfileActivity.getIntentForUserProfileEdit(requireActivity()));
+ return true;
+ }
+
+ return false;
+ }
+
+ private void setMediaCursorFactory(@Nullable ManageRecipientViewModel.CursorFactory cursorFactory) {
+ if (this.cursorFactory != cursorFactory) {
+ this.cursorFactory = cursorFactory;
+ applyMediaCursorFactory();
+ }
+ }
+
+ private void applyMediaCursorFactory() {
+ Context context = getContext();
+ if (context == null) return;
+ if (cursorFactory != null) {
+ Cursor cursor = cursorFactory.create();
+ getViewLifecycleOwner().getLifecycle().addObserver(new LifecycleCursorWrapper(cursor));
+
+ threadPhotoRailView.setCursor(GlideApp.with(context), cursor);
+ mediaCard.setVisibility(cursor.getCount() > 0 ? View.VISIBLE : View.GONE);
+ } else {
+ threadPhotoRailView.setCursor(GlideApp.with(context), null);
+ mediaCard.setVisibility(View.GONE);
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientRepository.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientRepository.java
new file mode 100644
index 0000000000..73fae6eff0
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientRepository.java
@@ -0,0 +1,108 @@
+package org.thoughtcrime.securesms.recipients.ui.managerecipient;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.WorkerThread;
+import androidx.core.util.Consumer;
+
+import com.annimon.stream.Stream;
+
+import org.thoughtcrime.securesms.color.MaterialColor;
+import org.thoughtcrime.securesms.color.MaterialColors;
+import org.thoughtcrime.securesms.database.DatabaseFactory;
+import org.thoughtcrime.securesms.database.GroupDatabase;
+import org.thoughtcrime.securesms.database.IdentityDatabase;
+import org.thoughtcrime.securesms.database.ThreadDatabase;
+import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage;
+import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.recipients.RecipientId;
+import org.thoughtcrime.securesms.sms.MessageSender;
+import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
+import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
+
+import java.util.ArrayList;
+import java.util.List;
+
+final class ManageRecipientRepository {
+
+ private final Context context;
+ private final RecipientId recipientId;
+
+ ManageRecipientRepository(@NonNull Context context, @NonNull RecipientId recipientId) {
+ this.context = context;
+ this.recipientId = recipientId;
+ }
+
+ public RecipientId getRecipientId() {
+ return recipientId;
+ }
+
+ void getThreadId(@NonNull Consumer onGetThreadId) {
+ SignalExecutors.BOUNDED.execute(() -> onGetThreadId.accept(getThreadId()));
+ }
+
+ @WorkerThread
+ private long getThreadId() {
+ ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
+ Recipient groupRecipient = Recipient.resolved(recipientId);
+
+ return threadDatabase.getThreadIdFor(groupRecipient);
+ }
+
+ void getIdentity(@NonNull Consumer callback) {
+ SignalExecutors.BOUNDED.execute(() -> callback.accept(DatabaseFactory.getIdentityDatabase(context)
+ .getIdentity(recipientId)
+ .orNull()));
+ }
+
+ void setExpiration(int newExpirationTime) {
+ SignalExecutors.BOUNDED.execute(() -> {
+ DatabaseFactory.getRecipientDatabase(context).setExpireMessages(recipientId, newExpirationTime);
+ OutgoingExpirationUpdateMessage outgoingMessage = new OutgoingExpirationUpdateMessage(Recipient.resolved(recipientId), System.currentTimeMillis(), newExpirationTime * 1000L);
+ MessageSender.send(context, outgoingMessage, getThreadId(), false, null);
+ });
+ }
+
+ void getGroupMembership(@NonNull Consumer> onComplete) {
+ SignalExecutors.BOUNDED.execute(() -> {
+ GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
+ List groupRecords = groupDatabase.getPushGroupsContainingMember(recipientId);
+ ArrayList groupRecipients = new ArrayList<>(groupRecords.size());
+
+ for (GroupDatabase.GroupRecord groupRecord : groupRecords) {
+ groupRecipients.add(groupRecord.getRecipientId());
+ }
+
+ onComplete.accept(groupRecipients);
+ });
+ }
+
+ public void getRecipient(@NonNull Consumer recipientCallback) {
+ SignalExecutors.BOUNDED.execute(() -> recipientCallback.accept(Recipient.resolved(recipientId)));
+ }
+
+ void setMuteUntil(long until) {
+ SignalExecutors.BOUNDED.execute(() -> DatabaseFactory.getRecipientDatabase(context).setMuted(recipientId, until));
+ }
+
+ void setColor(int color) {
+ SignalExecutors.BOUNDED.execute(() -> {
+ MaterialColor selectedColor = MaterialColors.CONVERSATION_PALETTE.getByColor(context, color);
+ if (selectedColor != null) {
+ DatabaseFactory.getRecipientDatabase(context).setColor(recipientId, selectedColor);
+ }
+ });
+ }
+
+ @WorkerThread
+ @NonNull List getSharedGroups(@NonNull RecipientId recipientId) {
+ return Stream.of(DatabaseFactory.getGroupDatabase(context)
+ .getPushGroupsContainingMember(recipientId))
+ .filter(g -> g.getMembers().contains(Recipient.self().getId()))
+ .map(GroupDatabase.GroupRecord::getRecipientId)
+ .map(Recipient::resolved)
+ .sortBy(gr -> gr.getDisplayName(context))
+ .toList();
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientViewModel.java
new file mode 100644
index 0000000000..51cb2515ba
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientViewModel.java
@@ -0,0 +1,283 @@
+package org.thoughtcrime.securesms.recipients.ui.managerecipient;
+
+import android.app.Activity;
+import android.content.Context;
+import android.database.Cursor;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.WorkerThread;
+import androidx.core.util.Consumer;
+import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.Transformations;
+import androidx.lifecycle.ViewModel;
+import androidx.lifecycle.ViewModelProvider;
+
+import com.annimon.stream.Stream;
+
+import org.thoughtcrime.securesms.BlockUnblockDialog;
+import org.thoughtcrime.securesms.ExpirationDialog;
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.VerifyIdentityActivity;
+import org.thoughtcrime.securesms.database.IdentityDatabase;
+import org.thoughtcrime.securesms.database.MediaDatabase;
+import org.thoughtcrime.securesms.database.loaders.MediaLoader;
+import org.thoughtcrime.securesms.database.loaders.ThreadMediaLoader;
+import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
+import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
+import org.thoughtcrime.securesms.groups.ui.addtogroup.AddToGroupsActivity;
+import org.thoughtcrime.securesms.notifications.NotificationChannels;
+import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.recipients.RecipientId;
+import org.thoughtcrime.securesms.recipients.RecipientUtil;
+import org.thoughtcrime.securesms.util.CommunicationActions;
+import org.thoughtcrime.securesms.util.DefaultValueLiveData;
+import org.thoughtcrime.securesms.util.ExpirationUtil;
+import org.thoughtcrime.securesms.util.Util;
+import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
+
+import java.util.List;
+
+public final class ManageRecipientViewModel extends ViewModel {
+
+ private static final int MAX_COLLAPSED_GROUPS = 5;
+
+ private final Context context;
+ private final ManageRecipientRepository manageRecipientRepository;
+ private final LiveData name;
+ private final LiveData disappearingMessageTimer;
+ private final MutableLiveData identity;
+ private final LiveData recipient;
+ private final MutableLiveData mediaCursor = new MutableLiveData<>(null);
+ private final LiveData muteState;
+ private final LiveData hasCustomNotifications;
+ private final LiveData canCollapseMemberList;
+ private final DefaultValueLiveData groupListCollapseState = new DefaultValueLiveData<>(CollapseState.COLLAPSED);
+ private final LiveData canBlock;
+ private final LiveData> visibleSharedGroups;
+ private final LiveData sharedGroupsCountSummary;
+
+ private ManageRecipientViewModel(@NonNull Context context, @NonNull ManageRecipientRepository manageRecipientRepository) {
+ this.context = context;
+ this.manageRecipientRepository = manageRecipientRepository;
+
+ manageRecipientRepository.getThreadId(this::onThreadIdLoaded);
+
+ this.recipient = Recipient.live(manageRecipientRepository.getRecipientId()).getLiveData();
+ this.name = Transformations.map(recipient, r -> r.getDisplayName(context));
+ this.identity = new MutableLiveData<>();
+
+ LiveData> allSharedGroups = LiveDataUtil.mapAsync(this.recipient, r -> manageRecipientRepository.getSharedGroups(r.getId()));
+
+ this.sharedGroupsCountSummary = Transformations.map(allSharedGroups, list -> {
+ int size = list.size();
+ return size == 0 ? context.getString(R.string.ManageRecipientActivity_no_groups_in_common)
+ : context.getResources().getQuantityString(R.plurals.ManageRecipientActivity_d_groups_in_common, size, size);
+ });
+
+ this.canCollapseMemberList = LiveDataUtil.combineLatest(this.groupListCollapseState,
+ Transformations.map(allSharedGroups, m -> m.size() > MAX_COLLAPSED_GROUPS),
+ (state, hasEnoughMembers) -> state != CollapseState.OPEN && hasEnoughMembers);
+ this.visibleSharedGroups = Transformations.map(LiveDataUtil.combineLatest(allSharedGroups,
+ this.groupListCollapseState,
+ ManageRecipientViewModel::filterSharedGroupList),
+ recipients -> Stream.of(recipients).map(r -> new GroupMemberEntry.FullMember(r, false)).toList());
+
+ this.disappearingMessageTimer = Transformations.map(this.recipient, r -> ExpirationUtil.getExpirationDisplayValue(context, r.getExpireMessages()));
+ this.muteState = Transformations.map(this.recipient, r -> new MuteState(r.getMuteUntil(), r.isMuted()));
+ this.hasCustomNotifications = Transformations.map(this.recipient, r -> r.getNotificationChannel() != null || !NotificationChannels.supported());
+ this.canBlock = Transformations.map(this.recipient, r -> !r.isBlocked());
+
+ boolean isSelf = manageRecipientRepository.getRecipientId().equals(Recipient.self().getId());
+ if (!isSelf) {
+ manageRecipientRepository.getIdentity(identity::postValue);
+ }
+ }
+
+ @WorkerThread
+ private void onThreadIdLoaded(long threadId) {
+ mediaCursor.postValue(new MediaCursor(threadId,
+ () -> new ThreadMediaLoader(context, threadId, MediaLoader.MediaType.GALLERY, MediaDatabase.Sorting.Newest).getCursor()));
+ }
+
+ public LiveData getName() {
+ return name;
+ }
+
+ public LiveData getRecipient() {
+ return recipient;
+ }
+
+ LiveData getMediaCursor() {
+ return mediaCursor;
+ }
+
+ LiveData getMuteState() {
+ return muteState;
+ }
+
+ LiveData getDisappearingMessageTimer() {
+ return disappearingMessageTimer;
+ }
+
+ LiveData hasCustomNotifications() {
+ return hasCustomNotifications;
+ }
+
+ LiveData getCanCollapseMemberList() {
+ return canCollapseMemberList;
+ }
+
+ LiveData getCanBlock() {
+ return canBlock;
+ }
+
+ void handleExpirationSelection(@NonNull Context context) {
+ withRecipient(recipient ->
+ ExpirationDialog.show(context,
+ recipient.getExpireMessages(),
+ manageRecipientRepository::setExpiration));
+ }
+
+ void setMuteUntil(long muteUntil) {
+ manageRecipientRepository.setMuteUntil(muteUntil);
+ }
+
+ void clearMuteUntil() {
+ manageRecipientRepository.setMuteUntil(0);
+ }
+
+ void revealCollapsedMembers() {
+ groupListCollapseState.setValue(CollapseState.OPEN);
+ }
+
+ void onAddToGroupButton(@NonNull Activity activity) {
+ manageRecipientRepository.getGroupMembership(existingGroups -> Util.runOnMain(() -> activity.startActivity(AddToGroupsActivity.newIntent(activity, manageRecipientRepository.getRecipientId(), existingGroups))));
+ }
+
+ private void withRecipient(@NonNull Consumer mainThreadRecipientCallback) {
+ manageRecipientRepository.getRecipient(recipient -> Util.runOnMain(() -> mainThreadRecipientCallback.accept(recipient)));
+ }
+
+ private static @NonNull List filterSharedGroupList(@NonNull List groups,
+ @NonNull CollapseState collapseState)
+ {
+ if (collapseState == CollapseState.COLLAPSED && groups.size() > MAX_COLLAPSED_GROUPS) {
+ return groups.subList(0, MAX_COLLAPSED_GROUPS);
+ } else {
+ return groups;
+ }
+ }
+
+ LiveData getIdentity() {
+ return identity;
+ }
+
+ void onBlockClicked(@NonNull FragmentActivity activity) {
+ withRecipient(recipient -> BlockUnblockDialog.showBlockFor(activity, activity.getLifecycle(), recipient, () -> RecipientUtil.block(context, recipient)));
+ }
+
+ void onUnblockClicked(@NonNull FragmentActivity activity) {
+ withRecipient(recipient -> BlockUnblockDialog.showUnblockFor(activity, activity.getLifecycle(), recipient, () -> RecipientUtil.unblock(context, recipient)));
+ }
+
+ void onViewSafetyNumberClicked(@NonNull Activity activity, @NonNull IdentityDatabase.IdentityRecord identityRecord) {
+ activity.startActivity(VerifyIdentityActivity.newIntent(activity, identityRecord));
+ }
+
+ LiveData> getVisibleSharedGroups() {
+ return visibleSharedGroups;
+ }
+
+ LiveData getSharedGroupsCountSummary() {
+ return sharedGroupsCountSummary;
+ }
+
+ void onSelectColor(int color) {
+ manageRecipientRepository.setColor(color);
+ }
+
+ void onGroupClicked(@NonNull Activity activity, @NonNull Recipient recipient) {
+ CommunicationActions.startConversation(activity, recipient, null);
+ activity.finish();
+ }
+
+ void onMessage(@NonNull FragmentActivity activity) {
+ withRecipient(r -> {
+ CommunicationActions.startConversation(activity, r, null);
+ activity.finish();
+ });
+ }
+
+ void onSecureCall(@NonNull FragmentActivity activity) {
+ withRecipient(r -> CommunicationActions.startVoiceCall(activity, r));
+ }
+
+ void onSecureVideoCall(@NonNull FragmentActivity activity) {
+ withRecipient(r -> CommunicationActions.startVideoCall(activity, r));
+ }
+
+ static final class MediaCursor {
+ private final long threadId;
+ @NonNull private final CursorFactory mediaCursorFactory;
+
+ private MediaCursor(long threadId,
+ @NonNull CursorFactory mediaCursorFactory)
+ {
+ this.threadId = threadId;
+ this.mediaCursorFactory = mediaCursorFactory;
+ }
+
+ long getThreadId() {
+ return threadId;
+ }
+
+ @NonNull CursorFactory getMediaCursorFactory() {
+ return mediaCursorFactory;
+ }
+ }
+
+ static final class MuteState {
+ private final long mutedUntil;
+ private final boolean isMuted;
+
+ MuteState(long mutedUntil, boolean isMuted) {
+ this.mutedUntil = mutedUntil;
+ this.isMuted = isMuted;
+ }
+
+ long getMutedUntil() {
+ return mutedUntil;
+ }
+
+ public boolean isMuted() {
+ return isMuted;
+ }
+ }
+
+ private enum CollapseState {
+ OPEN,
+ COLLAPSED
+ }
+
+ interface CursorFactory {
+ Cursor create();
+ }
+
+ public static class Factory implements ViewModelProvider.Factory {
+ private final Context context;
+ private final RecipientId recipientId;
+
+ public Factory(@NonNull RecipientId recipientId) {
+ this.context = ApplicationDependencies.getApplication();
+ this.recipientId = recipientId;
+ }
+
+ @Override
+ public @NonNull T create(@NonNull Class modelClass) {
+ //noinspection unchecked
+ return (T) new ManageRecipientViewModel(context, new ManageRecipientRepository(context, recipientId));
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/notifications/CustomNotificationsDialogFragment.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/notifications/CustomNotificationsDialogFragment.java
index 891969ba40..405064d8dd 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/notifications/CustomNotificationsDialogFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/notifications/CustomNotificationsDialogFragment.java
@@ -16,6 +16,7 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.SwitchCompat;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.DialogFragment;
@@ -25,21 +26,31 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.recipients.RecipientId;
+import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ThemeUtil;
import java.util.Objects;
public class CustomNotificationsDialogFragment extends DialogFragment {
- private static final short RINGTONE_PICKER_REQUEST_CODE = 13562;
+ private static final short MESSAGE_RINGTONE_PICKER_REQUEST_CODE = 13562;
+ private static final short CALL_RINGTONE_PICKER_REQUEST_CODE = 23621;
private static final String ARG_RECIPIENT_ID = "recipient_id";
+ private View customNotificationsRow;
private SwitchCompat customNotificationsSwitch;
+ private View soundRow;
private View soundLabel;
private TextView soundSelector;
+ private View vibrateRow;
private View vibrateLabel;
private SwitchCompat vibrateSwitch;
+ private View callHeading;
+ private View ringtoneRow;
+ private TextView ringtoneSelector;
+ private View callVibrateRow;
+ private TextView callVibrateSelector;
private CustomNotificationsViewModel viewModel;
@@ -79,10 +90,13 @@ public class CustomNotificationsDialogFragment extends DialogFragment {
@Override
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
- if (requestCode == RINGTONE_PICKER_REQUEST_CODE && resultCode == Activity.RESULT_OK && data != null) {
+ if (resultCode == Activity.RESULT_OK && data != null) {
Uri uri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
-
- viewModel.setMessageSound(uri);
+ if (requestCode == MESSAGE_RINGTONE_PICKER_REQUEST_CODE) {
+ viewModel.setMessageSound(uri);
+ } else if (requestCode == CALL_RINGTONE_PICKER_REQUEST_CODE) {
+ viewModel.setCallSound(uri);
+ }
}
}
@@ -96,11 +110,19 @@ public class CustomNotificationsDialogFragment extends DialogFragment {
}
private void initializeViews(@NonNull View view) {
+ customNotificationsRow = view.findViewById(R.id.custom_notifications_row);
customNotificationsSwitch = view.findViewById(R.id.custom_notifications_enable_switch);
+ soundRow = view.findViewById(R.id.custom_notifications_sound_row);
soundLabel = view.findViewById(R.id.custom_notifications_sound_label);
soundSelector = view.findViewById(R.id.custom_notifications_sound_selection);
+ vibrateRow = view.findViewById(R.id.custom_notifications_vibrate_row);
vibrateLabel = view.findViewById(R.id.custom_notifications_vibrate_label);
vibrateSwitch = view.findViewById(R.id.custom_notifications_vibrate_switch);
+ callHeading = view.findViewById(R.id.custom_notifications_call_settings_section_header);
+ ringtoneRow = view.findViewById(R.id.custom_notifications_ringtone_row);
+ ringtoneSelector = view.findViewById(R.id.custom_notifications_ringtone_selection);
+ callVibrateRow = view.findViewById(R.id.custom_notifications_call_vibrate_row);
+ callVibrateSelector = view.findViewById(R.id.custom_notifications_call_vibrate_selection);
Toolbar toolbar = view.findViewById(R.id.custom_notifications_toolbar);
@@ -119,8 +141,11 @@ public class CustomNotificationsDialogFragment extends DialogFragment {
}
customNotificationsSwitch.setOnCheckedChangeListener(onCustomNotificationsSwitchCheckChangedListener);
+ customNotificationsRow.setOnClickListener(v -> customNotificationsSwitch.toggle());
+ soundRow.setEnabled(hasCustomNotifications);
soundLabel.setEnabled(hasCustomNotifications);
+ vibrateRow.setEnabled(hasCustomNotifications);
vibrateLabel.setEnabled(hasCustomNotifications);
soundSelector.setVisibility(hasCustomNotifications ? View.VISIBLE : View.GONE);
vibrateSwitch.setVisibility(hasCustomNotifications ? View.VISIBLE : View.GONE);
@@ -145,18 +170,42 @@ public class CustomNotificationsDialogFragment extends DialogFragment {
vibrateSwitch.setOnCheckedChangeListener(onVibrateSwitchCheckChangedListener);
});
+ vibrateRow.setOnClickListener(v -> vibrateSwitch.toggle());
viewModel.getNotificationSound().observe(getViewLifecycleOwner(), sound -> {
soundSelector.setText(getRingtoneSummary(requireContext(), sound));
soundSelector.setTag(sound);
+ soundRow.setOnClickListener(v -> launchSoundSelector(sound, false));
});
- soundSelector.setOnClickListener(v -> launchSoundSelector(viewModel.getNotificationSound().getValue()));
+ viewModel.getShowCallingOptions().observe(getViewLifecycleOwner(), showCalling -> {
+ callHeading.setVisibility(showCalling ? View.VISIBLE : View.GONE);
+ ringtoneRow.setVisibility(showCalling ? View.VISIBLE : View.GONE);
+ callVibrateRow.setVisibility(showCalling ? View.VISIBLE : View.GONE);
+ });
+
+ viewModel.getRingtone().observe(getViewLifecycleOwner(), sound -> {
+ ringtoneSelector.setText(getRingtoneSummary(requireContext(), sound));
+ ringtoneSelector.setTag(sound);
+ ringtoneRow.setOnClickListener(v -> launchSoundSelector(sound, true));
+ });
+
+ viewModel.getCallingVibrateState().observe(getViewLifecycleOwner(), vibrateState -> {
+ String vibrateSummary = getVibrateSummary(requireContext(), vibrateState);
+ callVibrateSelector.setText(vibrateSummary);
+ callVibrateRow.setOnClickListener(v -> new AlertDialog.Builder(requireContext())
+ .setTitle(R.string.CustomNotificationsDialogFragment__vibrate)
+ .setSingleChoiceItems(R.array.recipient_vibrate_entries, vibrateState.ordinal(), ((dialog, which) -> {
+ viewModel.setCallingVibrate(RecipientDatabase.VibrateState.fromId(which));
+ dialog.dismiss();
+ })).setNegativeButton(android.R.string.cancel, null)
+ .show());
+ });
}
private @NonNull String getRingtoneSummary(@NonNull Context context, @Nullable Uri ringtone) {
if (ringtone == null) {
- return context.getString(R.string.preferences__default);
+ return context.getString(R.string.CustomNotificationsDialogFragment__default);
} else if (ringtone.toString().isEmpty()) {
return context.getString(R.string.preferences__silent);
} else {
@@ -167,18 +216,38 @@ public class CustomNotificationsDialogFragment extends DialogFragment {
}
}
- return context.getString(R.string.preferences__default);
+ return context.getString(R.string.CustomNotificationsDialogFragment__default);
}
- private void launchSoundSelector(@Nullable Uri current) {
+ private void launchSoundSelector(@Nullable Uri current, boolean calls) {
Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER);
- intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true);
- intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true);
- intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_NOTIFICATION);
- intent.putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, Settings.System.DEFAULT_NOTIFICATION_URI);
- intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, current);
+ if (current == null) current = calls ? Settings.System.DEFAULT_RINGTONE_URI : Settings.System.DEFAULT_NOTIFICATION_URI;
+ else if (current.toString().isEmpty()) current = null;
- startActivityForResult(intent, RINGTONE_PICKER_REQUEST_CODE);
+ intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true);
+ intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true);
+ intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, calls ? RingtoneManager.TYPE_RINGTONE : RingtoneManager.TYPE_NOTIFICATION);
+ intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, current);
+ intent.putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, defaultSound(calls));
+
+ startActivityForResult(intent, calls ? CALL_RINGTONE_PICKER_REQUEST_CODE : MESSAGE_RINGTONE_PICKER_REQUEST_CODE);
+ }
+
+ private Uri defaultSound(boolean calls) {
+ Uri defaultValue;
+
+ if (calls) defaultValue = TextSecurePreferences.getCallNotificationRingtone(requireContext());
+ else defaultValue = TextSecurePreferences.getNotificationRingtone(requireContext());
+ return defaultValue;
+ }
+
+ private static @NonNull String getVibrateSummary(@NonNull Context context, @NonNull RecipientDatabase.VibrateState vibrateState) {
+ switch (vibrateState) {
+ case DEFAULT : return context.getString(R.string.CustomNotificationsDialogFragment__default);
+ case ENABLED : return context.getString(R.string.CustomNotificationsDialogFragment__enabled);
+ case DISABLED : return context.getString(R.string.CustomNotificationsDialogFragment__disabled);
+ default : throw new AssertionError();
+ }
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/notifications/CustomNotificationsRepository.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/notifications/CustomNotificationsRepository.java
index 43715a149d..44b6f028e3 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/notifications/CustomNotificationsRepository.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/notifications/CustomNotificationsRepository.java
@@ -61,6 +61,10 @@ class CustomNotificationsRepository {
});
}
+ void setCallingVibrate(final RecipientDatabase.VibrateState vibrateState) {
+ SignalExecutors.SERIAL.execute(() -> DatabaseFactory.getRecipientDatabase(context).setCallVibrate(recipientId, vibrateState));
+ }
+
void setMessageSound(@Nullable Uri sound) {
SignalExecutors.SERIAL.execute(() -> {
Recipient recipient = getRecipient();
@@ -76,6 +80,19 @@ class CustomNotificationsRepository {
});
}
+ void setCallSound(@Nullable Uri sound) {
+ SignalExecutors.SERIAL.execute(() -> {
+ Uri defaultValue = TextSecurePreferences.getCallNotificationRingtone(context);
+ Uri newValue;
+
+ if (defaultValue.equals(sound)) newValue = null;
+ else if (sound == null) newValue = Uri.EMPTY;
+ else newValue = sound;
+
+ DatabaseFactory.getRecipientDatabase(context).setCallRingtone(recipientId, newValue);
+ });
+ }
+
@WorkerThread
private void createCustomNotificationChannel() {
Recipient recipient = getRecipient();
diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/notifications/CustomNotificationsViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/notifications/CustomNotificationsViewModel.java
index b5b6eae306..f5b726253f 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/notifications/CustomNotificationsViewModel.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/notifications/CustomNotificationsViewModel.java
@@ -22,14 +22,20 @@ public final class CustomNotificationsViewModel extends ViewModel {
private final LiveData notificationSound;
private final CustomNotificationsRepository repository;
private final MutableLiveData isInitialLoadComplete = new MutableLiveData<>();
+ private final LiveData showCallingOptions;
+ private final LiveData ringtone;
+ private final LiveData isCallingVibrateEnabled;
private CustomNotificationsViewModel(@NonNull RecipientId recipientId, @NonNull CustomNotificationsRepository repository) {
LiveData recipient = Recipient.live(recipientId).getLiveData();
- this.repository = repository;
- this.hasCustomNotifications = Transformations.map(recipient, r -> r.getNotificationChannel() != null || !NotificationChannels.supported());
- this.isVibrateEnabled = Transformations.map(recipient, Recipient::getMessageVibrate);
- this.notificationSound = Transformations.map(recipient, Recipient::getMessageRingtone);
+ this.repository = repository;
+ this.hasCustomNotifications = Transformations.map(recipient, r -> r.getNotificationChannel() != null || !NotificationChannels.supported());
+ this.isVibrateEnabled = Transformations.map(recipient, Recipient::getMessageVibrate);
+ this.notificationSound = Transformations.map(recipient, Recipient::getMessageRingtone);
+ this.showCallingOptions = Transformations.map(recipient, r -> !r.isGroup() && r.isRegistered());
+ this.ringtone = Transformations.map(recipient, Recipient::getCallRingtone);
+ this.isCallingVibrateEnabled = Transformations.map(recipient, Recipient::getCallVibrate);
repository.onLoad(() -> isInitialLoadComplete.postValue(true));
}
@@ -62,6 +68,26 @@ public final class CustomNotificationsViewModel extends ViewModel {
repository.setMessageSound(sound);
}
+ public void setCallSound(@Nullable Uri sound) {
+ repository.setCallSound(sound);
+ }
+
+ public LiveData getShowCallingOptions() {
+ return showCallingOptions;
+ }
+
+ public LiveData getRingtone() {
+ return ringtone;
+ }
+
+ public LiveData getCallingVibrateState() {
+ return isCallingVibrateEnabled;
+ }
+
+ public void setCallingVibrate(@NonNull RecipientDatabase.VibrateState vibrateState) {
+ repository.setCallingVibrate(vibrateState);
+ }
+
public static final class Factory implements ViewModelProvider.Factory {
private final RecipientId recipientId;
diff --git a/app/src/main/res/drawable/ic_message_outline_ultramarine_24.xml b/app/src/main/res/drawable/ic_message_outline_ultramarine_24.xml
new file mode 100644
index 0000000000..b7d6f1bdd6
--- /dev/null
+++ b/app/src/main/res/drawable/ic_message_outline_ultramarine_24.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_message_solid_ultramarine_light_24.xml b/app/src/main/res/drawable/ic_message_solid_ultramarine_light_24.xml
new file mode 100644
index 0000000000..546426482c
--- /dev/null
+++ b/app/src/main/res/drawable/ic_message_solid_ultramarine_light_24.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_phone_right_outline_ultramarine_24.xml b/app/src/main/res/drawable/ic_phone_right_outline_ultramarine_24.xml
new file mode 100644
index 0000000000..1c974bc4c3
--- /dev/null
+++ b/app/src/main/res/drawable/ic_phone_right_outline_ultramarine_24.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_phone_right_solid_ultramarine_light_24.xml b/app/src/main/res/drawable/ic_phone_right_solid_ultramarine_light_24.xml
new file mode 100644
index 0000000000..011a7d7cb8
--- /dev/null
+++ b/app/src/main/res/drawable/ic_phone_right_solid_ultramarine_light_24.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_video_outline_ultramarine_24.xml b/app/src/main/res/drawable/ic_video_outline_ultramarine_24.xml
new file mode 100644
index 0000000000..b4ac22da3b
--- /dev/null
+++ b/app/src/main/res/drawable/ic_video_outline_ultramarine_24.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_video_solid_ultramarine_light_24.xml b/app/src/main/res/drawable/ic_video_solid_ultramarine_light_24.xml
new file mode 100644
index 0000000000..61f3a0d3b8
--- /dev/null
+++ b/app/src/main/res/drawable/ic_video_solid_ultramarine_light_24.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/layout/add_group_details_fragment.xml b/app/src/main/res/layout/add_group_details_fragment.xml
index 6b709fb7a1..9864926853 100644
--- a/app/src/main/res/layout/add_group_details_fragment.xml
+++ b/app/src/main/res/layout/add_group_details_fragment.xml
@@ -25,7 +25,7 @@
app:layout_constraintTop_toBottomOf="@id/toolbar" />
-
-
-
+ app:layout_constraintTop_toBottomOf="@id/custom_notifications_message_section_header">
-
+
+
+
+
+
+
+ app:layout_constraintTop_toBottomOf="@id/custom_notifications_row">
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/custom_notifications_vibrate_row"
+ tools:visibility="visible" />
-
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/custom_notifications_call_settings_section_header"
+ tools:visibility="visible">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/group_manage_fragment.xml b/app/src/main/res/layout/group_manage_fragment.xml
index 4b6c77d62f..7568ce5d68 100644
--- a/app/src/main/res/layout/group_manage_fragment.xml
+++ b/app/src/main/res/layout/group_manage_fragment.xml
@@ -70,7 +70,7 @@
android:layout_marginEnd="10dp" />
+ app:layout_behavior=".recipients.ui.RecipientSettingsCoordinatorLayoutBehavior" />
+
+
+
+
+
diff --git a/app/src/main/res/layout/recipient_manage_fragment.xml b/app/src/main/res/layout/recipient_manage_fragment.xml
new file mode 100644
index 0000000000..ab578a5686
--- /dev/null
+++ b/app/src/main/res/layout/recipient_manage_fragment.xml
@@ -0,0 +1,546 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/menu/manage_recipient_fragment.xml b/app/src/main/res/menu/manage_recipient_fragment.xml
new file mode 100644
index 0000000000..0dcd8df502
--- /dev/null
+++ b/app/src/main/res/menu/manage_recipient_fragment.xml
@@ -0,0 +1,10 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
index 5f81def74d..5418fa656f 100644
--- a/app/src/main/res/values/attrs.xml
+++ b/app/src/main/res/values/attrs.xml
@@ -301,6 +301,11 @@
+
+
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index ea35267a67..54700e29fb 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -555,6 +555,31 @@
Choose who can add or invite new members
Choose who can change the group name and photo
+
+
+
+ Disappearing messages
+ Chat color
+ Block
+ Unblock
+ View safety number
+ Mute notifications
+ Custom notifications
+ Until %1$s
+ Off
+ On
+ Add to a group
+ View all groups
+ See all
+ No groups in common
+
+ - %d group in common
+ - %d groups in common
+
+ Edit name and picture
+ Message
+ Voice call
+ Video call
- %1$s invited 1 person
@@ -567,6 +592,11 @@
Use custom notifications
Notification sound
Vibrate
+ Call settings
+ Ringtone
+ Enabled
+ Disabled
+ Default
Do you want to cancel the invite you sent to %1$s?
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 21e4412ebc..f5b513590a 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -448,4 +448,26 @@
- 0.5dp
- ?android:attr/windowBackground
+
+
+
+
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index f877e67b25..03a16b3fbf 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -437,6 +437,11 @@
- @color/core_grey_90
- @drawable/progress_button_state_light
+
+ - @drawable/ic_message_outline_ultramarine_24
+ - @drawable/ic_phone_right_outline_ultramarine_24
+ - @drawable/ic_video_outline_ultramarine_24
+ - @color/core_grey_02