Implement adding/removing contacts + detail contact view

This commit is contained in:
Paul Schaub 2020-06-04 23:49:03 +02:00
parent 8fa067acfb
commit 71e16d0fef
Signed by: vanitasvitae
GPG Key ID: 62BEE9264BF17311
29 changed files with 884 additions and 37 deletions

View File

@ -35,6 +35,8 @@
<activity
android:name=".ui.account.LoginActivity"
android:label="@string/title_activity_login" />
<activity android:name=".ui.roster.contacts.detail.ContactDetailActivity" />
<service android:name=".service.MercuryConnectionService" />
</application>

View File

@ -16,6 +16,8 @@ import org.mercury_im.messenger.ui.account.AccountsViewModel;
import org.mercury_im.messenger.ui.account.LoginActivity;
import org.mercury_im.messenger.ui.account.LoginViewModel;
import org.mercury_im.messenger.ui.roster.contacts.ContactListViewModel;
import org.mercury_im.messenger.ui.roster.contacts.detail.ContactDetailActivity;
import org.mercury_im.messenger.ui.roster.contacts.detail.ContactDetailViewModel;
import javax.inject.Singleton;
@ -49,7 +51,8 @@ public interface AppComponent {
void inject(ChatInputFragment chatInputFragment);
void inject(ChatListViewModel chatListViewModel);
void inject(ContactDetailActivity contactDetailActivity);
// ViewModels
@ -64,6 +67,10 @@ public interface AppComponent {
void inject(AccountsViewModel accountsViewModel);
void inject(ChatListViewModel chatListViewModel);
void inject(ContactDetailViewModel contactDetailViewModel);
// Services

View File

@ -18,6 +18,7 @@ import org.jxmpp.jid.EntityBareJid;
import org.jxmpp.jid.impl.JidCreate;
import org.mercury_im.messenger.MercuryImApplication;
import org.mercury_im.messenger.R;
import org.mercury_im.messenger.entity.contact.Peer;
import java.util.UUID;
@ -114,13 +115,15 @@ public class ChatActivity extends AppCompatActivity
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_debug:
Log.d("CHATACTIVITY", "Fetch MAM messages!");
chatViewModel.requestMamMessages()
.subscribeOn(Schedulers.io())
.subscribe();
Peer peer = chatViewModel.getContact().getValue();
Toast.makeText(this, "subscription: " + peer.getSubscriptionDirection().toString() +
" isApproved: " + peer.isSubscriptionApproved() + " isPending: " + peer.isSubscriptionPending(), Toast.LENGTH_SHORT).show();
break;
// menu_chat
case R.id.action_delete_contact:
chatViewModel.deleteContact();
break;
case R.id.action_call:
case R.id.action_clear_history:
case R.id.action_notification_settings:

View File

@ -6,6 +6,7 @@ import androidx.lifecycle.ViewModel;
import org.jxmpp.jid.EntityBareJid;
import org.mercury_im.messenger.MercuryImApplication;
import org.mercury_im.messenger.Messenger;
import org.mercury_im.messenger.data.repository.DirectChatRepository;
import org.mercury_im.messenger.data.repository.MessageRepository;
import org.mercury_im.messenger.data.repository.PeerRepository;
@ -24,7 +25,6 @@ import io.reactivex.Completable;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.functions.Consumer;
import io.reactivex.schedulers.Schedulers;
public class ChatViewModel extends ViewModel {
@ -41,6 +41,9 @@ public class ChatViewModel extends ViewModel {
@Inject
MessageRepository messageRepository;
@Inject
Messenger messenger;
private MutableLiveData<Peer> contact = new MutableLiveData<>();
private MutableLiveData<List<Message>> messages = new MutableLiveData<>();
private MutableLiveData<String> contactDisplayName = new MutableLiveData<>();
@ -60,10 +63,15 @@ public class ChatViewModel extends ViewModel {
public void init(DirectChat chat) {
this.chat.setValue(chat);
this.contact.setValue(chat.getPeer());
// Subscribe peer
disposable.add(contactRepository.observePeer(chat.getPeer().getId())
.subscribe(peer -> contactDisplayName.setValue(peer.getItem().getName()),
.subscribe(peer -> {
if (peer.isPresent()) {
contactDisplayName.setValue(peer.getItem().getName());
}
},
error -> LOGGER.log(Level.SEVERE, "Error subscribing display name to peer", error)));
// Subscribe messages
@ -112,4 +120,14 @@ public class ChatViewModel extends ViewModel {
*/
return null;
}
public void deleteContact() {
Peer contact = getContact().getValue();
disposable.add(messenger.deleteContact(contact)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(() -> LOGGER.log(Level.INFO, "Contact deleted."), e -> {
LOGGER.log(Level.SEVERE, e.getMessage(), e);
}));
}
}

View File

@ -15,10 +15,12 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.view.ActionMode;
import androidx.recyclerview.widget.RecyclerView;
import org.jivesoftware.smack.util.Objects;
import org.mercury_im.messenger.R;
import org.mercury_im.messenger.entity.chat.DirectChat;
import org.mercury_im.messenger.ui.avatar.AvatarDrawable;
import org.mercury_im.messenger.ui.chat.ChatActivity;
import org.mercury_im.messenger.ui.roster.contacts.detail.ContactDetailActivity;
import org.mercury_im.messenger.ui.util.AbstractRecyclerViewAdapter;
import org.mercury_im.messenger.util.ColorUtil;
@ -102,7 +104,7 @@ public class ChatListRecyclerViewAdapter
@Override
public boolean areContentsTheSame(DirectChat oldItem, DirectChat newItem) {
return oldItem.getPeer().getName().equals(newItem.getPeer().getName());
return Objects.equals(oldItem.getPeer().getName(), newItem.getPeer().getName());
}
}

View File

@ -2,41 +2,90 @@ package org.mercury_im.messenger.ui.roster.contacts;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.database.DataSetObserver;
import android.opengl.Visibility;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.Spinner;
import android.widget.SpinnerAdapter;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatDialogFragment;
import androidx.lifecycle.LiveData;
import com.google.android.material.textfield.TextInputEditText;
import com.google.android.material.textfield.TextInputLayout;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.util.Async;
import org.jxmpp.jid.impl.JidCreate;
import org.jxmpp.stringprep.XmppStringprepException;
import org.mercury_im.messenger.Messenger;
import org.mercury_im.messenger.R;
import org.mercury_im.messenger.entity.Account;
import org.mercury_im.messenger.exception.ConnectionNotFoundException;
import org.mercury_im.messenger.exception.ContactAlreadyAddedException;
import java.util.List;
import butterknife.BindView;
import butterknife.ButterKnife;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
public class AddContactDialogFragment extends AppCompatDialogFragment {
private final LiveData<List<Account>> accounts;
private final List<Account> accounts;
private final Messenger messenger;
public AddContactDialogFragment(LiveData<List<Account>> accountList) {
@BindView(R.id.account_select_container)
LinearLayout accountSelectorContainer;
@BindView(R.id.spinner)
Spinner accountSelector;
@BindView(R.id.address_layout)
TextInputLayout contactAddressLayout;
@BindView(R.id.address)
TextInputEditText contactAddress;
private final CompositeDisposable disposable = new CompositeDisposable();
public AddContactDialogFragment(List<Account> accountList, Messenger messenger) {
this.accounts = accountList;
this.messenger = messenger;
}
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
LayoutInflater inflater = requireActivity().getLayoutInflater();
View dialogView = inflater.inflate(R.layout.dialog_add_contact, null);
Spinner spinner = dialogView.findViewById(R.id.spinner);
spinner.setAdapter(
new ArrayAdapter<>(requireActivity(), R.layout.support_simple_spinner_dropdown_item, accounts.getValue()));
ButterKnife.bind(this, dialogView);
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
// Hide Spinner when only one account.
if (accounts == null || accounts.size() <= 1) {
accountSelectorContainer.setVisibility(View.GONE);
}
accountSelector.setAdapter(new AccountAdapter(requireActivity(), accounts));
accountSelector.setSelection(0);
builder.setMessage("Add Contact")
.setView(dialogView)
@ -44,7 +93,7 @@ public class AddContactDialogFragment extends AppCompatDialogFragment {
.setPositiveButton("Add", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// Later overwrite in onResume.
}
})
.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
@ -56,4 +105,69 @@ public class AddContactDialogFragment extends AppCompatDialogFragment {
return builder.create();
}
@Override
public void onResume()
{
super.onResume();
final AlertDialog d = (AlertDialog)getDialog();
if(d != null)
{
Button positiveButton = d.getButton(Dialog.BUTTON_POSITIVE);
positiveButton.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v)
{
Account account = accounts.get(accountSelector.getSelectedItemPosition());
String address = contactAddress.getText() != null ? contactAddress.getText().toString() : "";
disposable.add(messenger.addContact(account.getId(), address)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()).subscribe(
AddContactDialogFragment.this::dismiss,
e -> {
if (e instanceof SmackException.NotLoggedInException || e instanceof SmackException.NotConnectedException) {
contactAddressLayout.setError("Account not connected");
} else if (e instanceof ContactAlreadyAddedException) {
contactAddressLayout.setError("Contact already added");
} else if (e instanceof XmppStringprepException) {
contactAddressLayout.setError("Invalid address");
} else {
contactAddressLayout.setError(e.getClass().getName());
}
}
));
}
});
}
}
@Override
public void onDestroy() {
super.onDestroy();
disposable.dispose();
}
private static class AccountAdapter extends ArrayAdapter<Account> {
public AccountAdapter(@NonNull Context context, @NonNull List<Account> objects) {
super(context, R.layout.spinner_item_account, objects);
}
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
if (convertView == null) {
convertView = LayoutInflater.from(getContext()).inflate(R.layout.spinner_item_account, parent, false);
}
TextView textView = convertView.findViewById(R.id.account_address);
textView.setText(getItem(position).getAddress());
return convertView;
}
@Override
public View getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
return getView(position, convertView, parent);
}
}
}

