From b53827f32b5a50fa90bdd6c3cfb56dafff986050 Mon Sep 17 00:00:00 2001 From: Alan Evans Date: Tue, 16 Jun 2020 17:42:54 -0300 Subject: [PATCH] Manage recipient activity. --- app/src/main/AndroidManifest.xml | 4 + .../ApplicationPreferencesActivity.java | 7 +- .../RecipientPreferenceActivity.java | 10 + .../securesms/color/MaterialColors.java | 4 +- ...ackPhoto80.java => FallbackPhoto80dp.java} | 19 +- .../details/AddGroupDetailsFragment.java | 2 +- .../ui/managegroup/ManageGroupFragment.java | 7 +- .../profiles/edit/EditProfileActivity.java | 9 +- ...entSettingsCoordinatorLayoutBehavior.java} | 24 +- .../ManageRecipientActivity.java | 60 ++ .../ManageRecipientFragment.java | 349 +++++++++++ .../ManageRecipientRepository.java | 108 ++++ .../ManageRecipientViewModel.java | 283 +++++++++ .../CustomNotificationsDialogFragment.java | 97 +++- .../CustomNotificationsRepository.java | 17 + .../CustomNotificationsViewModel.java | 34 +- .../ic_message_outline_ultramarine_24.xml | 9 + .../ic_message_solid_ultramarine_light_24.xml | 9 + .../ic_phone_right_outline_ultramarine_24.xml | 9 + ...phone_right_solid_ultramarine_light_24.xml | 9 + .../ic_video_outline_ultramarine_24.xml | 9 + .../ic_video_solid_ultramarine_light_24.xml | 9 + .../res/layout/add_group_details_fragment.xml | 2 +- .../custom_notifications_dialog_fragment.xml | 239 ++++++-- .../main/res/layout/group_manage_fragment.xml | 6 +- .../res/layout/recipient_manage_activity.xml | 13 + .../res/layout/recipient_manage_fragment.xml | 546 ++++++++++++++++++ .../res/menu/manage_recipient_fragment.xml | 10 + app/src/main/res/values/attrs.xml | 5 + app/src/main/res/values/strings.xml | 30 + app/src/main/res/values/styles.xml | 22 + app/src/main/res/values/themes.xml | 10 + 32 files changed, 1869 insertions(+), 102 deletions(-) rename app/src/main/java/org/thoughtcrime/securesms/contacts/avatars/{GroupFallbackPhoto80.java => FallbackPhoto80dp.java} (76%) rename app/src/main/java/org/thoughtcrime/securesms/{groups/ui/creategroup/GroupSettingsCoordinatorLayoutBehavior.java => recipients/ui/RecipientSettingsCoordinatorLayoutBehavior.java} (82%) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientActivity.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientFragment.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientRepository.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientViewModel.java create mode 100644 app/src/main/res/drawable/ic_message_outline_ultramarine_24.xml create mode 100644 app/src/main/res/drawable/ic_message_solid_ultramarine_light_24.xml create mode 100644 app/src/main/res/drawable/ic_phone_right_outline_ultramarine_24.xml create mode 100644 app/src/main/res/drawable/ic_phone_right_solid_ultramarine_light_24.xml create mode 100644 app/src/main/res/drawable/ic_video_outline_ultramarine_24.xml create mode 100644 app/src/main/res/drawable/ic_video_solid_ultramarine_light_24.xml create mode 100644 app/src/main/res/layout/recipient_manage_activity.xml create mode 100644 app/src/main/res/layout/recipient_manage_fragment.xml create mode 100644 app/src/main/res/menu/manage_recipient_fragment.xml 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