Manage recipient activity.

This commit is contained in:
Alan Evans
2020-06-16 17:42:54 -03:00
committed by Greyson Parrelli
parent d9641128a8
commit b53827f32b
32 changed files with 1869 additions and 102 deletions

View File

@@ -272,12 +272,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
private class ProfileClickListener implements Preference.OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(Preference preference) {
Intent intent = new Intent(preference.getContext(), EditProfileActivity.class);
intent.putExtra(EditProfileActivity.EXCLUDE_SYSTEM, true);
intent.putExtra(EditProfileActivity.DISPLAY_USERNAME, true);
intent.putExtra(EditProfileActivity.NEXT_BUTTON_TEXT, R.string.save);
requireActivity().startActivity(intent);
requireActivity().startActivity(EditProfileActivity.getIntentForUserProfileEdit(preference.getContext()));
return true;
}
}

View File

@@ -73,6 +73,7 @@ import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.recipients.ui.managerecipient.ManageRecipientActivity;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.DynamicDarkToolbarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
@@ -117,6 +118,15 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
private CollapsingToolbarLayout toolbarLayout;
public static @NonNull Intent getLaunchIntent(@NonNull Context context, @NonNull RecipientId id) {
if (FeatureFlags.newGroupUI()) {
return ManageRecipientActivity.newIntent(context, id);
}
return getOldLaunchIntent(context, id);
}
@Deprecated
public static Intent getOldLaunchIntent(@NonNull Context context, @NonNull RecipientId id) {
Intent intent = new Intent(context, RecipientPreferenceActivity.class);
intent.putExtra(RecipientPreferenceActivity.RECIPIENT_ID, id);

View File

@@ -1,6 +1,8 @@
package org.thoughtcrime.securesms.color;
import android.content.Context;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -52,7 +54,7 @@ public class MaterialColors {
return null;
}
public int[] asConversationColorArray(@NonNull Context context) {
public @ColorInt int[] asConversationColorArray(@NonNull Context context) {
int[] results = new int[colors.size()];
int index = 0;

View File

@@ -2,13 +2,11 @@ package org.thoughtcrime.securesms.contacts.avatars;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.InsetDrawable;
import android.graphics.drawable.LayerDrawable;
import android.os.Build;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.drawable.DrawableCompat;
import org.thoughtcrime.securesms.R;
@@ -18,7 +16,16 @@ import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.Objects;
public final class GroupFallbackPhoto80 implements FallbackContactPhoto {
public final class FallbackPhoto80dp implements FallbackContactPhoto {
@DrawableRes private final int drawable80dp;
private final MaterialColor backgroundColor;
public FallbackPhoto80dp(@DrawableRes int drawable80dp, @NonNull MaterialColor backgroundColor) {
this.drawable80dp = drawable80dp;
this.backgroundColor = backgroundColor;
}
@Override
public Drawable asDrawable(Context context, int color) {
return buildDrawable(context);
@@ -41,12 +48,12 @@ public final class GroupFallbackPhoto80 implements FallbackContactPhoto {
private @NonNull Drawable buildDrawable(@NonNull Context context) {
Drawable background = DrawableCompat.wrap(Objects.requireNonNull(AppCompatResources.getDrawable(context, R.drawable.circle_tintable)));
Drawable foreground = AppCompatResources.getDrawable(context, R.drawable.ic_group_80);
Drawable foreground = AppCompatResources.getDrawable(context, drawable80dp);
Drawable gradient = ThemeUtil.getThemedDrawable(context, R.attr.resource_placeholder_gradient);
LayerDrawable drawable = new LayerDrawable(new Drawable[]{background, foreground, gradient});
int foregroundInset = ViewUtil.dpToPx(24);
DrawableCompat.setTint(background, MaterialColor.ULTRAMARINE.toAvatarColor(context));
DrawableCompat.setTint(background, backgroundColor.toAvatarColor(context));
drawable.setLayerInset(1, foregroundInset, foregroundInset, foregroundInset, foregroundInset);

View File

@@ -91,7 +91,7 @@ public class AddGroupDetailsFragment extends Fragment {
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
create = view.findViewById(R.id.create);
name = view.findViewById(R.id.group_name);
name = view.findViewById(R.id.name);
toolbar = view.findViewById(R.id.toolbar);
setCreateEnabled(false, false);

View File

@@ -27,10 +27,11 @@ import org.thoughtcrime.securesms.MediaPreviewActivity;
import org.thoughtcrime.securesms.MuteDialog;
import org.thoughtcrime.securesms.PushContactSelectionActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.color.MaterialColor;
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.GroupFallbackPhoto80;
import org.thoughtcrime.securesms.contacts.avatars.FallbackPhoto80dp;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.ui.GroupMemberListView;
import org.thoughtcrime.securesms.groups.ui.LeaveGroupDialog;
@@ -97,7 +98,7 @@ public class ManageGroupFragment extends Fragment {
private final Recipient.FallbackPhotoProvider fallbackPhotoProvider = new Recipient.FallbackPhotoProvider() {
@Override
public @NonNull FallbackContactPhoto getPhotoForGroup() {
return new GroupFallbackPhoto80();
return new FallbackPhoto80dp(R.drawable.ic_group_80, MaterialColor.ULTRAMARINE);
}
};
@@ -120,7 +121,7 @@ public class ManageGroupFragment extends Fragment {
avatar = view.findViewById(R.id.group_avatar);
toolbar = view.findViewById(R.id.toolbar);
groupName = view.findViewById(R.id.group_name);
groupName = view.findViewById(R.id.name);
memberCountUnderAvatar = view.findViewById(R.id.member_count);
memberCountAboveList = view.findViewById(R.id.member_count_2);
groupMemberList = view.findViewById(R.id.group_members);

View File

@@ -13,7 +13,6 @@ import androidx.navigation.Navigation;
import org.thoughtcrime.securesms.BaseActionBarActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.DynamicRegistrationTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
@@ -35,6 +34,14 @@ public class EditProfileActivity extends BaseActionBarActivity implements EditPr
return intent;
}
public static @NonNull Intent getIntentForUserProfileEdit(@NonNull Context context) {
Intent intent = new Intent(context, EditProfileActivity.class);
intent.putExtra(EditProfileActivity.EXCLUDE_SYSTEM, true);
intent.putExtra(EditProfileActivity.DISPLAY_USERNAME, true);
intent.putExtra(EditProfileActivity.NEXT_BUTTON_TEXT, R.string.save);
return intent;
}
public static @NonNull Intent getIntentForGroupProfile(@NonNull Context context, @NonNull GroupId.Push groupId) {
Intent intent = new Intent(context, EditProfileActivity.class);
intent.putExtra(EditProfileActivity.SHOW_TOOLBAR, true);

View File

@@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.groups.ui.creategroup;
package org.thoughtcrime.securesms.recipients.ui;
import android.content.Context;
import android.graphics.Rect;
@@ -19,17 +19,17 @@ import org.thoughtcrime.securesms.R;
import java.lang.ref.WeakReference;
public final class GroupSettingsCoordinatorLayoutBehavior extends CoordinatorLayout.Behavior<View> {
public final class RecipientSettingsCoordinatorLayoutBehavior extends CoordinatorLayout.Behavior<View> {
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<View> ref = new WeakReference<>(null);
private final @IdRes int idRes;
private ViewRef(@IdRes int idRes) {
private ViewReference(@IdRes int idRes) {
this.idRes = idRes;
}

View File

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

View File

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

View File

@@ -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<Long> 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<IdentityDatabase.IdentityRecord> 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<List<RecipientId>> onComplete) {
SignalExecutors.BOUNDED.execute(() -> {
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
List<GroupDatabase.GroupRecord> groupRecords = groupDatabase.getPushGroupsContainingMember(recipientId);
ArrayList<RecipientId> groupRecipients = new ArrayList<>(groupRecords.size());
for (GroupDatabase.GroupRecord groupRecord : groupRecords) {
groupRecipients.add(groupRecord.getRecipientId());
}
onComplete.accept(groupRecipients);
});
}
public void getRecipient(@NonNull Consumer<Recipient> 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<Recipient> 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();
}
}

View File

@@ -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<String> name;
private final LiveData<String> disappearingMessageTimer;
private final MutableLiveData<IdentityDatabase.IdentityRecord> identity;
private final LiveData<Recipient> recipient;
private final MutableLiveData<MediaCursor> mediaCursor = new MutableLiveData<>(null);
private final LiveData<MuteState> muteState;
private final LiveData<Boolean> hasCustomNotifications;
private final LiveData<Boolean> canCollapseMemberList;
private final DefaultValueLiveData<CollapseState> groupListCollapseState = new DefaultValueLiveData<>(CollapseState.COLLAPSED);
private final LiveData<Boolean> canBlock;
private final LiveData<List<GroupMemberEntry.FullMember>> visibleSharedGroups;
private final LiveData<String> 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<List<Recipient>> 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<String> getName() {
return name;
}
public LiveData<Recipient> getRecipient() {
return recipient;
}
LiveData<MediaCursor> getMediaCursor() {
return mediaCursor;
}
LiveData<MuteState> getMuteState() {
return muteState;
}
LiveData<String> getDisappearingMessageTimer() {
return disappearingMessageTimer;
}
LiveData<Boolean> hasCustomNotifications() {
return hasCustomNotifications;
}
LiveData<Boolean> getCanCollapseMemberList() {
return canCollapseMemberList;
}
LiveData<Boolean> 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<Recipient> mainThreadRecipientCallback) {
manageRecipientRepository.getRecipient(recipient -> Util.runOnMain(() -> mainThreadRecipientCallback.accept(recipient)));
}
private static @NonNull List<Recipient> filterSharedGroupList(@NonNull List<Recipient> groups,
@NonNull CollapseState collapseState)
{
if (collapseState == CollapseState.COLLAPSED && groups.size() > MAX_COLLAPSED_GROUPS) {
return groups.subList(0, MAX_COLLAPSED_GROUPS);
} else {
return groups;
}
}
LiveData<IdentityDatabase.IdentityRecord> 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<List<GroupMemberEntry.FullMember>> getVisibleSharedGroups() {
return visibleSharedGroups;
}
LiveData<String> 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 extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection unchecked
return (T) new ManageRecipientViewModel(context, new ManageRecipientRepository(context, recipientId));
}
}
}

View File

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

View File

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

View File

@@ -22,14 +22,20 @@ public final class CustomNotificationsViewModel extends ViewModel {
private final LiveData<Uri> notificationSound;
private final CustomNotificationsRepository repository;
private final MutableLiveData<Boolean> isInitialLoadComplete = new MutableLiveData<>();
private final LiveData<Boolean> showCallingOptions;
private final LiveData<Uri> ringtone;
private final LiveData<RecipientDatabase.VibrateState> isCallingVibrateEnabled;
private CustomNotificationsViewModel(@NonNull RecipientId recipientId, @NonNull CustomNotificationsRepository repository) {
LiveData<Recipient> 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<Boolean> getShowCallingOptions() {
return showCallingOptions;
}
public LiveData<Uri> getRingtone() {
return ringtone;
}
public LiveData<RecipientDatabase.VibrateState> 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;