View File

@ -16,6 +16,8 @@ import com.google.android.material.floatingactionbutton.ExtendedFloatingActionBu
import org.mercury_im.messenger.R;
import javax.inject.Inject;
import butterknife.BindView;
import butterknife.ButterKnife;
@ -52,7 +54,7 @@ public class ContactListFragment extends Fragment {
}
private void displayAddContactDialog() {
AddContactDialogFragment addContactDialogFragment = new AddContactDialogFragment(contactListViewModel.getAccounts());
AddContactDialogFragment addContactDialogFragment = new AddContactDialogFragment(contactListViewModel.getAccounts().getValue(), contactListViewModel.getMessenger());
addContactDialogFragment.show(this.getParentFragmentManager(), "add");
}

View File

@ -15,6 +15,7 @@ import org.mercury_im.messenger.R;
import org.mercury_im.messenger.entity.contact.Peer;
import org.mercury_im.messenger.ui.avatar.AvatarDrawable;
import org.mercury_im.messenger.ui.chat.ChatActivity;
import org.mercury_im.messenger.ui.roster.contacts.detail.ContactDetailActivity;
import org.mercury_im.messenger.ui.util.AbstractRecyclerViewAdapter;
import org.mercury_im.messenger.util.ColorUtil;
@ -73,9 +74,9 @@ public class ContactListRecyclerViewAdapter
avatarView.setImageDrawable(new AvatarDrawable(name, address));
view.setOnClickListener(view -> {
Intent intent = new Intent(context, ChatActivity.class);
intent.putExtra(ChatActivity.EXTRA_JID, address);
intent.putExtra(ChatActivity.EXTRA_ACCOUNT, contact.getAccount().getId().toString());
Intent intent = new Intent(context, ContactDetailActivity.class);
intent.putExtra(ContactDetailActivity.EXTRA_JID, address);
intent.putExtra(ContactDetailActivity.EXTRA_ACCOUNT, contact.getAccount().getId().toString());
context.startActivity(intent);
});

