diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 109236ef94..65680bacc9 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -443,7 +443,7 @@
android:theme="@style/TextSecure.FullScreenMedia"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
-
diff --git a/app/src/main/java/org/thoughtcrime/securesms/BlockedContactsActivity.java b/app/src/main/java/org/thoughtcrime/securesms/BlockedContactsActivity.java
deleted file mode 100644
index 9468f208fa..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/BlockedContactsActivity.java
+++ /dev/null
@@ -1,139 +0,0 @@
-package org.thoughtcrime.securesms;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.AdapterView;
-import android.widget.ListView;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.cursoradapter.widget.CursorAdapter;
-import androidx.fragment.app.ListFragment;
-import androidx.loader.app.LoaderManager;
-import androidx.loader.content.Loader;
-
-import org.thoughtcrime.securesms.database.RecipientDatabase;
-import org.thoughtcrime.securesms.database.loaders.BlockedContactsLoader;
-import org.thoughtcrime.securesms.mms.GlideApp;
-import org.thoughtcrime.securesms.mms.GlideRequests;
-import org.thoughtcrime.securesms.preferences.BlockedContactListItem;
-import org.thoughtcrime.securesms.recipients.LiveRecipient;
-import org.thoughtcrime.securesms.recipients.Recipient;
-import org.thoughtcrime.securesms.recipients.RecipientId;
-import org.thoughtcrime.securesms.recipients.RecipientUtil;
-import org.thoughtcrime.securesms.util.DynamicTheme;
-
-public class BlockedContactsActivity extends PassphraseRequiredActivity {
-
- private final DynamicTheme dynamicTheme = new DynamicTheme();
-
- @Override
- public void onPreCreate() {
- dynamicTheme.onCreate(this);
- }
-
- @Override
- public void onCreate(Bundle bundle, boolean ready) {
- getSupportActionBar().setDisplayHomeAsUpEnabled(true);
- getSupportActionBar().setTitle(R.string.BlockedContactsActivity_blocked_contacts);
- initFragment(android.R.id.content, new BlockedContactsFragment());
- }
-
- @Override
- public void onResume() {
- super.onResume();
- dynamicTheme.onResume(this);
- }
-
- @Override
- public boolean onSupportNavigateUp() {
- onBackPressed();
- return true;
- }
-
- public static class BlockedContactsFragment
- extends ListFragment
- implements LoaderManager.LoaderCallbacks, ListView.OnItemClickListener
- {
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
- return inflater.inflate(R.layout.blocked_contacts_fragment, container, false);
- }
-
- @Override
- public void onCreate(Bundle bundle) {
- super.onCreate(bundle);
- setListAdapter(new BlockedContactAdapter(requireActivity(), GlideApp.with(this), null));
- LoaderManager.getInstance(this).initLoader(0, null, this);
- }
-
- @Override
- public void onStart() {
- super.onStart();
- LoaderManager.getInstance(this).restartLoader(0, null, this);
- }
-
- @Override
- public void onActivityCreated(Bundle bundle) {
- super.onActivityCreated(bundle);
- getListView().setOnItemClickListener(this);
- }
-
- @Override
- public @NonNull Loader onCreateLoader(int id, Bundle args) {
- return new BlockedContactsLoader(getActivity());
- }
-
- @Override
- public void onLoadFinished(@NonNull Loader loader, Cursor data) {
- if (getListAdapter() != null) {
- ((CursorAdapter) getListAdapter()).changeCursor(data);
- }
- }
-
- @Override
- public void onLoaderReset(@NonNull Loader loader) {
- if (getListAdapter() != null) {
- ((CursorAdapter) getListAdapter()).changeCursor(null);
- }
- }
-
- @Override
- public void onItemClick(AdapterView> parent, View view, int position, long id) {
- Recipient recipient = ((BlockedContactListItem)view).getRecipient();
- BlockUnblockDialog.showUnblockFor(requireContext(), getLifecycle(), recipient, () -> {
- RecipientUtil.unblock(requireContext(), recipient);
- LoaderManager.getInstance(this).restartLoader(0, null, this);
- });
- }
-
- private static class BlockedContactAdapter extends CursorAdapter {
-
- private final GlideRequests glideRequests;
-
- BlockedContactAdapter(@NonNull Context context, @NonNull GlideRequests glideRequests, @Nullable Cursor c) {
- super(context, c);
- this.glideRequests = glideRequests;
- }
-
- @Override
- public View newView(Context context, Cursor cursor, ViewGroup parent) {
- return LayoutInflater.from(context)
- .inflate(R.layout.blocked_contact_list_item, parent, false);
- }
-
- @Override
- public void bindView(View view, Context context, Cursor cursor) {
- RecipientId recipientId = RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(RecipientDatabase.ID)));
- LiveRecipient recipient = Recipient.live(recipientId);
-
- ((BlockedContactListItem) view).set(glideRequests, recipient);
- }
- }
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/blocked/BlockedUsersActivity.java b/app/src/main/java/org/thoughtcrime/securesms/blocked/BlockedUsersActivity.java
new file mode 100644
index 0000000000..5d80d79c3e
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/blocked/BlockedUsersActivity.java
@@ -0,0 +1,167 @@
+package org.thoughtcrime.securesms.blocked;
+
+import android.app.AlertDialog;
+import android.content.Intent;
+import android.graphics.Color;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.ViewSwitcher;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.StringRes;
+import androidx.appcompat.widget.Toolbar;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.ViewModelProviders;
+
+import com.google.android.material.snackbar.Snackbar;
+
+import org.thoughtcrime.securesms.ContactSelectionListFragment;
+import org.thoughtcrime.securesms.PassphraseRequiredActivity;
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.components.ContactFilterToolbar;
+import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
+import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.recipients.RecipientId;
+import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
+import org.thoughtcrime.securesms.util.DynamicTheme;
+import org.whispersystems.libsignal.util.guava.Optional;
+
+public class BlockedUsersActivity extends PassphraseRequiredActivity implements BlockedUsersFragment.Listener, ContactSelectionListFragment.OnContactSelectedListener {
+
+ private static final String CONTACT_SELECTION_FRAGMENT = "Contact.Selection.Fragment";
+
+ private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
+
+ private BlockedUsersViewModel viewModel;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState, boolean ready) {
+ super.onCreate(savedInstanceState, ready);
+
+ dynamicTheme.onCreate(this);
+
+ setContentView(R.layout.blocked_users_activity);
+
+ BlockedUsersRepository repository = new BlockedUsersRepository(this);
+ BlockedUsersViewModel.Factory factory = new BlockedUsersViewModel.Factory(repository);
+
+ viewModel = ViewModelProviders.of(this, factory).get(BlockedUsersViewModel.class);
+
+ ViewSwitcher viewSwitcher = findViewById(R.id.toolbar_switcher);
+ Toolbar toolbar = findViewById(R.id.toolbar);
+ ContactFilterToolbar contactFilterToolbar = findViewById(R.id.filter_toolbar);
+ View container = findViewById(R.id.fragment_container);
+
+ toolbar.setNavigationOnClickListener(unused -> onBackPressed());
+ contactFilterToolbar.setNavigationOnClickListener(unused -> onBackPressed());
+ contactFilterToolbar.setOnFilterChangedListener(query -> {
+ Fragment fragment = getSupportFragmentManager().findFragmentByTag(CONTACT_SELECTION_FRAGMENT);
+ if (fragment != null) {
+ ((ContactSelectionListFragment) fragment).setQueryFilter(query);
+ }
+ });
+ contactFilterToolbar.setHint(R.string.BlockedUsersActivity__add_blocked_user);
+
+ //noinspection CodeBlock2Expr
+ getSupportFragmentManager().addOnBackStackChangedListener(() -> {
+ viewSwitcher.setDisplayedChild(getSupportFragmentManager().getBackStackEntryCount());
+ });
+
+ getSupportFragmentManager().beginTransaction()
+ .add(R.id.fragment_container, new BlockedUsersFragment())
+ .commit();
+
+ viewModel.getEvents().observe(this, event -> handleEvent(container, event));
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ dynamicTheme.onResume(this);
+ }
+
+ @Override
+ public boolean onBeforeContactSelected(Optional recipientId, String number) {
+ final String displayName = recipientId.transform(id -> Recipient.resolved(id).getDisplayName(this)).or(number);
+
+ AlertDialog confirmationDialog = new AlertDialog.Builder(BlockedUsersActivity.this)
+ .setTitle(R.string.BlockedUsersActivity__block_user)
+ .setMessage(getString(R.string.BlockedUserActivity__s_will_not_be_able_to, displayName))
+ .setPositiveButton(R.string.BlockedUsersActivity__block, (dialog, which) -> {
+ if (recipientId.isPresent()) {
+ viewModel.block(recipientId.get());
+ } else {
+ viewModel.createAndBlock(number);
+ }
+ dialog.dismiss();
+ onBackPressed();
+ })
+ .setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.dismiss())
+ .setCancelable(true)
+ .create();
+
+ confirmationDialog.setOnShowListener(dialog -> {
+ confirmationDialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(Color.RED);
+ });
+
+ confirmationDialog.show();
+
+ return false;
+ }
+
+ @Override
+ public void onContactDeselected(Optional recipientId, String number) {
+
+ }
+
+ @Override
+ public void handleAddUserToBlockedList() {
+ ContactSelectionListFragment fragment = new ContactSelectionListFragment();
+ Intent intent = getIntent();
+
+ fragment.setOnContactSelectedListener(this);
+
+ intent.putExtra(ContactSelectionListFragment.REFRESHABLE, false);
+ intent.putExtra(ContactSelectionListFragment.SELECTION_LIMITS, 1);
+ intent.putExtra(ContactSelectionListFragment.HIDE_COUNT, true);
+ intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE,
+ ContactsCursorLoader.DisplayMode.FLAG_PUSH |
+ ContactsCursorLoader.DisplayMode.FLAG_SMS |
+ ContactsCursorLoader.DisplayMode.FLAG_ACTIVE_GROUPS |
+ ContactsCursorLoader.DisplayMode.FLAG_INACTIVE_GROUPS |
+ ContactsCursorLoader.DisplayMode.FLAG_BLOCK);
+
+ getSupportFragmentManager().beginTransaction()
+ .replace(R.id.fragment_container, fragment, CONTACT_SELECTION_FRAGMENT)
+ .addToBackStack(null)
+ .commit();
+ }
+
+ private void handleEvent(@NonNull View view, @NonNull BlockedUsersViewModel.Event event) {
+ final String displayName;
+
+ if (event.getRecipient() == null) {
+ displayName = event.getNumber();
+ } else {
+ displayName = event.getRecipient().getDisplayName(this);
+ }
+
+ final @StringRes int messageResId;
+ switch (event.getEventType()) {
+ case BLOCK_SUCCEEDED:
+ messageResId = R.string.BlockedUsersActivity__s_has_been_blocked;
+ break;
+ case BLOCK_FAILED:
+ messageResId = R.string.BlockedUsersActivity__failed_to_block_s;
+ break;
+ case UNBLOCK_SUCCEEDED:
+ messageResId = R.string.BlockedUsersActivity__s_has_been_unblocked;
+ break;
+ default:
+ throw new IllegalArgumentException("Unsupported event type " + event);
+ }
+
+ Snackbar.make(view, getString(messageResId, displayName), Snackbar.LENGTH_SHORT).show();
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/blocked/BlockedUsersAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/blocked/BlockedUsersAdapter.java
new file mode 100644
index 0000000000..f5a2dd091f
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/blocked/BlockedUsersAdapter.java
@@ -0,0 +1,96 @@
+package org.thoughtcrime.securesms.blocked;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.core.util.Consumer;
+import androidx.recyclerview.widget.DiffUtil;
+import androidx.recyclerview.widget.ListAdapter;
+import androidx.recyclerview.widget.RecyclerView;
+
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.components.AvatarImageView;
+import org.thoughtcrime.securesms.recipients.Recipient;
+
+import java.util.Objects;
+
+final class BlockedUsersAdapter extends ListAdapter {
+
+ private final RecipientClickedListener recipientClickedListener;
+
+ BlockedUsersAdapter(@NonNull RecipientClickedListener recipientClickedListener) {
+ super(new RecipientDiffCallback());
+
+ this.recipientClickedListener = recipientClickedListener;
+ }
+
+ @Override
+ public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.blocked_users_adapter_item, parent, false),
+ position -> recipientClickedListener.onRecipientClicked(Objects.requireNonNull(getItem(position))));
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
+ holder.bind(Objects.requireNonNull(getItem(position)));
+ }
+
+ static final class ViewHolder extends RecyclerView.ViewHolder {
+
+ private final AvatarImageView avatar;
+ private final TextView displayName;
+ private final TextView numberOrUsername;
+
+ public ViewHolder(@NonNull View itemView, Consumer clickConsumer) {
+ super(itemView);
+
+ this.avatar = itemView.findViewById(R.id.avatar);
+ this.displayName = itemView.findViewById(R.id.display_name);
+ this.numberOrUsername = itemView.findViewById(R.id.number_or_username);
+
+ itemView.setOnClickListener(unused -> {
+ if (getAdapterPosition() != RecyclerView.NO_POSITION) {
+ clickConsumer.accept(getAdapterPosition());
+ }
+ });
+ }
+
+ public void bind(@NonNull Recipient recipient) {
+ avatar.setAvatar(recipient);
+ displayName.setText(recipient.getDisplayName(itemView.getContext()));
+
+ if (recipient.hasAUserSetDisplayName(itemView.getContext())) {
+ String identifier = recipient.getE164().or(recipient.getUsername()).orNull();
+
+ if (identifier != null) {
+ numberOrUsername.setText(identifier);
+ numberOrUsername.setVisibility(View.VISIBLE);
+ } else {
+ numberOrUsername.setVisibility(View.GONE);
+ }
+ } else {
+ numberOrUsername.setVisibility(View.GONE);
+ }
+ }
+ }
+
+ private static final class RecipientDiffCallback extends DiffUtil.ItemCallback {
+
+ @Override
+ public boolean areItemsTheSame(@NonNull Recipient oldItem, @NonNull Recipient newItem) {
+ return oldItem.equals(newItem);
+ }
+
+ @Override
+ public boolean areContentsTheSame(@NonNull Recipient oldItem, @NonNull Recipient newItem) {
+ return oldItem.equals(newItem);
+ }
+ }
+
+ interface RecipientClickedListener {
+ void onRecipientClicked(@NonNull Recipient recipient);
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/blocked/BlockedUsersFragment.java b/app/src/main/java/org/thoughtcrime/securesms/blocked/BlockedUsersFragment.java
new file mode 100644
index 0000000000..eb7f7e7c22
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/blocked/BlockedUsersFragment.java
@@ -0,0 +1,100 @@
+package org.thoughtcrime.securesms.blocked;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.graphics.Color;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.ViewModelProviders;
+import androidx.recyclerview.widget.RecyclerView;
+
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.recipients.Recipient;
+
+public class BlockedUsersFragment extends Fragment {
+
+ private BlockedUsersViewModel viewModel;
+ private Listener listener;
+
+ @Override
+ public void onAttach(@NonNull Context context) {
+ super.onAttach(context);
+
+ if (context instanceof Listener) {
+ listener = (Listener) context;
+ } else {
+ throw new ClassCastException("Expected context to implement Listener");
+ }
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+
+ listener = null;
+ }
+
+ @Override
+ public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.blocked_users_fragment, container, false);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ View addUser = view.findViewById(R.id.add_blocked_user_touch_target);
+ RecyclerView recycler = view.findViewById(R.id.blocked_users_recycler);
+ View empty = view.findViewById(R.id.no_blocked_users);
+ BlockedUsersAdapter adapter = new BlockedUsersAdapter(this::handleRecipientClicked);
+
+ recycler.setAdapter(adapter);
+
+ addUser.setOnClickListener(unused -> {
+ if (listener != null) {
+ listener.handleAddUserToBlockedList();
+ }
+ });
+
+ viewModel = ViewModelProviders.of(requireActivity()).get(BlockedUsersViewModel.class);
+ viewModel.getRecipients().observe(getViewLifecycleOwner(), list -> {
+ if (list.isEmpty()) {
+ empty.setVisibility(View.VISIBLE);
+ } else {
+ empty.setVisibility(View.GONE);
+ }
+
+ adapter.submitList(list);
+ });
+ }
+
+ private void handleRecipientClicked(@NonNull Recipient recipient) {
+ AlertDialog confirmationDialog = new AlertDialog.Builder(requireContext())
+ .setTitle(R.string.BlockedUsersActivity__unblock_user)
+ .setMessage(getString(R.string.BlockedUsersActivity__do_you_want_to_unblock_s, recipient.getDisplayName(requireContext())))
+ .setPositiveButton(R.string.BlockedUsersActivity__unblock, (dialog, which) -> {
+ viewModel.unblock(recipient.getId());
+ dialog.dismiss();
+ })
+ .setNegativeButton(android.R.string.cancel, (dialog, which) -> {
+ dialog.dismiss();
+ })
+ .setCancelable(true)
+ .create();
+
+ confirmationDialog.setOnShowListener(dialog -> {
+ confirmationDialog.getButton(DialogInterface.BUTTON_POSITIVE).setTextColor(Color.RED);
+ });
+
+ confirmationDialog.show();
+ }
+
+ interface Listener {
+ void handleAddUserToBlockedList();
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/blocked/BlockedUsersRepository.java b/app/src/main/java/org/thoughtcrime/securesms/blocked/BlockedUsersRepository.java
new file mode 100644
index 0000000000..4c387a94f3
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/blocked/BlockedUsersRepository.java
@@ -0,0 +1,76 @@
+package org.thoughtcrime.securesms.blocked;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.core.util.Consumer;
+
+import org.thoughtcrime.securesms.database.DatabaseFactory;
+import org.thoughtcrime.securesms.database.RecipientDatabase;
+import org.thoughtcrime.securesms.groups.GroupChangeBusyException;
+import org.thoughtcrime.securesms.groups.GroupChangeFailedException;
+import org.thoughtcrime.securesms.logging.Log;
+import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.recipients.RecipientId;
+import org.thoughtcrime.securesms.recipients.RecipientUtil;
+import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+class BlockedUsersRepository {
+
+ private static final String TAG = Log.tag(BlockedUsersRepository.class);
+
+ private final Context context;
+
+ BlockedUsersRepository(@NonNull Context context) {
+ this.context = context;
+ }
+
+ void getBlocked(@NonNull Consumer> blockedUsers) {
+ SignalExecutors.BOUNDED.execute(() -> {
+ RecipientDatabase db = DatabaseFactory.getRecipientDatabase(context);
+ try (RecipientDatabase.RecipientReader reader = db.readerForBlocked(db.getBlocked())) {
+ int count = reader.getCount();
+ if (count == 0) {
+ blockedUsers.accept(Collections.emptyList());
+ } else {
+ List recipients = new ArrayList<>();
+ while (reader.getNext() != null) {
+ recipients.add(reader.getCurrent());
+ }
+ blockedUsers.accept(recipients);
+ }
+ }
+ });
+ }
+
+ void block(@NonNull RecipientId recipientId, @NonNull Runnable success, @NonNull Runnable failure) {
+ SignalExecutors.BOUNDED.execute(() -> {
+ try {
+ RecipientUtil.block(context, Recipient.resolved(recipientId));
+ success.run();
+ } catch (IOException | GroupChangeFailedException | GroupChangeBusyException e) {
+ Log.w(TAG, "block: failed to block recipient: ", e);
+ failure.run();
+ }
+ });
+ }
+
+ void createAndBlock(@NonNull String number, @NonNull Runnable success) {
+ SignalExecutors.BOUNDED.execute(() -> {
+ RecipientUtil.blockNonGroup(context, Recipient.external(context, number));
+ success.run();
+ });
+ }
+
+ void unblock(@NonNull RecipientId recipientId, @NonNull Runnable success) {
+ SignalExecutors.BOUNDED.execute(() -> {
+ RecipientUtil.unblock(context, Recipient.resolved(recipientId));
+ success.run();
+ });
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/blocked/BlockedUsersViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/blocked/BlockedUsersViewModel.java
new file mode 100644
index 0000000000..f2d6ef0e2f
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/blocked/BlockedUsersViewModel.java
@@ -0,0 +1,115 @@
+package org.thoughtcrime.securesms.blocked;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModel;
+import androidx.lifecycle.ViewModelProvider;
+
+import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.recipients.RecipientId;
+import org.thoughtcrime.securesms.util.SingleLiveEvent;
+
+import java.util.List;
+import java.util.Objects;
+
+public class BlockedUsersViewModel extends ViewModel {
+
+ private final BlockedUsersRepository repository;
+ private final MutableLiveData> recipients;
+ private final SingleLiveEvent events = new SingleLiveEvent<>();
+
+ private BlockedUsersViewModel(@NonNull BlockedUsersRepository repository) {
+ this.repository = repository;
+ this.recipients = new MutableLiveData<>();
+
+ loadRecipients();
+ }
+
+ public LiveData> getRecipients() {
+ return recipients;
+ }
+
+ public LiveData getEvents() {
+ return events;
+ }
+
+ void block(@NonNull RecipientId recipientId) {
+ repository.block(recipientId,
+ () -> {
+ loadRecipients();
+ events.postValue(new Event(EventType.BLOCK_SUCCEEDED, Recipient.resolved(recipientId)));
+ },
+ () -> events.postValue(new Event(EventType.BLOCK_FAILED, Recipient.resolved(recipientId))));
+ }
+
+ void createAndBlock(@NonNull String number) {
+ repository.createAndBlock(number, () -> {
+ loadRecipients();
+ events.postValue(new Event(EventType.BLOCK_SUCCEEDED, number));
+ });
+ }
+
+ void unblock(@NonNull RecipientId recipientId) {
+ repository.unblock(recipientId, () -> {
+ loadRecipients();
+ events.postValue(new Event(EventType.UNBLOCK_SUCCEEDED, Recipient.resolved(recipientId)));
+ });
+ }
+
+ private void loadRecipients() {
+ repository.getBlocked(recipients::postValue);
+ }
+
+ enum EventType {
+ BLOCK_SUCCEEDED,
+ BLOCK_FAILED,
+ UNBLOCK_SUCCEEDED
+ }
+
+ public static final class Event {
+
+ private final EventType eventType;
+ private final Recipient recipient;
+ private final String number;
+
+ private Event(@NonNull EventType eventType, @NonNull Recipient recipient) {
+ this.eventType = eventType;
+ this.recipient = recipient;
+ this.number = null;
+ }
+
+ private Event(@NonNull EventType eventType, @NonNull String number) {
+ this.eventType = eventType;
+ this.recipient = null;
+ this.number = number;
+ }
+
+ public @Nullable Recipient getRecipient() {
+ return recipient;
+ }
+
+ public @Nullable String getNumber() {
+ return number;
+ }
+
+ public @NonNull EventType getEventType() {
+ return eventType;
+ }
+ }
+
+ public static class Factory implements ViewModelProvider.Factory {
+
+ private final BlockedUsersRepository repository;
+
+ public Factory(@NonNull BlockedUsersRepository repository) {
+ this.repository = repository;
+ }
+
+ @Override
+ public @NonNull T create(@NonNull Class modelClass) {
+ return Objects.requireNonNull(modelClass.cast(new BlockedUsersViewModel(repository)));
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactsCursorLoader.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactsCursorLoader.java
index 7b4ed13f12..294aa1a3a6 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactsCursorLoader.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactsCursorLoader.java
@@ -60,6 +60,7 @@ public class ContactsCursorLoader extends CursorLoader {
public static final int FLAG_ACTIVE_GROUPS = 1 << 2;
public static final int FLAG_INACTIVE_GROUPS = 1 << 3;
public static final int FLAG_SELF = 1 << 4;
+ public static final int FLAG_BLOCK = 1 << 5;
public static final int FLAG_ALL = FLAG_PUSH | FLAG_SMS | FLAG_ACTIVE_GROUPS | FLAG_INACTIVE_GROUPS | FLAG_SELF;
}
@@ -342,8 +343,13 @@ public class ContactsCursorLoader extends CursorLoader {
}
private String getUnknownContactTitle() {
- return getContext().getString(newConversation(mode) ? R.string.contact_selection_list__unknown_contact
- : R.string.contact_selection_list__unknown_contact_add_to_group);
+ if (blockUser(mode)) {
+ return getContext().getString(R.string.contact_selection_list__unknown_contact_block);
+ } else if (newConversation(mode)) {
+ return getContext().getString(R.string.contact_selection_list__unknown_contact);
+ } else {
+ return getContext().getString(R.string.contact_selection_list__unknown_contact_add_to_group);
+ }
}
private @NonNull Cursor filterNonPushContacts(@NonNull Cursor cursor) {
@@ -382,6 +388,10 @@ public class ContactsCursorLoader extends CursorLoader {
return flagSet(mode, DisplayMode.FLAG_SELF);
}
+ private static boolean blockUser(int mode) {
+ return flagSet(mode, DisplayMode.FLAG_BLOCK);
+ }
+
private static boolean newConversation(int mode) {
return groupsEnabled(mode);
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java
index 15cd5ec055..0b5bb0e414 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java
@@ -3018,6 +3018,11 @@ public class RecipientDatabase extends Database {
return getCurrent();
}
+ public int getCount() {
+ if (cursor != null) return cursor.getCount();
+ else return 0;
+ }
+
public void close() {
cursor.close();
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/loaders/BlockedContactsLoader.java b/app/src/main/java/org/thoughtcrime/securesms/database/loaders/BlockedContactsLoader.java
deleted file mode 100644
index 0707a6d5f1..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/database/loaders/BlockedContactsLoader.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package org.thoughtcrime.securesms.database.loaders;
-
-import android.content.Context;
-import android.database.Cursor;
-
-import org.thoughtcrime.securesms.database.DatabaseFactory;
-import org.thoughtcrime.securesms.util.AbstractCursorLoader;
-
-public class BlockedContactsLoader extends AbstractCursorLoader {
-
- public BlockedContactsLoader(Context context) {
- super(context);
- }
-
- @Override
- public Cursor getCursor() {
- return DatabaseFactory.getRecipientDatabase(getContext()).getBlocked();
- }
-
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java
index 7dd5a0f7a3..1029bedda1 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java
@@ -30,9 +30,9 @@ import com.google.android.material.snackbar.Snackbar;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
-import org.thoughtcrime.securesms.BlockedContactsActivity;
import org.thoughtcrime.securesms.PassphraseChangeActivity;
import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.blocked.BlockedUsersActivity;
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat;
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher;
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
@@ -280,7 +280,7 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
private class BlockedContactsClickListener implements Preference.OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(Preference preference) {
- Intent intent = new Intent(getActivity(), BlockedContactsActivity.class);
+ Intent intent = new Intent(getActivity(), BlockedUsersActivity.class);
startActivity(intent);
return true;
}
diff --git a/app/src/main/res/layout/blocked_contact_list_item.xml b/app/src/main/res/layout/blocked_contact_list_item.xml
deleted file mode 100644
index 2a83568a18..0000000000
--- a/app/src/main/res/layout/blocked_contact_list_item.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/blocked_contacts_fragment.xml b/app/src/main/res/layout/blocked_contacts_fragment.xml
deleted file mode 100644
index 49ccc82aa6..0000000000
--- a/app/src/main/res/layout/blocked_contacts_fragment.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/blocked_users_activity.xml b/app/src/main/res/layout/blocked_users_activity.xml
new file mode 100644
index 0000000000..6b4923ebaa
--- /dev/null
+++ b/app/src/main/res/layout/blocked_users_activity.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/blocked_users_adapter_item.xml b/app/src/main/res/layout/blocked_users_adapter_item.xml
new file mode 100644
index 0000000000..bf842af77d
--- /dev/null
+++ b/app/src/main/res/layout/blocked_users_adapter_item.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/blocked_users_fragment.xml b/app/src/main/res/layout/blocked_users_fragment.xml
new file mode 100644
index 0000000000..bd959d5856
--- /dev/null
+++ b/app/src/main/res/layout/blocked_users_fragment.xml
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 02513b0141..c23b1feed2 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -97,8 +97,18 @@
Error playing audio!
-
- Blocked contacts
+
+ Blocked users
+ Add blocked user
+ Blocked users will not be able to call you or send you messages.
+ No blocked users
+ Block user?
+ \"%1$s\" will not be able to call you or send you messages.
+ Block
+ Unblock user?
+ Do you want to unblock \"%1$s\"?
+ Unblock
+
Block and leave %1$s?
@@ -2253,7 +2263,7 @@
Typing indicators
If typing indicators are disabled, you won\'t be able to see typing indicators from others.
Request keyboard to disable personalized learning
- Blocked contacts
+ Blocked users
When using mobile data
When using Wi-Fi
When roaming
@@ -2351,6 +2361,7 @@
New message to…
+ Block user
Add to group
@@ -2824,6 +2835,9 @@
%1$s/%2$s
+ \"%1$s\" has been blocked.
+ Failed to block \"%1$s\"
+ \"%1$s\" has been unblocked.
Review Members
diff --git a/app/src/main/res/xml/preferences_app_protection.xml b/app/src/main/res/xml/preferences_app_protection.xml
index 730c14d931..545cc630d5 100644
--- a/app/src/main/res/xml/preferences_app_protection.xml
+++ b/app/src/main/res/xml/preferences_app_protection.xml
@@ -95,7 +95,7 @@
+ android:title="@string/preferences_app_protection__blocked_users" />