diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 46c12f2..221bfc4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -35,6 +35,8 @@ + + diff --git a/app/src/main/java/org/mercury_im/messenger/di/component/AppComponent.java b/app/src/main/java/org/mercury_im/messenger/di/component/AppComponent.java index ca69c42..d834ef4 100644 --- a/app/src/main/java/org/mercury_im/messenger/di/component/AppComponent.java +++ b/app/src/main/java/org/mercury_im/messenger/di/component/AppComponent.java @@ -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 diff --git a/app/src/main/java/org/mercury_im/messenger/ui/chat/ChatActivity.java b/app/src/main/java/org/mercury_im/messenger/ui/chat/ChatActivity.java index a6e2064..1564c01 100644 --- a/app/src/main/java/org/mercury_im/messenger/ui/chat/ChatActivity.java +++ b/app/src/main/java/org/mercury_im/messenger/ui/chat/ChatActivity.java @@ -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: diff --git a/app/src/main/java/org/mercury_im/messenger/ui/chat/ChatViewModel.java b/app/src/main/java/org/mercury_im/messenger/ui/chat/ChatViewModel.java index 39c2a38..dfeab82 100644 --- a/app/src/main/java/org/mercury_im/messenger/ui/chat/ChatViewModel.java +++ b/app/src/main/java/org/mercury_im/messenger/ui/chat/ChatViewModel.java @@ -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 contact = new MutableLiveData<>(); private MutableLiveData> messages = new MutableLiveData<>(); private MutableLiveData 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); + })); + } } diff --git a/app/src/main/java/org/mercury_im/messenger/ui/chatlist/ChatListRecyclerViewAdapter.java b/app/src/main/java/org/mercury_im/messenger/ui/chatlist/ChatListRecyclerViewAdapter.java index 13a1e1f..87ff7cd 100644 --- a/app/src/main/java/org/mercury_im/messenger/ui/chatlist/ChatListRecyclerViewAdapter.java +++ b/app/src/main/java/org/mercury_im/messenger/ui/chatlist/ChatListRecyclerViewAdapter.java @@ -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()); } } diff --git a/app/src/main/java/org/mercury_im/messenger/ui/roster/contacts/AddContactDialogFragment.java b/app/src/main/java/org/mercury_im/messenger/ui/roster/contacts/AddContactDialogFragment.java index c0170dc..1debbbe 100644 --- a/app/src/main/java/org/mercury_im/messenger/ui/roster/contacts/AddContactDialogFragment.java +++ b/app/src/main/java/org/mercury_im/messenger/ui/roster/contacts/AddContactDialogFragment.java @@ -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> accounts; + private final List accounts; + private final Messenger messenger; - public AddContactDialogFragment(LiveData> 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 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 { + + public AccountAdapter(@NonNull Context context, @NonNull List 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); + } + } } diff --git a/app/src/main/java/org/mercury_im/messenger/ui/roster/contacts/ContactListFragment.java b/app/src/main/java/org/mercury_im/messenger/ui/roster/contacts/ContactListFragment.java index c503995..e15b6d7 100644 --- a/app/src/main/java/org/mercury_im/messenger/ui/roster/contacts/ContactListFragment.java +++ b/app/src/main/java/org/mercury_im/messenger/ui/roster/contacts/ContactListFragment.java @@ -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"); } diff --git a/app/src/main/java/org/mercury_im/messenger/ui/roster/contacts/ContactListRecyclerViewAdapter.java b/app/src/main/java/org/mercury_im/messenger/ui/roster/contacts/ContactListRecyclerViewAdapter.java index 584f866..77e6adf 100644 --- a/app/src/main/java/org/mercury_im/messenger/ui/roster/contacts/ContactListRecyclerViewAdapter.java +++ b/app/src/main/java/org/mercury_im/messenger/ui/roster/contacts/ContactListRecyclerViewAdapter.java @@ -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); }); diff --git a/app/src/main/java/org/mercury_im/messenger/ui/roster/contacts/ContactListViewModel.java b/app/src/main/java/org/mercury_im/messenger/ui/roster/contacts/ContactListViewModel.java index e807fea..5ddfb8a 100644 --- a/app/src/main/java/org/mercury_im/messenger/ui/roster/contacts/ContactListViewModel.java +++ b/app/src/main/java/org/mercury_im/messenger/ui/roster/contacts/ContactListViewModel.java @@ -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> rosterEntryList = new MutableLiveData<>(); private final MutableLiveData> accounts = new MutableLiveData<>(); private final CompositeDisposable compositeDisposable = new CompositeDisposable(); diff --git a/app/src/main/java/org/mercury_im/messenger/ui/roster/contacts/detail/ContactDetailActivity.java b/app/src/main/java/org/mercury_im/messenger/ui/roster/contacts/detail/ContactDetailActivity.java new file mode 100644 index 0000000..29fd5e8 --- /dev/null +++ b/app/src/main/java/org/mercury_im/messenger/ui/roster/contacts/detail/ContactDetailActivity.java @@ -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(); + } +} diff --git a/app/src/main/java/org/mercury_im/messenger/ui/roster/contacts/detail/ContactDetailFragment.java b/app/src/main/java/org/mercury_im/messenger/ui/roster/contacts/detail/ContactDetailFragment.java new file mode 100644 index 0000000..1094c47 --- /dev/null +++ b/app/src/main/java/org/mercury_im/messenger/ui/roster/contacts/detail/ContactDetailFragment.java @@ -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)); + } +} diff --git a/app/src/main/java/org/mercury_im/messenger/ui/roster/contacts/detail/ContactDetailViewModel.java b/app/src/main/java/org/mercury_im/messenger/ui/roster/contacts/detail/ContactDetailViewModel.java new file mode 100644 index 0000000..52e2a64 --- /dev/null +++ b/app/src/main/java/org/mercury_im/messenger/ui/roster/contacts/detail/ContactDetailViewModel.java @@ -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 contactAccountId = new MutableLiveData<>(UUID.randomUUID()); + private MutableLiveData contactAddress = new MutableLiveData<>("alice@wonderland.lit"); + private MutableLiveData contactAvatar = new MutableLiveData<>(new AvatarDrawable("Alice Wonderland", "alice@wonderland.lit")); + private MutableLiveData contactPresenceMode = new MutableLiveData<>(Presence.Mode.available); + private MutableLiveData contactPresenceStatus = new MutableLiveData<>("Going down the rabbit hole."); + private MutableLiveData contactName = new MutableLiveData<>("Alice Wonderland"); + private MutableLiveData 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 getContactAddress() { + return contactAddress; + } + + @Override + protected void onCleared() { + super.onCleared(); + disposable.dispose(); + if (roster != null) { + roster.removePresenceEventListener(presenceEventListener); + } + } + + public LiveData getAccountId() { + return contactAccountId; + } + + public LiveData getContactAvatar() { + return contactAvatar; + } + + public LiveData getContactPresenceMode() { + return contactPresenceMode; + } + + public LiveData getContactName() { + return contactName; + } + + public LiveData getContactPresenceStatus() { + return contactPresenceStatus; + } + + public LiveData 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()); + } + } + }; +} diff --git a/app/src/main/res/layout/activity_fragment_container.xml b/app/src/main/res/layout/activity_fragment_container.xml new file mode 100644 index 0000000..dd469fa --- /dev/null +++ b/app/src/main/res/layout/activity_fragment_container.xml @@ -0,0 +1,19 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_add_contact.xml b/app/src/main/res/layout/dialog_add_contact.xml index 5a8addf..b54de8a 100644 --- a/app/src/main/res/layout/dialog_add_contact.xml +++ b/app/src/main/res/layout/dialog_add_contact.xml @@ -20,10 +20,12 @@ + android:layout_height="56dp" + tools:listitem="@layout/spinner_item_account"/> diff --git a/app/src/main/res/layout/fragment_account_list.xml b/app/src/main/res/layout/fragment_account_list.xml index bbbbc9f..c49bb15 100644 --- a/app/src/main/res/layout/fragment_account_list.xml +++ b/app/src/main/res/layout/fragment_account_list.xml @@ -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"/> diff --git a/app/src/main/res/layout/fragment_bookmarks_list.xml b/app/src/main/res/layout/fragment_bookmarks_list.xml index 2e0e5bf..f41ef12 100644 --- a/app/src/main/res/layout/fragment_bookmarks_list.xml +++ b/app/src/main/res/layout/fragment_bookmarks_list.xml @@ -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"/> diff --git a/app/src/main/res/layout/fragment_contact_details.xml b/app/src/main/res/layout/fragment_contact_details.xml index 3509b84..c59483c 100644 --- a/app/src/main/res/layout/fragment_contact_details.xml +++ b/app/src/main/res/layout/fragment_contact_details.xml @@ -1,6 +1,162 @@ - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_contact_list.xml b/app/src/main/res/layout/fragment_contact_list.xml index 0fa9386..ee01cac 100644 --- a/app/src/main/res/layout/fragment_contact_list.xml +++ b/app/src/main/res/layout/fragment_contact_list.xml @@ -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"/> diff --git a/app/src/main/res/layout/spinner_item_account.xml b/app/src/main/res/layout/spinner_item_account.xml new file mode 100644 index 0000000..a9b6923 --- /dev/null +++ b/app/src/main/res/layout/spinner_item_account.xml @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_chat.xml b/app/src/main/res/menu/menu_chat.xml index 7b59a6d..1e7c05c 100644 --- a/app/src/main/res/menu/menu_chat.xml +++ b/app/src/main/res/menu/menu_chat.xml @@ -11,6 +11,12 @@ app:actionViewClass="androidx.appcompat.widget.SearchView" app:showAsAction="ifRoom" /> + + Add Bookmark Close Chat Copy + Remove Contact diff --git a/data/src/main/java/org/mercury_im/messenger/data/repository/XmppPeerRepository.java b/data/src/main/java/org/mercury_im/messenger/data/repository/XmppPeerRepository.java index 52af54f..0187832 100644 --- a/data/src/main/java/org/mercury_im/messenger/data/repository/XmppPeerRepository.java +++ b/data/src/main/java/org/mercury_im/messenger/data/repository/XmppPeerRepository.java @@ -140,7 +140,7 @@ public class XmppPeerRepository public Observable> 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, ?> isContact() { - return PeerModel.SUBSCRIPTION_DIRECTION.in(Arrays.asList( - SubscriptionDirection.both, - SubscriptionDirection.to)); - } - @Override public Single updatePeer(Peer peer) { // In order to update, we fetch the model, update it and write it back. diff --git a/domain/src/main/java/org/mercury_im/messenger/Messenger.java b/domain/src/main/java/org/mercury_im/messenger/Messenger.java index fc3f5b9..3f8a0b8 100644 --- a/domain/src/main/java/org/mercury_im/messenger/Messenger.java +++ b/domain/src/main/java/org/mercury_im/messenger/Messenger.java @@ -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!"); + } + } } diff --git a/domain/src/main/java/org/mercury_im/messenger/exception/ConnectionNotFoundException.java b/domain/src/main/java/org/mercury_im/messenger/exception/ConnectionNotFoundException.java new file mode 100644 index 0000000..1aee554 --- /dev/null +++ b/domain/src/main/java/org/mercury_im/messenger/exception/ConnectionNotFoundException.java @@ -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; + } +} diff --git a/domain/src/main/java/org/mercury_im/messenger/exception/ContactAlreadyAddedException.java b/domain/src/main/java/org/mercury_im/messenger/exception/ContactAlreadyAddedException.java new file mode 100644 index 0000000..2f9b30b --- /dev/null +++ b/domain/src/main/java/org/mercury_im/messenger/exception/ContactAlreadyAddedException.java @@ -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; + } +} diff --git a/domain/src/main/java/org/mercury_im/messenger/store/MercuryRosterStore.java b/domain/src/main/java/org/mercury_im/messenger/store/MercuryRosterStore.java index 7a8fdbc..e53fc1e 100644 --- a/domain/src/main/java/org/mercury_im/messenger/store/MercuryRosterStore.java +++ b/domain/src/main/java/org/mercury_im/messenger/store/MercuryRosterStore.java @@ -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; diff --git a/domain/src/main/java/org/mercury_im/messenger/usecase/RosterStoreBinder.java b/domain/src/main/java/org/mercury_im/messenger/usecase/RosterStoreBinder.java index 6e40009..d826042 100644 --- a/domain/src/main/java/org/mercury_im/messenger/usecase/RosterStoreBinder.java +++ b/domain/src/main/java/org/mercury_im/messenger/usecase/RosterStoreBinder.java @@ -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 addresses) { + LOGGER.log(Level.INFO, "entriesAdded " + Arrays.toString(addresses.toArray())); + } + + @Override + public void entriesUpdated(Collection addresses) { + LOGGER.log(Level.INFO, "entriesUpdated " + Arrays.toString(addresses.toArray())); + } + + @Override + public void entriesDeleted(Collection 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) { diff --git a/domain/src/main/java/org/mercury_im/messenger/util/CombinedPresenceListener.java b/domain/src/main/java/org/mercury_im/messenger/util/CombinedPresenceListener.java new file mode 100644 index 0000000..d5ef8d2 --- /dev/null +++ b/domain/src/main/java/org/mercury_im/messenger/util/CombinedPresenceListener.java @@ -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); +} diff --git a/domain/src/main/java/org/mercury_im/messenger/xmpp/SmackConfig.java b/domain/src/main/java/org/mercury_im/messenger/xmpp/SmackConfig.java index aef78b8..4027424 100644 --- a/domain/src/main/java/org/mercury_im/messenger/xmpp/SmackConfig.java +++ b/domain/src/main/java/org/mercury_im/messenger/xmpp/SmackConfig.java @@ -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); } }