View File

@ -7,6 +7,7 @@ import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import org.mercury_im.messenger.MercuryImApplication;
import org.mercury_im.messenger.Messenger;
import org.mercury_im.messenger.data.repository.XmppAccountRepository;
import org.mercury_im.messenger.data.repository.XmppPeerRepository;
import org.mercury_im.messenger.entity.Account;
@ -17,6 +18,7 @@ import java.util.List;
import javax.inject.Inject;
import io.reactivex.disposables.CompositeDisposable;
import lombok.Getter;
public class ContactListViewModel extends ViewModel {
@ -27,6 +29,10 @@ public class ContactListViewModel extends ViewModel {
@Inject
XmppAccountRepository accountRepository;
@Inject
@Getter
Messenger messenger;
private final MutableLiveData<List<Peer>> rosterEntryList = new MutableLiveData<>();
private final MutableLiveData<List<Account>> accounts = new MutableLiveData<>();
private final CompositeDisposable compositeDisposable = new CompositeDisposable();

View File

@ -0,0 +1,48 @@
package org.mercury_im.messenger.ui.roster.contacts.detail;
import android.os.Bundle;
import android.widget.FrameLayout;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.ViewModelProvider;
import org.mercury_im.messenger.MercuryImApplication;
import org.mercury_im.messenger.R;
import java.util.UUID;
import butterknife.BindView;
import butterknife.ButterKnife;
public class ContactDetailActivity extends AppCompatActivity {
public static final String EXTRA_JID = "JID";
public static final String EXTRA_ACCOUNT = "ACCOUNT";
@BindView(R.id.fragment)
FrameLayout container;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_fragment_container);
ButterKnife.bind(this);
MercuryImApplication.getApplication().getAppComponent().inject(this);
if (savedInstanceState == null) {
savedInstanceState = getIntent().getExtras();
if (savedInstanceState == null) return;
}
String jidString = savedInstanceState.getString(EXTRA_JID);
if (jidString != null) {
UUID accountId = UUID.fromString(savedInstanceState.getString(EXTRA_ACCOUNT));
ContactDetailViewModel viewModel = new ViewModelProvider(this).get(ContactDetailViewModel.class);
viewModel.bind(accountId, jidString);
}
getSupportFragmentManager().beginTransaction().replace(R.id.fragment, new ContactDetailFragment(), "contact_details").commit();
}
}

View File

@ -0,0 +1,109 @@
package org.mercury_im.messenger.ui.roster.contacts.detail;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.ViewModelStoreOwner;
import com.google.android.material.chip.ChipGroup;
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton;
import org.mercury_im.messenger.R;
import org.mercury_im.messenger.ui.chat.ChatActivity;
import org.mercury_im.messenger.util.ColorUtil;
import butterknife.BindView;
import butterknife.ButterKnife;
public class ContactDetailFragment extends Fragment {
@BindView(R.id.contact_avatar)
ImageView contactAvatar;
@BindView(R.id.contact_status_badge)
ImageView contactStatusBadge;
@BindView(R.id.contact_name)
TextView contactName;
@BindView(R.id.contact_address)
TextView contactAddress;
@BindView(R.id.contact_presence)
TextView contactPresence;
@BindView(R.id.contact_account)
TextView contactAccount;
@BindView(R.id.contact_groups)
ChipGroup contactGroups;
@BindView(R.id.fab)
ExtendedFloatingActionButton fab;
private ContactDetailViewModel viewModel;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_contact_details, container, false);
ButterKnife.bind(this, view);
if (fab != null) {
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(getContext(), ChatActivity.class);
intent.putExtra(ChatActivity.EXTRA_JID, viewModel.getContactAddress().getValue());
intent.putExtra(ChatActivity.EXTRA_ACCOUNT, viewModel.getAccountId().getValue().toString());
startActivity(intent);
}
});
}
return view;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
viewModel = new ViewModelProvider((ViewModelStoreOwner) context).get(ContactDetailViewModel.class);
observeViewModel();
}
private void observeViewModel() {
viewModel.getContactAvatar().observe(this, drawable -> contactAvatar.setImageDrawable(drawable));
viewModel.getContactPresenceMode().observe(this, mode -> {
int color = 0;
switch (mode) {
case chat:
case available:
color = ColorUtil.rgb(0, 255, 0);
break;
case away:
case xa:
color = ColorUtil.rgb(255, 128, 0);
break;
case dnd:
color = ColorUtil.rgb(255, 0, 0);
break;
}
contactStatusBadge.setColorFilter(color);
});
viewModel.getContactName().observe(this, name -> contactName.setText(name));
viewModel.getContactAddress().observe(this, address -> contactAddress.setText(address));
viewModel.getContactPresenceStatus().observe(this, presenceText -> contactPresence.setText(presenceText));
viewModel.getContactAccountAddress().observe(this, address -> contactAccount.setText(address));
}
}

View File

@ -0,0 +1,129 @@
package org.mercury_im.messenger.ui.roster.contacts.detail;
import android.graphics.drawable.Drawable;
import android.util.Log;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import org.jivesoftware.smack.PresenceListener;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.roster.PresenceEventListener;
import org.jivesoftware.smack.roster.Roster;
import org.jxmpp.jid.Jid;
import org.jxmpp.jid.impl.JidCreate;
import org.mercury_im.messenger.MercuryImApplication;
import org.mercury_im.messenger.Messenger;
import org.mercury_im.messenger.data.repository.PeerRepository;
import org.mercury_im.messenger.entity.contact.Peer;
import org.mercury_im.messenger.ui.avatar.AvatarDrawable;
import org.mercury_im.messenger.util.CombinedPresenceListener;
import java.util.UUID;
import javax.inject.Inject;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
public class ContactDetailViewModel extends ViewModel {
@Inject
PeerRepository peerRepository;
@Inject
Messenger messenger;
private MutableLiveData<UUID> contactAccountId = new MutableLiveData<>(UUID.randomUUID());
private MutableLiveData<String> contactAddress = new MutableLiveData<>("alice@wonderland.lit");
private MutableLiveData<Drawable> contactAvatar = new MutableLiveData<>(new AvatarDrawable("Alice Wonderland", "alice@wonderland.lit"));
private MutableLiveData<Presence.Mode> contactPresenceMode = new MutableLiveData<>(Presence.Mode.available);
private MutableLiveData<String> contactPresenceStatus = new MutableLiveData<>("Going down the rabbit hole.");
private MutableLiveData<String> contactName = new MutableLiveData<>("Alice Wonderland");
private MutableLiveData<String> contactAccountAddress = new MutableLiveData<>("mad@hatter.lit");
private Roster roster;
private CompositeDisposable disposable = new CompositeDisposable();
public ContactDetailViewModel() {
super();
MercuryImApplication.getApplication().getAppComponent().inject(this);
}
public void bind(UUID accountId, String peerAddress) {
Log.d("MMMMMM", "Bind!");
contactAddress.setValue(peerAddress);
contactAccountId.setValue(accountId);
disposable.add(peerRepository.observePeerByAddress(accountId, peerAddress)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(peerOptional -> {
if (!peerOptional.isPresent()) {
return;
}
Peer peer = peerOptional.getItem();
contactAccountAddress.setValue(peer.getAccount().getAddress());
contactAvatar.setValue(new AvatarDrawable(peer.getDisplayName(), peer.getAddress()));
contactName.setValue(peer.getDisplayName());
}));
roster = Roster.getInstanceFor(messenger.getConnectionManager().getConnection(accountId).getConnection());
roster.addPresenceEventListener(presenceEventListener);
Presence presence = roster.getPresence(JidCreate.entityBareFromOrThrowUnchecked(peerAddress));
if (presence != null) {
contactPresenceMode.postValue(presence.getMode());
contactPresenceStatus.postValue(presence.getStatus());
}
}
public LiveData<String> getContactAddress() {
return contactAddress;
}
@Override
protected void onCleared() {
super.onCleared();
disposable.dispose();
if (roster != null) {
roster.removePresenceEventListener(presenceEventListener);
}
}
public LiveData<UUID> getAccountId() {
return contactAccountId;
}
public LiveData<Drawable> getContactAvatar() {
return contactAvatar;
}
public LiveData<Presence.Mode> getContactPresenceMode() {
return contactPresenceMode;
}
public LiveData<String> getContactName() {
return contactName;
}
public LiveData<String> getContactPresenceStatus() {
return contactPresenceStatus;
}
public LiveData<String> getContactAccountAddress() {
return contactAccountAddress;
}
private final PresenceEventListener presenceEventListener = new CombinedPresenceListener() {
@Override
public void presenceReceived(Jid address, Presence presence) {
if (presence.getFrom().asBareJid().toString().equals(getContactAddress().getValue())) {
contactPresenceMode.postValue(presence.getMode());
contactPresenceStatus.postValue(presence.getStatus());
}
}
};
}

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/appbar_layout"
app:layout_constraintBottom_toTopOf="@id/bottom_navigation">
</FrameLayout>
</LinearLayout>

View File

@ -20,10 +20,12 @@
<Spinner
android:id="@+id/spinner"
android:layout_width="match_parent"
android:layout_height="56dp" />
android:layout_height="56dp"
tools:listitem="@layout/spinner_item_account"/>
</LinearLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/address_layout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content">

View File

@ -23,7 +23,6 @@
android:transitionName="fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/action_add_account"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
app:icon="@drawable/ic_add_white_24dp"/>

View File

@ -21,7 +21,6 @@
android:layout_height="wrap_content"
android:id="@+id/fab"
android:transitionName="fab"
android:text="@string/action_add_bookmark"
android:layout_margin="16dp"
android:layout_gravity="bottom|end"
app:icon="@drawable/ic_group_add_black_24dp"/>

View File

@ -1,6 +1,162 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
</LinearLayout>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:background="@color/lightThemeBackground">
<androidx.cardview.widget.CardView
xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="8dp"
card_view:cardElevation="4dp"
card_view:cardCornerRadius="6dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/contact_avatar"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_person_black_24dp" />
<ImageView
android:id="@+id/contact_status_badge"
android:layout_width="15dp"
android:layout_height="15dp"
android:src="@drawable/circle"
android:tint="@android:color/holo_green_dark"
app:layout_constraintBottom_toBottomOf="@+id/contact_avatar"
app:layout_constraintEnd_toEndOf="@+id/contact_avatar" />
<TextView
android:id="@+id/contact_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="@tools:sample/full_names"
android:textSize="20sp"
app:layout_constraintTop_toTopOf="@+id/contact_avatar"
app:layout_constraintStart_toEndOf="@+id/contact_avatar"
android:layout_marginStart="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toTopOf="@+id/contact_address" />
<TextView
android:id="@+id/contact_address"
android:layout_width="285dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
app:layout_constraintBottom_toBottomOf="@+id/contact_avatar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.050"
app:layout_constraintStart_toEndOf="@+id/contact_avatar"
tools:text="romeo@montague.lit" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/contact_presence"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="4dp"
tools:text="Unavailable due to extended inactivity for more than 15 minutes."/>
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="8dp"
app:cardElevation="4dp"
app:cardCornerRadius="6dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Used Account"
android:textAppearance="@style/TextAppearance.AppCompat.Title"/>
<TextView
android:id="@+id/contact_account"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="vanitasvitae@jabberhead.tk"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Categories"
android:textAppearance="@style/TextAppearance.AppCompat.Title"/>
<com.google.android.material.chip.ChipGroup
android:id="@+id/contact_groups"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:chipSpacing="4dp">
<com.google.android.material.chip.Chip
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:chipStrokeColor="@color/black"
android:text="Work"/>
<com.google.android.material.chip.Chip
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Dungeons n' Dragons"/>
</com.google.android.material.chip.ChipGroup>
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
style="@style/Widget.MaterialComponents.ExtendedFloatingActionButton.Icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/fab"
android:transitionName="fab"
android:layout_margin="16dp"
android:layout_gravity="bottom|end"
app:icon="@drawable/ic_message_black_24dp"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -21,7 +21,6 @@
android:layout_height="wrap_content"
android:id="@+id/fab"
android:transitionName="fab"
android:text="@string/action_add_contact"
android:layout_margin="16dp"
android:layout_gravity="bottom|end"
app:icon="@drawable/ic_person_add_black_24dp"/>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/account_address"
android:layout_width="match_parent"
android:layout_height="56dp"
android:padding="16dp"
android:textAppearance="@style/TextAppearance.AppCompat.Widget.DropDownItem" />

View File

@ -11,6 +11,12 @@
app:actionViewClass="androidx.appcompat.widget.SearchView"
app:showAsAction="ifRoom" />
<item android:id="@+id/action_delete_contact"
android:orderInCategory="50"
android:title="@string/action_remove_contact"
android:icon="@drawable/ic_delete_black_24dp"
app:showAsAction="never" />
<item android:id="@+id/action_call"
android:orderInCategory="100"
android:title="@string/action_call"

View File

@ -131,4 +131,5 @@
<string name="action_add_bookmark">Add Bookmark</string>
<string name="action_close_chat">Close Chat</string>
<string name="action_copy">Copy</string>
<string name="action_remove_contact">Remove Contact</string>
</resources>

View File

@ -140,7 +140,7 @@ public class XmppPeerRepository
public Observable<List<Peer>> observeAllContactsOfAccount(UUID accountId) {
return data().select(PeerModel.class)
.where(PeerModel.ACCOUNT_ID.eq(accountId))
.and(isContact())
//.and(isContact())
.get().observableResult()
.map(ResultDelegate::toList)
.map(this::peerModelsToEntities)
@ -148,12 +148,6 @@ public class XmppPeerRepository
.observeOn(observerScheduler());
}
private LogicalCondition<? extends Expression<SubscriptionDirection>, ?> isContact() {
return PeerModel.SUBSCRIPTION_DIRECTION.in(Arrays.asList(
SubscriptionDirection.both,
SubscriptionDirection.to));
}
@Override
public Single<Peer> updatePeer(Peer peer) {
// In order to update, we fetch the model, update it and write it back.

View File

@ -1,16 +1,31 @@
package org.mercury_im.messenger;
import org.jivesoftware.smack.SmackConfiguration;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.roster.Roster;
import org.jivesoftware.smack.roster.RosterEntry;
import org.jxmpp.jid.BareJid;
import org.jxmpp.jid.EntityBareJid;
import org.jxmpp.jid.impl.JidCreate;
import org.jxmpp.stringprep.XmppStringprepException;
import org.mercury_im.messenger.data.repository.Repositories;
import org.mercury_im.messenger.entity.Account;
import org.mercury_im.messenger.entity.contact.Peer;
import org.mercury_im.messenger.exception.ConnectionNotFoundException;
import org.mercury_im.messenger.exception.ContactAlreadyAddedException;
import org.mercury_im.messenger.xmpp.MercuryConnection;
import org.mercury_im.messenger.xmpp.MercuryConnectionManager;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.inject.Inject;
import javax.inject.Singleton;
import io.reactivex.Completable;
import io.reactivex.Single;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
@ -54,4 +69,58 @@ public class Messenger {
return account;
}
public Completable addContact(UUID accountId, String contactAddress) {
return Completable.fromAction(() -> doAddContact(accountId, contactAddress));
}
private void doAddContact(UUID accountId, String contactAddress)
throws ConnectionNotFoundException, XmppStringprepException, ContactAlreadyAddedException,
SmackException.NotLoggedInException, XMPPException.XMPPErrorException,
SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException {
MercuryConnection connection = getConnectionManager().getConnection(accountId);
if (connection == null) {
throw new ConnectionNotFoundException(accountId);
}
EntityBareJid jid = JidCreate.entityBareFrom(contactAddress);
Roster roster = Roster.getInstanceFor(connection.getConnection());
if (roster.getEntry(jid) != null) {
throw new ContactAlreadyAddedException(jid);
}
if (roster.isSubscriptionPreApprovalSupported()) {
LOGGER.log(Level.INFO, "Pre-Approval supported.");
try {
roster.preApproveAndCreateEntry(jid, null, null);
} catch (SmackException.FeatureNotSupportedException e) {
throw new AssertionError("pre-approval failed even though the feature is announced.");
}
} else {
roster.createItemAndRequestSubscription(jid, null, null);
}
}
public Completable deleteContact(Peer contact) {
return Completable.fromAction(() -> doDeleteContact(contact));
}
private void doDeleteContact(Peer contact)
throws ConnectionNotFoundException, XmppStringprepException,
SmackException.NotLoggedInException, XMPPException.XMPPErrorException,
SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException {
MercuryConnection connection = getConnectionManager().getConnection(contact.getAccount().getId());
if (connection == null) {
throw new ConnectionNotFoundException(contact.getAccount().getId());
}
Roster roster = Roster.getInstanceFor(connection.getConnection());
EntityBareJid jid = JidCreate.entityBareFrom(contact.getAddress());
RosterEntry entry = roster.getEntry(jid);
if (entry != null) {
roster.removeEntry(entry);
} else {
throw new IllegalStateException("Contact " + jid.toString() + " not in roster!");
}
}
}

View File

@ -0,0 +1,16 @@
package org.mercury_im.messenger.exception;
import java.util.UUID;
import lombok.Getter;
public class ConnectionNotFoundException extends Exception {
@Getter
private final UUID accountId;
public ConnectionNotFoundException(UUID accountId) {
super("Connection with ID " + accountId.toString() + " not registered.");
this.accountId = accountId;
}
}

View File

@ -0,0 +1,16 @@
package org.mercury_im.messenger.exception;
import org.jxmpp.jid.Jid;
import lombok.Getter;
public class ContactAlreadyAddedException extends Exception {
@Getter
private final Jid jid;
public ContactAlreadyAddedException(Jid jid) {
super("Contact with address " + jid.toString() + " is already a contact.");
this.jid = jid;
}
}

View File

@ -21,8 +21,6 @@ import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import io.reactivex.Observable;
import io.reactivex.Scheduler;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
import io.reactivex.subjects.BehaviorSubject;

View File

@ -1,13 +1,28 @@
package org.mercury_im.messenger.usecase;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.roster.PresenceEventListener;
import org.jivesoftware.smack.roster.Roster;
import org.jivesoftware.smack.roster.RosterEntry;
import org.jivesoftware.smack.roster.RosterListener;
import org.jivesoftware.smack.roster.RosterLoadedListener;
import org.jivesoftware.smack.roster.SubscribeListener;
import org.jivesoftware.smack.roster.rosterstore.RosterStore;
import org.jxmpp.jid.BareJid;
import org.jxmpp.jid.FullJid;
import org.jxmpp.jid.Jid;
import org.mercury_im.messenger.data.repository.AccountRepository;
import org.mercury_im.messenger.data.repository.PeerRepository;
import org.mercury_im.messenger.entity.Account;
import org.mercury_im.messenger.store.MercuryRosterStore;
import org.mercury_im.messenger.xmpp.MercuryConnection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.inject.Inject;
@ -16,6 +31,8 @@ public class RosterStoreBinder {
private final AccountRepository accountRepository;
private final PeerRepository peerRepository;
private final Logger LOGGER = Logger.getLogger(RosterStoreBinder.class.getName());
@Inject
public RosterStoreBinder(AccountRepository accountRepository, PeerRepository peerRepository) {
this.accountRepository = accountRepository;
@ -27,6 +44,75 @@ public class RosterStoreBinder {
createRosterStore(connection.getAccount().getId(), accountRepository, peerRepository);
Roster roster = Roster.getInstanceFor(connection.getConnection());
roster.setRosterStore(store);
roster.addSubscribeListener(new SubscribeListener() {
@Override
public SubscribeAnswer processSubscribe(Jid from, Presence subscribeRequest) {
RosterEntry entry = roster.getEntry(from.asBareJid());
if (entry != null) {
return SubscribeAnswer.ApproveAndAlsoRequestIfRequired;
}
LOGGER.log(Level.INFO, "processSubscribe " + from);
return null;
}
});
roster.addPresenceEventListener(new PresenceEventListener() {
@Override
public void presenceAvailable(FullJid address, Presence availablePresence) {
LOGGER.log(Level.INFO, "presenceAvailable " + address.toString());
}
@Override
public void presenceUnavailable(FullJid address, Presence presence) {
LOGGER.log(Level.INFO, "presenceUnavailable " + address);
}
@Override
public void presenceError(Jid address, Presence errorPresence) {
LOGGER.log(Level.INFO, "presenceError " + address);
}
@Override
public void presenceSubscribed(BareJid address, Presence subscribedPresence) {
LOGGER.log(Level.INFO, "presenceSubscribed " + address);
}
@Override
public void presenceUnsubscribed(BareJid address, Presence unsubscribedPresence) {
LOGGER.log(Level.INFO, "presenceUnsubscribed " + address);
}
});
roster.addRosterListener(new RosterListener() {
@Override
public void entriesAdded(Collection<Jid> addresses) {
LOGGER.log(Level.INFO, "entriesAdded " + Arrays.toString(addresses.toArray()));
}
@Override
public void entriesUpdated(Collection<Jid> addresses) {
LOGGER.log(Level.INFO, "entriesUpdated " + Arrays.toString(addresses.toArray()));
}
@Override
public void entriesDeleted(Collection<Jid> addresses) {
LOGGER.log(Level.INFO, "entriesDeleted " + Arrays.toString(addresses.toArray()));
}
@Override
public void presenceChanged(Presence presence) {
LOGGER.log(Level.INFO, "presenceChanged " + presence.toString());
}
});
roster.addRosterLoadedListener(new RosterLoadedListener() {
@Override
public void onRosterLoaded(Roster roster) {
LOGGER.log(Level.INFO, "onRosterLoaded");
}
@Override
public void onRosterLoadingFailed(Exception exception) {
LOGGER.log(Level.INFO, "onRosterLoadingFailed");
}
});
}
private MercuryRosterStore createRosterStore(UUID accountId, AccountRepository accountRepository, PeerRepository peerRepository) {

View File

@ -0,0 +1,36 @@
package org.mercury_im.messenger.util;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.roster.PresenceEventListener;
import org.jxmpp.jid.BareJid;
import org.jxmpp.jid.FullJid;
import org.jxmpp.jid.Jid;
public abstract class CombinedPresenceListener implements PresenceEventListener {
@Override
public void presenceAvailable(FullJid address, Presence availablePresence) {
presenceReceived(address, availablePresence);
}
@Override
public void presenceUnavailable(FullJid address, Presence presence) {
presenceReceived(address, presence);
}
@Override
public void presenceError(Jid address, Presence errorPresence) {
presenceReceived(address, errorPresence);
}
@Override
public void presenceSubscribed(BareJid address, Presence subscribedPresence) {
presenceReceived(address, subscribedPresence);
}
@Override
public void presenceUnsubscribed(BareJid address, Presence unsubscribedPresence) {
presenceReceived(address, unsubscribedPresence);
}
public abstract void presenceReceived(Jid address, Presence presence);
}

View File

@ -1,6 +1,7 @@
package org.mercury_im.messenger.xmpp;
import org.jivesoftware.smack.ReconnectionManager;
import org.jivesoftware.smack.roster.Roster;
import org.jivesoftware.smackx.carbons.CarbonManager;
import org.jivesoftware.smackx.iqversion.VersionManager;
import org.jivesoftware.smackx.mam.MamManager;
@ -21,5 +22,7 @@ public class SmackConfig {
StableUniqueStanzaIdManager.setEnabledByDefault(true);
CarbonManager.setEnabledByDefault(true);
Roster.setRosterLoadedAtLoginDefault(true);
}
}