From fdf5ded0a4f17dba0becc66acdd006e5220beabe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20B=C3=A9raud?= <adrien.beraud@savoirfairelinux.com> Date: Wed, 19 May 2021 15:47:56 -0400 Subject: [PATCH] swarm: various fixes and improvements Change-Id: Ib36417ad4d68acfbb7d257c76ac739f6f02c4e01 --- .../cx/ring/adapters/ConversationAdapter.java | 62 ++++-- .../cx/ring/adapters/SmartListAdapter.java | 2 +- .../cx/ring/adapters/SmartListDiffUtil.java | 8 +- .../ring/client/ContactDetailsActivity.java | 176 ++++++++++++--- .../cx/ring/client/ConversationActivity.java | 5 +- .../client/ConversationSelectionActivity.java | 4 +- .../java/cx/ring/fragments/CallFragment.java | 3 +- .../ring/fragments/ContactPickerFragment.java | 4 +- .../ring/fragments/ConversationFragment.java | 44 ++-- .../cx/ring/fragments/LinkDeviceFragment.java | 18 +- .../cx/ring/fragments/SmartListFragment.java | 132 +++++------ .../ring/service/CallNotificationService.java | 8 +- .../java/cx/ring/service/DRingService.java | 1 + .../cx/ring/services/DataTransferService.java | 83 ++++--- .../services/DeviceRuntimeServiceImpl.java | 20 +- .../services/NotificationServiceImpl.java | 101 ++++----- .../SharedPreferencesServiceImpl.java | 5 + .../cx/ring/services/VCardServiceImpl.java | 5 +- .../cx/ring/settings/AccountFragment.java | 7 +- .../java/cx/ring/share/ShareFragment.java | 11 +- .../java/cx/ring/tv/call/TVCallFragment.java | 3 +- .../ring/tv/cards/contacts/ContactCard.java | 4 +- .../ring/tv/contact/TVContactPresenter.java | 2 +- .../TVContactRequestDetailPresenter.java | 2 +- .../conversation/TvConversationAdapter.java | 35 ++- .../java/cx/ring/tv/main/HomeActivity.java | 5 +- .../java/cx/ring/tv/main/MainFragment.java | 2 +- .../java/cx/ring/utils/AndroidFileUtils.java | 8 +- .../java/cx/ring/utils/BluetoothWrapper.java | 3 +- .../java/cx/ring/utils/ClipboardHelper.java | 30 +-- .../java/cx/ring/utils/ConversationPath.java | 23 +- .../java/cx/ring/utils/ResourceMapper.java | 48 ++-- .../java/cx/ring/views/AvatarDrawable.java | 130 ++++++----- .../res/layout/activity_contact_details.xml | 16 +- .../src/main/res/layout/frag_conversation.xml | 3 +- .../res/layout/item_contact_horizontal.xml | 56 +++++ .../app/src/main/res/values/strings.xml | 1 + .../java/net/jami/call/CallPresenter.java | 34 +-- .../src/main/java/net/jami/call/CallView.java | 3 +- .../conversation/ConversationPresenter.java | 66 +++--- .../net/jami/facades/ConversationFacade.java | 174 ++++++++------- .../src/main/java/net/jami/model/Account.java | 17 +- .../src/main/java/net/jami/model/Call.java | 34 ++- .../src/main/java/net/jami/model/Contact.java | 4 +- .../java/net/jami/model/Conversation.java | 207 +++++++++++------- .../java/net/jami/model/DataTransfer.java | 16 +- .../main/java/net/jami/model/Interaction.java | 18 +- .../main/java/net/jami/model/TextMessage.java | 9 - .../net/jami/services/AccountService.java | 152 ++++++++----- .../java/net/jami/services/CallService.java | 11 +- .../net/jami/services/ContactService.java | 7 +- .../jami/services/DeviceRuntimeService.java | 2 + .../net/jami/services/HistoryService.java | 2 +- .../jami/services/NotificationService.java | 22 +- .../java/net/jami/services/VCardService.java | 8 +- .../jami/smartlist/SmartListPresenter.java | 43 ++-- .../jami/smartlist/SmartListViewModel.java | 9 +- .../main/java/net/jami/utils/VCardUtils.java | 2 +- 58 files changed, 1149 insertions(+), 761 deletions(-) create mode 100644 ring-android/app/src/main/res/layout/item_contact_horizontal.xml diff --git a/ring-android/app/src/main/java/cx/ring/adapters/ConversationAdapter.java b/ring-android/app/src/main/java/cx/ring/adapters/ConversationAdapter.java index cc69caea7..f6cf7b112 100644 --- a/ring-android/app/src/main/java/cx/ring/adapters/ConversationAdapter.java +++ b/ring-android/app/src/main/java/cx/ring/adapters/ConversationAdapter.java @@ -65,6 +65,18 @@ import com.bumptech.glide.load.resource.bitmap.CenterInside; import com.bumptech.glide.load.resource.bitmap.RoundedCorners; import com.bumptech.glide.request.target.DrawableImageViewTarget; +import net.jami.conversation.ConversationPresenter; +import net.jami.model.Account; +import net.jami.model.Call; +import net.jami.model.Contact; +import net.jami.model.ContactEvent; +import net.jami.model.DataTransfer; +import net.jami.model.Interaction; +import net.jami.model.Interaction.InteractionStatus; +import net.jami.model.Interaction.InteractionType; +import net.jami.model.TextMessage; +import net.jami.utils.StringUtils; + import java.io.File; import java.text.DateFormat; import java.util.ArrayList; @@ -75,24 +87,11 @@ import java.util.concurrent.TimeUnit; import cx.ring.R; import cx.ring.client.MediaViewerActivity; -import net.jami.conversation.ConversationPresenter; import cx.ring.fragments.ConversationFragment; -import net.jami.model.Account; -import net.jami.model.Contact; -import net.jami.model.ContactEvent; -import net.jami.model.DataTransfer; -import net.jami.model.Interaction; -import net.jami.model.Interaction.InteractionStatus; -import net.jami.model.Interaction.InteractionType; -import net.jami.model.Call; -import net.jami.model.TextMessage; -import cx.ring.service.DRingService; -import cx.ring.utils.AndroidFileUtils; import cx.ring.utils.ContentUriHandler; import cx.ring.utils.GlideApp; import cx.ring.utils.GlideOptions; import cx.ring.utils.ResourceMapper; -import net.jami.utils.StringUtils; import cx.ring.views.ConversationViewHolder; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; @@ -178,6 +177,7 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationViewHo } for (int i = 0, n = mInteractions.size(); i<n; i++) { if (mInteractions.get(i).getParentIds().contains(e.getMessageId())) { + Log.w(TAG, "Adding message at " + i + " previous count " + n); mInteractions.add(i, e); notifyItemInserted(i); return i == n-1; @@ -194,6 +194,7 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationViewHo } public void update(Interaction e) { + Log.w(TAG, "update " + e.getMessageId()); if (!e.isIncoming() && e.getStatus() == InteractionStatus.SUCCESS) { notifyItemChanged(lastDeliveredPosition); } @@ -207,18 +208,33 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationViewHo } public void remove(Interaction e) { - for (int i = mInteractions.size() - 1; i >= 0; i--) { - Interaction element = mInteractions.get(i); - if (e.getId() == element.getId()) { - mInteractions.remove(i); - notifyItemRemoved(i); - if (i > 0) { - notifyItemChanged(i - 1); + if (e.getMessageId() != null) { + for (int i = mInteractions.size() - 1; i >= 0; i--) { + if (e.getMessageId().equals(mInteractions.get(i).getMessageId())) { + mInteractions.remove(i); + notifyItemRemoved(i); + if (i > 0) { + notifyItemChanged(i - 1); + } + if (i != mInteractions.size()) { + notifyItemChanged(i); + } + break; } - if (i != mInteractions.size()) { - notifyItemChanged(i); + } + } else { + for (int i = mInteractions.size() - 1; i >= 0; i--) { + if (e.getId() == mInteractions.get(i).getId()) { + mInteractions.remove(i); + notifyItemRemoved(i); + if (i > 0) { + notifyItemChanged(i - 1); + } + if (i != mInteractions.size()) { + notifyItemChanged(i); + } + break; } - break; } } } diff --git a/ring-android/app/src/main/java/cx/ring/adapters/SmartListAdapter.java b/ring-android/app/src/main/java/cx/ring/adapters/SmartListAdapter.java index d4e973218..4d8c77179 100644 --- a/ring-android/app/src/main/java/cx/ring/adapters/SmartListAdapter.java +++ b/ring-android/app/src/main/java/cx/ring/adapters/SmartListAdapter.java @@ -106,7 +106,7 @@ public class SmartListAdapter extends RecyclerView.Adapter<SmartListViewHolder> public void update(SmartListViewModel smartListViewModel) { for (int i = 0; i < mSmartListViewModels.size(); i++) { SmartListViewModel old = mSmartListViewModels.get(i); - if (old.getContact() == smartListViewModel.getContact()) { + if (old.getContacts() == smartListViewModel.getContacts()) { mSmartListViewModels.set(i, smartListViewModel); notifyItemChanged(i); return; diff --git a/ring-android/app/src/main/java/cx/ring/adapters/SmartListDiffUtil.java b/ring-android/app/src/main/java/cx/ring/adapters/SmartListDiffUtil.java index 52a951677..f26aff1fb 100644 --- a/ring-android/app/src/main/java/cx/ring/adapters/SmartListDiffUtil.java +++ b/ring-android/app/src/main/java/cx/ring/adapters/SmartListDiffUtil.java @@ -51,11 +51,11 @@ public class SmartListDiffUtil extends DiffUtil.Callback { SmartListViewModel newItem = mNewList.get(newItemPosition); if (newItem.getHeaderTitle() != oldItem.getHeaderTitle()) return false; - if (newItem.getContact() != oldItem.getContact()) { - if (newItem.getContact().size() != oldItem.getContact().size()) + if (newItem.getContacts() != oldItem.getContacts()) { + if (newItem.getContacts().size() != oldItem.getContacts().size()) return false; - for (int i = 0; i < newItem.getContact().size(); i++) { - if (newItem.getContact().get(i) != oldItem.getContact().get(i)) + for (int i = 0; i < newItem.getContacts().size(); i++) { + if (newItem.getContacts().get(i) != oldItem.getContacts().get(i)) return false; } } diff --git a/ring-android/app/src/main/java/cx/ring/client/ContactDetailsActivity.java b/ring-android/app/src/main/java/cx/ring/client/ContactDetailsActivity.java index a5d520c14..71dc2eb04 100644 --- a/ring-android/app/src/main/java/cx/ring/client/ContactDetailsActivity.java +++ b/ring-android/app/src/main/java/cx/ring/client/ContactDetailsActivity.java @@ -26,48 +26,53 @@ import android.content.Intent; import android.content.SharedPreferences; import android.content.res.ColorStateList; import android.graphics.Color; +import android.graphics.drawable.Drawable; import android.os.Bundle; - -import com.google.android.material.appbar.CollapsingToolbarLayout; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.google.android.material.floatingactionbutton.FloatingActionButton; -import com.google.android.material.snackbar.Snackbar; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.StringRes; import androidx.appcompat.app.AppCompatActivity; +import androidx.core.view.ViewCompat; +import androidx.recyclerview.widget.RecyclerView; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.ViewGroup; -import android.widget.Toast; +import com.google.android.material.appbar.CollapsingToolbarLayout; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.google.android.material.snackbar.Snackbar; + +import net.jami.facades.ConversationFacade; +import net.jami.model.Call; +import net.jami.model.Conference; +import net.jami.model.Contact; +import net.jami.model.Conversation; +import net.jami.model.Uri; +import net.jami.services.AccountService; +import net.jami.services.NotificationService; import java.util.ArrayList; +import java.util.List; import javax.inject.Inject; import javax.inject.Singleton; -import androidx.core.view.ViewCompat; -import androidx.recyclerview.widget.RecyclerView; - import cx.ring.R; import cx.ring.application.JamiApplication; import cx.ring.views.AvatarFactory; import cx.ring.databinding.ActivityContactDetailsBinding; import cx.ring.databinding.ItemContactActionBinding; -import net.jami.facades.ConversationFacade; +import cx.ring.databinding.ItemContactHorizontalBinding; import cx.ring.fragments.CallFragment; import cx.ring.fragments.ConversationFragment; -import net.jami.model.Contact; -import net.jami.model.Conference; -import net.jami.model.Conversation; -import net.jami.model.Call; -import net.jami.model.Uri; -import net.jami.services.AccountService; -import net.jami.services.NotificationService; +import cx.ring.services.SharedPreferencesServiceImpl; import cx.ring.utils.ConversationPath; import cx.ring.views.AvatarDrawable; +import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; @@ -93,6 +98,8 @@ public class ContactDetailsActivity extends AppCompatActivity { static class ContactAction { @DrawableRes final int icon; + final Single<Drawable> drawable; + final CharSequence title; final IContactAction callback; @@ -104,6 +111,7 @@ public class ContactDetailsActivity extends AppCompatActivity { iconTint = tint; title = t; callback = cb; + drawable = null; } ContactAction(@DrawableRes int i, CharSequence t, IContactAction cb) { @@ -111,6 +119,14 @@ public class ContactDetailsActivity extends AppCompatActivity { iconTint = Color.BLACK; title = t; callback = cb; + drawable = null; + } + ContactAction(Single<Drawable> d, CharSequence t, IContactAction cb) { + drawable = d; + icon = 0; + iconTint = Color.BLACK; + title = t; + callback = cb; } void setIconTint(int tint) { @@ -125,10 +141,12 @@ public class ContactDetailsActivity extends AppCompatActivity { static class ContactActionView extends RecyclerView.ViewHolder { final ItemContactActionBinding binding; IContactAction callback; + final CompositeDisposable disposable = new CompositeDisposable(); - ContactActionView(@NonNull ItemContactActionBinding b) { + ContactActionView(@NonNull ItemContactActionBinding b, CompositeDisposable parentDisposable) { super(b.getRoot()); binding = b; + parentDisposable.add(disposable); itemView.setOnClickListener(view -> { try { if (callback != null) @@ -142,34 +160,112 @@ public class ContactDetailsActivity extends AppCompatActivity { private static class ContactActionAdapter extends RecyclerView.Adapter<ContactActionView> { private final ArrayList<ContactAction> actions = new ArrayList<>(); + private final CompositeDisposable disposable; + + private ContactActionAdapter(CompositeDisposable disposable) { + this.disposable = disposable; + } @NonNull @Override public ContactActionView onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext()); ItemContactActionBinding itemBinding = ItemContactActionBinding.inflate(layoutInflater, parent, false); - return new ContactActionView(itemBinding); + return new ContactActionView(itemBinding, disposable); } @Override public void onBindViewHolder(@NonNull ContactActionView holder, int position) { ContactAction action = actions.get(position); - holder.binding.actionIcon.setBackgroundResource(action.icon); - holder.binding.actionIcon.setText(action.iconSymbol); - if (action.iconTint != Color.BLACK) - ViewCompat.setBackgroundTintList(holder.binding.actionIcon, ColorStateList.valueOf(action.iconTint)); + holder.disposable.clear(); + if (action.drawable != null) { + holder.disposable.add(action.drawable.subscribe(holder.binding.actionIcon::setBackground)); + } else { + holder.binding.actionIcon.setBackgroundResource(action.icon); + holder.binding.actionIcon.setText(action.iconSymbol); + if (action.iconTint != Color.BLACK) + ViewCompat.setBackgroundTintList(holder.binding.actionIcon, ColorStateList.valueOf(action.iconTint)); + } holder.binding.actionTitle.setText(action.title); holder.callback = action.callback; } + @Override + public void onViewRecycled(@NonNull ContactActionView holder) { + holder.disposable.clear(); + holder.binding.actionIcon.setBackground(null); + } + @Override public int getItemCount() { return actions.size(); } } - private final ContactActionAdapter adapter = new ContactActionAdapter(); + static class ContactView extends RecyclerView.ViewHolder { + final ItemContactHorizontalBinding binding; + IContactAction callback; + final CompositeDisposable disposable = new CompositeDisposable(); + + ContactView(@NonNull ItemContactHorizontalBinding b, CompositeDisposable parentDisposable) { + super(b.getRoot()); + binding = b; + parentDisposable.add(disposable); + itemView.setOnClickListener(view -> { + try { + if (callback != null) + callback.onAction(); + } catch (Exception e) { + Log.w(TAG, "Error performing action", e); + } + }); + } + } + private static class ContactViewAdapter extends RecyclerView.Adapter<ContactView> { + private final List<Contact> contacts; + private final CompositeDisposable disposable; + interface ContactCallback { + void onContactClicked(Contact contact); + } + private final ContactCallback callback; + + private ContactViewAdapter(CompositeDisposable disposable, List<Contact> contacts, ContactCallback cb) { + this.disposable = disposable; + this.contacts = contacts; + this.callback = cb; + } + + @NonNull + @Override + public ContactView onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext()); + ItemContactHorizontalBinding itemBinding = ItemContactHorizontalBinding.inflate(layoutInflater, parent, false); + return new ContactView(itemBinding, disposable); + } + + @Override + public void onBindViewHolder(@NonNull ContactView holder, int position) { + Contact contact = contacts.get(position); + holder.disposable.clear(); + holder.disposable.add(AvatarFactory.getAvatar(holder.itemView.getContext(), contact, false).subscribe(holder.binding.photo::setImageDrawable)); + holder.binding.displayName.setText(contact.isUser() ? holder.itemView.getContext().getText(R.string.conversation_info_contact_you) : contact.getDisplayName()); + holder.itemView.setOnClickListener(v -> callback.onContactClicked(contact)); + } + + @Override + public void onViewRecycled(@NonNull ContactView holder) { + holder.disposable.clear(); + holder.binding.photo.setImageDrawable(null); + } + + @Override + public int getItemCount() { + return contacts.size(); + } + } + private final CompositeDisposable mDisposableBag = new CompositeDisposable(); + private final ContactActionAdapter adapter = new ContactActionAdapter(mDisposableBag); private ContactAction colorAction; private ContactAction symbolAction; @@ -194,7 +290,7 @@ public class ContactDetailsActivity extends AppCompatActivity { setSupportActionBar(findViewById(R.id.toolbar)); - FloatingActionButton fab = findViewById(R.id.fab); + FloatingActionButton fab = findViewById(R.id.sendMessage); fab.setOnClickListener(view -> goToConversationActivity(mConversation.getAccountId(), mConversation.getUri())); colorActionPosition = 1; @@ -205,7 +301,7 @@ public class ContactDetailsActivity extends AppCompatActivity { .observeOn(AndroidSchedulers.mainThread()) .subscribe(conversation -> { mConversation = conversation; - mPreferences = getSharedPreferences(conversation.getAccountId() + "_" + conversation.getUri().getUri(), Context.MODE_PRIVATE); + mPreferences = SharedPreferencesServiceImpl.getConversationPreferences(this, conversation.getAccountId(), conversation.getUri()); binding.contactImage.setImageDrawable( new AvatarDrawable.Builder() .withConversation(conversation) @@ -221,10 +317,11 @@ public class ContactDetailsActivity extends AppCompatActivity { @StringRes int infoString = conversation.isSwarm() ? (conversation.getMode() == Conversation.Mode.OneToOne - ? R.string.conversation_type_private - : R.string.conversation_type_group) + ? R.string.conversation_type_private + : R.string.conversation_type_group) : R.string.conversation_type_contact; - adapter.actions.add(new ContactAction(R.drawable.baseline_info_24, getText(infoString), () -> {})); + adapter.actions.add(new ContactAction(R.drawable.baseline_info_24, getText(infoString), () -> { + })); colorAction = new ContactAction(R.drawable.item_color_background, 0, getText(R.string.conversation_preference_color), () -> { ColorChooserBottomSheet frag = new ColorChooserBottomSheet(); @@ -288,7 +385,8 @@ public class ContactDetailsActivity extends AppCompatActivity { .create() .show())); } - contactAction = new ContactAction(R.drawable.baseline_person_24, conversation.getUriTitle(), () -> { + String conversationUri = conversation.isSwarm() ? conversation.getUri().toString() : conversation.getUriTitle(); + contactAction = new ContactAction(conversation.isSwarm() ? R.drawable.baseline_group_24 : R.drawable.baseline_person_24, conversationUri, () -> { ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); if (clipboard != null) { clipboard.setPrimaryClip(ClipData.newPlainText(getText(R.string.clip_contact_uri), path.getConversationId())); @@ -297,6 +395,18 @@ public class ContactDetailsActivity extends AppCompatActivity { }); adapter.actions.add(contactAction); binding.contactActionList.setAdapter(adapter); + + binding.contactList.setVisibility(conversation.isSwarm() ? View.VISIBLE : View.GONE); + if (conversation.isSwarm()) { + binding.contactList.setAdapter(new ContactViewAdapter(mDisposableBag, conversation.getContacts(), contact -> { + ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); + if (clipboard != null) { + String toCopy = contact.getUri().getRawUriString(); + clipboard.setPrimaryClip(ClipData.newPlainText(getText(R.string.clip_contact_uri), toCopy)); + Snackbar.make(binding.getRoot(), getString(R.string.conversation_action_copied_peer_number_clipboard, toCopy), Snackbar.LENGTH_LONG).show(); + } + })); + } })); } diff --git a/ring-android/app/src/main/java/cx/ring/client/ConversationActivity.java b/ring-android/app/src/main/java/cx/ring/client/ConversationActivity.java index 0474329f7..d10cc5430 100644 --- a/ring-android/app/src/main/java/cx/ring/client/ConversationActivity.java +++ b/ring-android/app/src/main/java/cx/ring/client/ConversationActivity.java @@ -24,14 +24,11 @@ import android.content.Intent; import android.os.Bundle; import android.view.KeyEvent; import android.view.Menu; -import android.view.View; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; -import androidx.coordinatorlayout.widget.CoordinatorLayout; -import androidx.core.view.ViewCompat; import cx.ring.R; import cx.ring.application.JamiApplication; @@ -85,7 +82,7 @@ public class ConversationActivity extends AppCompatActivity implements Colorable mConversationFragment.setArguments(bundle); getSupportFragmentManager().beginTransaction() .replace(R.id.main_frame, mConversationFragment, null) - .commit(); + .commitNow(); } if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action) || Intent.ACTION_VIEW.equals(action)) { mPendingIntent = intent; diff --git a/ring-android/app/src/main/java/cx/ring/client/ConversationSelectionActivity.java b/ring-android/app/src/main/java/cx/ring/client/ConversationSelectionActivity.java index f9062b4ee..3f464ecfc 100644 --- a/ring-android/app/src/main/java/cx/ring/client/ConversationSelectionActivity.java +++ b/ring-android/app/src/main/java/cx/ring/client/ConversationSelectionActivity.java @@ -117,11 +117,11 @@ public class ConversationSelectionActivity extends AppCompatActivity { return vm; List<SmartListViewModel> filteredVms = new ArrayList<>(vm.size()); models: for (SmartListViewModel v : vm) { - List<Contact> contacts = v.getContact(); + List<Contact> contacts = v.getContacts(); if (contacts.size() != 1) continue; for (Call call : conf.getParticipants()) { - if (call.getContact() == v.getContact().get(0)) { + if (call.getContact() == v.getContacts().get(0)) { continue models; } } diff --git a/ring-android/app/src/main/java/cx/ring/fragments/CallFragment.java b/ring-android/app/src/main/java/cx/ring/fragments/CallFragment.java index 6e84813e1..687e76f29 100644 --- a/ring-android/app/src/main/java/cx/ring/fragments/CallFragment.java +++ b/ring-android/app/src/main/java/cx/ring/fragments/CallFragment.java @@ -92,6 +92,7 @@ import net.jami.daemon.JamiService; import net.jami.model.Call; import net.jami.model.Conference; import net.jami.model.Contact; +import net.jami.model.Uri; import net.jami.services.DeviceRuntimeService; import net.jami.services.HardwareService; import net.jami.services.NotificationService; @@ -1237,7 +1238,7 @@ public class CallFragment extends BaseSupportFragment<CallPresenter> implements } @Override - public void goToConversation(String accountId, String conversationId) { + public void goToConversation(String accountId, Uri conversationId) { Context context = requireContext(); if (DeviceUtils.isTablet(context)) { startActivity(new Intent(DRingService.ACTION_CONV_ACCEPT, ConversationPath.toUri(accountId, conversationId), context, HomeActivity.class)); diff --git a/ring-android/app/src/main/java/cx/ring/fragments/ContactPickerFragment.java b/ring-android/app/src/main/java/cx/ring/fragments/ContactPickerFragment.java index 19cd9c897..9e61d435c 100644 --- a/ring-android/app/src/main/java/cx/ring/fragments/ContactPickerFragment.java +++ b/ring-android/app/src/main/java/cx/ring/fragments/ContactPickerFragment.java @@ -107,7 +107,7 @@ public class ContactPickerFragment extends BottomSheetDialogFragment { adapter.update(item); Runnable remover = () -> { - mCurrentSelection.remove(item.getContact().get(0)); + mCurrentSelection.remove(item.getContacts().get(0)); if (mCurrentSelection.isEmpty()) binding.createGroupBtn.setEnabled(false); item.setChecked(false); @@ -118,7 +118,7 @@ public class ContactPickerFragment extends BottomSheetDialogFragment { }; if (checked) { - if (mCurrentSelection.add(item.getContact().get(0))) { + if (mCurrentSelection.add(item.getContacts().get(0))) { Chip chip = new Chip(requireContext(), null, R.style.Widget_MaterialComponents_Chip_Entry); chip.setText(item.getContactName()); chip.setChipIcon(new AvatarDrawable.Builder() diff --git a/ring-android/app/src/main/java/cx/ring/fragments/ConversationFragment.java b/ring-android/app/src/main/java/cx/ring/fragments/ConversationFragment.java index 1b4cb7af1..14dfae4d0 100644 --- a/ring-android/app/src/main/java/cx/ring/fragments/ConversationFragment.java +++ b/ring-android/app/src/main/java/cx/ring/fragments/ConversationFragment.java @@ -102,6 +102,7 @@ import cx.ring.service.DRingService; import cx.ring.services.LocationSharingService; import net.jami.services.NotificationService; import cx.ring.services.NotificationServiceImpl; +import cx.ring.services.SharedPreferencesServiceImpl; import cx.ring.utils.ActionHelper; import cx.ring.utils.AndroidFileUtils; import cx.ring.utils.ContentUriHandler; @@ -248,7 +249,7 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen View layout = binding.conversationLayout; // remove action bar height for tablet layout - if (DeviceUtils.isTablet(getContext())) { + if (DeviceUtils.isTablet(layout.getContext())) { layout.setPadding(layout.getPaddingLeft(), 0, layout.getPaddingRight(), layout.getPaddingBottom()); } @@ -799,7 +800,7 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen try { fileUri = ContentUriHandler.getUriForFile(c, ContentUriHandler.AUTHORITY_FILES, path); } catch (IllegalArgumentException e) { - Log.e("File Selector", "The selected file can't be shared: " + path.getName()); + Log.e(TAG, "The selected file can't be shared: " + path.getName()); } if (fileUri != null) { Intent sendIntent = new Intent(); @@ -861,28 +862,28 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen @Override public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - startActivity(new Intent(getActivity(), HomeActivity.class)); - return true; - case R.id.conv_action_audiocall: - presenter.goToCall(true); - return true; - case R.id.conv_action_videocall: - presenter.goToCall(false); - return true; - case R.id.conv_contact_details: - presenter.openContact(); - return true; - default: - return super.onOptionsItemSelected(item); + int itemId = item.getItemId(); + if (itemId == android.R.id.home) { + startActivity(new Intent(getActivity(), HomeActivity.class)); + return true; + } else if (itemId == R.id.conv_action_audiocall) { + presenter.goToCall(true); + return true; + } else if (itemId == R.id.conv_action_videocall) { + presenter.goToCall(false); + return true; + } else if (itemId == R.id.conv_contact_details) { + presenter.openContact(); + return true; } + return super.onOptionsItemSelected(item); } @Override protected void initPresenter(ConversationPresenter presenter) { ConversationPath path = ConversationPath.fromBundle(getArguments()); mIsBubble = getArguments().getBoolean(NotificationServiceImpl.EXTRA_BUBBLE); + Log.w(TAG, "initPresenter " + path); if (path == null) return; @@ -890,7 +891,7 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen mAdapter = new ConversationAdapter(this, presenter); presenter.init(uri, path.getAccountId()); try { - mPreferences = requireActivity().getSharedPreferences(path.getAccountId() + "_" + uri.getUri(), Context.MODE_PRIVATE); + mPreferences = SharedPreferencesServiceImpl.getConversationPreferences(requireContext(), path.getAccountId(), uri); mPreferences.registerOnSharedPreferenceChangeListener(this); presenter.setConversationColor(mPreferences.getInt(KEY_PREFERENCE_CONVERSATION_COLOR, getResources().getColor(R.color.color_primary_light))); presenter.setConversationSymbol(mPreferences.getString(KEY_PREFERENCE_CONVERSATION_SYMBOL, getResources().getText(R.string.conversation_default_emoji).toString())); @@ -955,7 +956,11 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen .observeOn(AndroidSchedulers.mainThread()) .subscribe(avatar -> { mParticipantAvatars.put(contactKey, (AvatarDrawable) avatar); - mSmallParticipantAvatars.put(contactKey, new AvatarDrawable((AvatarDrawable) avatar)); + mSmallParticipantAvatars.put(contactKey, new AvatarDrawable.Builder() + .withContact(contact) + .withCircleCrop(true) + .withPresence(false) + .build(requireContext())); mAdapter.setPhoto(); })); } @@ -996,6 +1001,7 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen @Override public void goToHome() { + if (getActivity() instanceof ConversationActivity) { getActivity().finish(); } diff --git a/ring-android/app/src/main/java/cx/ring/fragments/LinkDeviceFragment.java b/ring-android/app/src/main/java/cx/ring/fragments/LinkDeviceFragment.java index c2a7e5806..5e04bac96 100644 --- a/ring-android/app/src/main/java/cx/ring/fragments/LinkDeviceFragment.java +++ b/ring-android/app/src/main/java/cx/ring/fragments/LinkDeviceFragment.java @@ -43,13 +43,14 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout; import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import cx.ring.R; -import cx.ring.account.AccountEditionFragment; import net.jami.account.LinkDevicePresenter; import net.jami.account.LinkDeviceView; +import net.jami.model.Account; + +import cx.ring.R; +import cx.ring.account.AccountEditionFragment; import cx.ring.application.JamiApplication; import cx.ring.databinding.FragLinkDeviceBinding; -import net.jami.model.Account; import cx.ring.mvp.BaseBottomSheetFragment; import cx.ring.utils.DeviceUtils; import cx.ring.utils.KeyboardVisibilityManager; @@ -76,10 +77,8 @@ public class LinkDeviceFragment extends BaseBottomSheetFragment<LinkDevicePresen @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { super.onCreateView(inflater, container, savedInstanceState); - - ((JamiApplication) getActivity().getApplication()).getInjectionComponent().inject(this); + ((JamiApplication) requireActivity().getApplication()).getInjectionComponent().inject(this); mBinding = FragLinkDeviceBinding.inflate(inflater, container, false); - return mBinding.getRoot(); } @@ -110,7 +109,7 @@ public class LinkDeviceFragment extends BaseBottomSheetFragment<LinkDevicePresen public Dialog onCreateDialog(Bundle savedInstanceState) { final Dialog dialog = super.onCreateDialog(savedInstanceState); dialog.setOnShowListener(dialogINterface -> { - if (DeviceUtils.isTablet(getContext())) { + if (DeviceUtils.isTablet(requireContext())) { dialog.getWindow().setLayout( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT); @@ -122,7 +121,7 @@ public class LinkDeviceFragment extends BaseBottomSheetFragment<LinkDevicePresen @Override public void onResume() { super.onResume(); - addGlobalLayoutListener(getView()); + addGlobalLayoutListener(requireView()); } private void addGlobalLayoutListener(final View view) { @@ -145,12 +144,11 @@ public class LinkDeviceFragment extends BaseBottomSheetFragment<LinkDevicePresen } private BottomSheetBehavior<?> getBottomSheetBehaviour() { - CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) ((View) getView().getParent()).getLayoutParams(); + CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) ((View) requireView().getParent()).getLayoutParams(); CoordinatorLayout.Behavior<?> behavior = layoutParams.getBehavior(); if (behavior instanceof BottomSheetBehavior) { return (BottomSheetBehavior<?>) behavior; } - return null; } diff --git a/ring-android/app/src/main/java/cx/ring/fragments/SmartListFragment.java b/ring-android/app/src/main/java/cx/ring/fragments/SmartListFragment.java index d3ef98517..4bb7dd86f 100644 --- a/ring-android/app/src/main/java/cx/ring/fragments/SmartListFragment.java +++ b/ring-android/app/src/main/java/cx/ring/fragments/SmartListFragment.java @@ -27,21 +27,6 @@ import android.content.Intent; import android.net.Uri; import android.os.Build; import android.os.Bundle; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton; -import com.google.android.material.snackbar.Snackbar; - -import androidx.recyclerview.widget.DefaultItemAnimator; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import androidx.appcompat.widget.SearchView; -import androidx.appcompat.widget.Toolbar; - -import android.os.Handler; import android.text.InputType; import android.util.Log; import android.util.TypedValue; @@ -55,6 +40,24 @@ import android.view.inputmethod.EditorInfo; import android.widget.EditText; import android.widget.RelativeLayout; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.SearchView; +import androidx.appcompat.widget.Toolbar; +import androidx.recyclerview.widget.DefaultItemAnimator; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton; +import com.google.android.material.snackbar.Snackbar; + +import net.jami.model.Conversation; +import net.jami.services.AccountService; +import net.jami.smartlist.SmartListPresenter; +import net.jami.smartlist.SmartListView; +import net.jami.smartlist.SmartListViewModel; + import java.util.List; import javax.inject.Inject; @@ -65,12 +68,7 @@ import cx.ring.application.JamiApplication; import cx.ring.client.CallActivity; import cx.ring.client.HomeActivity; import cx.ring.databinding.FragSmartlistBinding; -import net.jami.model.Conversation; import cx.ring.mvp.BaseSupportFragment; -import net.jami.services.AccountService; -import net.jami.smartlist.SmartListPresenter; -import net.jami.smartlist.SmartListView; -import net.jami.smartlist.SmartListViewModel; import cx.ring.utils.ActionHelper; import cx.ring.utils.ClipboardHelper; import cx.ring.utils.ConversationPath; @@ -80,12 +78,9 @@ import cx.ring.viewholders.SmartListViewHolder; public class SmartListFragment extends BaseSupportFragment<SmartListPresenter> implements SearchView.OnQueryTextListener, SmartListViewHolder.SmartListListeners, Conversation.ConversationActionCallback, - ClipboardHelper.ClipboardHelperCallback, - SmartListView -{ + SmartListView { private static final String TAG = SmartListFragment.class.getSimpleName(); private static final String STATE_LOADING = TAG + ".STATE_LOADING"; - public static final String KEY_ACCOUNT_ID = "accountId"; private static final int SCROLL_DIRECTION_UP = -1; @@ -148,7 +143,7 @@ public class SmartListFragment extends BaseSupportFragment<SmartListPresenter> i @Override public void onStart() { super.onStart(); - Activity activity = getActivity(); + Activity activity = getActivity(); Intent intent = activity == null ? null : activity.getIntent(); if (intent != null) handleIntent(intent); @@ -177,30 +172,29 @@ public class SmartListFragment extends BaseSupportFragment<SmartListPresenter> i @Override public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.menu_contact_search: - mSearchView.setInputType(EditorInfo.TYPE_CLASS_TEXT - | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS - ); - return false; - case R.id.menu_contact_dial: - if (mSearchView.getInputType() == EditorInfo.TYPE_CLASS_PHONE) { - mSearchView.setInputType(EditorInfo.TYPE_CLASS_TEXT); - mDialpadMenuItem.setIcon(R.drawable.baseline_dialpad_24); - } else { - mSearchView.setInputType(EditorInfo.TYPE_CLASS_PHONE); - mDialpadMenuItem.setIcon(R.drawable.baseline_keyboard_24); - } - return true; - case R.id.menu_settings: - ((HomeActivity) getActivity()).goToSettings(); - return true; - case R.id.menu_about: - ((HomeActivity) getActivity()).goToAbout(); - return true; - default: - return false; + int itemId = item.getItemId(); + if (itemId == R.id.menu_contact_search) { + mSearchView.setInputType(EditorInfo.TYPE_CLASS_TEXT + | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS + ); + return false; + } else if (itemId == R.id.menu_contact_dial) { + if (mSearchView.getInputType() == EditorInfo.TYPE_CLASS_PHONE) { + mSearchView.setInputType(EditorInfo.TYPE_CLASS_TEXT); + mDialpadMenuItem.setIcon(R.drawable.baseline_dialpad_24); + } else { + mSearchView.setInputType(EditorInfo.TYPE_CLASS_PHONE); + mDialpadMenuItem.setIcon(R.drawable.baseline_keyboard_24); + } + return true; + } else if (itemId == R.id.menu_settings) { + ((HomeActivity) requireActivity()).goToSettings(); + return true; + } else if (itemId == R.id.menu_about) { + ((HomeActivity) requireActivity()).goToAbout(); + return true; } + return false; } @Override @@ -228,7 +222,7 @@ public class SmartListFragment extends BaseSupportFragment<SmartListPresenter> i @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { binding = FragSmartlistBinding.inflate(inflater, container, false); - ((JamiApplication) getActivity().getApplication()).getInjectionComponent().inject(this); + ((JamiApplication) requireActivity().getApplication()).getInjectionComponent().inject(this); return binding.getRoot(); } @@ -244,15 +238,9 @@ public class SmartListFragment extends BaseSupportFragment<SmartListPresenter> i super.onViewCreated(view, savedInstanceState); binding.qrCode.setOnClickListener(v -> presenter.clickQRSearch()); - //binding.newGroup.setOnClickListener(v -> startNewGroup()); binding.confsList.addOnScrollListener(new RecyclerView.OnScrollListener() { - @Override - public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { - super.onScrollStateChanged(recyclerView, newState); - } - @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { boolean canScrollUp = recyclerView.canScrollVertically(SCROLL_DIRECTION_UP); @@ -307,8 +295,8 @@ public class SmartListFragment extends BaseSupportFragment<SmartListPresenter> i } @Override - public void removeConversation(net.jami.model.Uri callContact) { - presenter.removeConversation(callContact); + public void removeConversation(net.jami.model.Uri conversationUri) { + presenter.removeConversation(conversationUri); } @Override @@ -318,13 +306,9 @@ public class SmartListFragment extends BaseSupportFragment<SmartListPresenter> i @Override public void copyContactNumberToClipboard(String contactNumber) { - ClipboardHelper.copyNumberToClipboard(getActivity(), contactNumber, this); - } - - @Override - public void clipBoardDidCopyNumber(String copiedNumber) { + ClipboardHelper.copyToClipboard(requireContext(), contactNumber); String snackbarText = getString(R.string.conversation_action_copied_peer_number_clipboard, - ActionHelper.getShortenedNumber(copiedNumber)); + ActionHelper.getShortenedNumber(contactNumber)); Snackbar.make(binding.listCoordinator, snackbarText, Snackbar.LENGTH_LONG).show(); } @@ -496,7 +480,7 @@ public class SmartListFragment extends BaseSupportFragment<SmartListPresenter> i if (binding == null || binding.separator == null) return; - if (DeviceUtils.isTablet(getContext())) { + if (DeviceUtils.isTablet(binding.getRoot().getContext())) { int margin = 0; if (open) { @@ -514,21 +498,9 @@ public class SmartListFragment extends BaseSupportFragment<SmartListPresenter> i } } - private void selectFirstItem() { - if (mSmartListAdapter != null && mSmartListAdapter.getItemCount() > 0) { - new Handler().postDelayed(() -> { - if (binding != null) { - RecyclerView.ViewHolder holder = binding.confsList.findViewHolderForAdapterPosition(0); - if (holder != null) - holder.itemView.performClick(); - } - - }, 100); - } - } - private void setTabletQRLayout(boolean show) { - if (!DeviceUtils.isTablet(getContext())) + Context context = requireContext(); + if (!DeviceUtils.isTablet(context)) return; RelativeLayout.LayoutParams params = @@ -539,8 +511,8 @@ public class SmartListFragment extends BaseSupportFragment<SmartListPresenter> i } else { params.removeRule(RelativeLayout.BELOW); TypedValue value = new TypedValue(); - if (getActivity().getTheme().resolveAttribute(android.R.attr.actionBarSize, value, true)) { - params.topMargin = TypedValue.complexToDimensionPixelSize(value.data, getResources().getDisplayMetrics()); + if (context.getTheme().resolveAttribute(android.R.attr.actionBarSize, value, true)) { + params.topMargin = TypedValue.complexToDimensionPixelSize(value.data, context.getResources().getDisplayMetrics()); } } binding.listCoordinator.setLayoutParams(params); diff --git a/ring-android/app/src/main/java/cx/ring/service/CallNotificationService.java b/ring-android/app/src/main/java/cx/ring/service/CallNotificationService.java index a9d595ac1..f78049766 100644 --- a/ring-android/app/src/main/java/cx/ring/service/CallNotificationService.java +++ b/ring-android/app/src/main/java/cx/ring/service/CallNotificationService.java @@ -32,12 +32,13 @@ import androidx.annotation.Nullable; import javax.inject.Inject; import cx.ring.application.JamiApplication; +import cx.ring.services.NotificationServiceImpl; + import net.jami.services.NotificationService; public class CallNotificationService extends Service { public static final String ACTION_START = "START"; public static final String ACTION_STOP = "STOP"; - private static final int NOTIF_CALL_ID = 1001; @Inject NotificationService mNotificationService; @@ -55,13 +56,14 @@ public class CallNotificationService extends Service { Notification notification = (Notification) mNotificationService.showCallNotification(intent.getIntExtra(NotificationService.KEY_NOTIFICATION_ID, -1)); if (notification != null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) - startForeground(NOTIF_CALL_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL | ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION); + startForeground(NotificationServiceImpl.NOTIF_CALL_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL | ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION); else - startForeground(NOTIF_CALL_ID, notification); + startForeground(NotificationServiceImpl.NOTIF_CALL_ID, notification); } } else if (ACTION_STOP.equals(intent.getAction())) { stopForeground(true); stopSelf(); + mNotificationService.cancelCallNotification(); } return START_NOT_STICKY; } diff --git a/ring-android/app/src/main/java/cx/ring/service/DRingService.java b/ring-android/app/src/main/java/cx/ring/service/DRingService.java index 32ff9859b..319115791 100644 --- a/ring-android/app/src/main/java/cx/ring/service/DRingService.java +++ b/ring-android/app/src/main/java/cx/ring/service/DRingService.java @@ -654,6 +654,7 @@ public class DRingService extends Service { long id = extras.getLong(KEY_TRANSFER_ID); ConversationPath path = ConversationPath.fromUri(uri); if (action.equals(ACTION_FILE_ACCEPT)) { + mNotificationService.removeTransferNotification(path.getAccountId(), path.getConversationUri(), id); mAccountService.acceptFileTransfer(path.getAccountId(), path.getConversationUri(), id); } else if (action.equals(ACTION_FILE_CANCEL)) { mConversationFacade.cancelFileTransfer(path.getAccountId(), path.getConversationUri(), id); diff --git a/ring-android/app/src/main/java/cx/ring/services/DataTransferService.java b/ring-android/app/src/main/java/cx/ring/services/DataTransferService.java index 5e3654060..f8974fe85 100644 --- a/ring-android/app/src/main/java/cx/ring/services/DataTransferService.java +++ b/ring-android/app/src/main/java/cx/ring/services/DataTransferService.java @@ -25,6 +25,7 @@ import android.content.Intent; import android.content.pm.ServiceInfo; import android.os.Build; import android.os.IBinder; +import android.util.Log; import androidx.annotation.Nullable; import androidx.core.app.NotificationManagerCompat; @@ -34,58 +35,72 @@ import javax.inject.Inject; import cx.ring.application.JamiApplication; import net.jami.services.NotificationService; -import net.jami.utils.Log; + +import java.util.HashSet; +import java.util.Set; public class DataTransferService extends Service { private final String TAG = DataTransferService.class.getSimpleName(); + public static final String ACTION_START = "startTransfer"; + public static final String ACTION_STOP = "stopTransfer"; + private static final int NOTIF_FILE_SERVICE_ID = 1002; @Inject NotificationService mNotificationService; private NotificationManagerCompat notificationManager; + private boolean started = false; - private boolean isFirst = true; - private static final int NOTIF_FILE_SERVICE_ID = 1002; - private int serviceNotificationId; + private int serviceNotificationId = 0; + private final Set<Integer> serviceNotifications = new HashSet<>(); @Override public int onStartCommand(Intent intent, int flags, int startId) { - super.onStartCommand(intent, flags, startId); - int notificationId = intent.getIntExtra(NotificationService.KEY_NOTIFICATION_ID, -1); - Notification notification = (Notification) mNotificationService.getDataTransferNotification(notificationId); - - if (notification == null) { - stopSelf(); - return START_NOT_STICKY; + String action = intent.getAction(); + if (ACTION_START.equals(action)) { + serviceNotifications.add(notificationId); + Notification notification = (Notification) mNotificationService.getDataTransferNotification(notificationId); + // Log.w(TAG, "Updating notification " + intent); + if (!started) { + Log.w(TAG, "starting transfer service " + intent); + serviceNotificationId = notificationId; + started = true; + } + if (notificationId == serviceNotificationId) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) + startForeground(NOTIF_FILE_SERVICE_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC); + else + startForeground(NOTIF_FILE_SERVICE_ID, notification); + } else { + notificationManager.notify(notificationId, notification); + } } - - //if (isFirst) { - // isFirst = false; - mNotificationService.cancelFileNotification(notificationId, true); - serviceNotificationId = notificationId; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) - startForeground(NOTIF_FILE_SERVICE_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC); - else - startForeground(NOTIF_FILE_SERVICE_ID, notification); - //} - - if (mNotificationService.getDataTransferNotification(serviceNotificationId) == null) { - mNotificationService.cancelFileNotification(notificationId, true); - serviceNotificationId = notificationId; + else if (ACTION_STOP.equals(action)) { + serviceNotifications.remove(notificationId); + if (notificationId == serviceNotificationId) { + // The service notification is removed. Migrate service to other notification or stop it + serviceNotificationId = serviceNotifications.isEmpty() ? 0 : serviceNotifications.iterator().next(); + if (serviceNotificationId == 0) { + Log.w(TAG, "stopping transfer service " + intent); + stopForeground(true); + stopSelf(); + started = false; + } else { + // migrate notification to service + notificationManager.cancel(serviceNotificationId); + Notification notification = (Notification) mNotificationService.getDataTransferNotification(serviceNotificationId); + notificationManager.notify(NOTIF_FILE_SERVICE_ID, notification); + } + } else { + notificationManager.cancel(notificationId); + } } - - if (notificationId == serviceNotificationId) - notificationManager.notify(NOTIF_FILE_SERVICE_ID, notification); - else - notificationManager.notify(notificationId, notification); - - return START_NOT_STICKY; } @Override public void onCreate() { - Log.d(TAG, "OnCreate(), Service has been initialized"); + Log.d(TAG, "OnCreate(), DataTransferService has been initialized"); ((JamiApplication) getApplication()).getInjectionComponent().inject(this); notificationManager = NotificationManagerCompat.from(this); super.onCreate(); @@ -94,7 +109,7 @@ public class DataTransferService extends Service { @Override public void onDestroy() { - Log.d(TAG, "OnDestroy(), Service has been destroyed"); + Log.d(TAG, "OnDestroy(), DataTransferService has been destroyed"); super.onDestroy(); } diff --git a/ring-android/app/src/main/java/cx/ring/services/DeviceRuntimeServiceImpl.java b/ring-android/app/src/main/java/cx/ring/services/DeviceRuntimeServiceImpl.java index f5adc8df1..5467f4b9d 100644 --- a/ring-android/app/src/main/java/cx/ring/services/DeviceRuntimeServiceImpl.java +++ b/ring-android/app/src/main/java/cx/ring/services/DeviceRuntimeServiceImpl.java @@ -28,8 +28,9 @@ import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.Build; import android.provider.ContactsContract; +import android.system.ErrnoException; +import android.system.Os; import android.util.Log; -import android.widget.Toast; import androidx.core.content.ContextCompat; @@ -46,6 +47,7 @@ import cx.ring.utils.AndroidFileUtils; import cx.ring.utils.NetworkUtils; import net.jami.services.DeviceRuntimeService; +import net.jami.utils.FileUtils; import net.jami.utils.StringUtils; public class DeviceRuntimeServiceImpl extends DeviceRuntimeService { @@ -100,6 +102,11 @@ public class DeviceRuntimeServiceImpl extends DeviceRuntimeService { return AndroidFileUtils.getTempPath(mContext, conversationId, name); } + @Override + public File getConversationDir(String conversationId) { + return AndroidFileUtils.getConversationDir(mContext, conversationId); + } + @Override public File getCacheDir() { return mContext.getCacheDir(); @@ -179,6 +186,17 @@ public class DeviceRuntimeServiceImpl extends DeviceRuntimeService { return null; } + @Override + public boolean hardLinkOrCopy(File source, File dest) { + try { + Os.link(source.getAbsolutePath(), dest.getAbsolutePath()); + } catch (ErrnoException e) { + Log.w(TAG, "Can't create hardlink, copying instead: " + e.getMessage()); + return FileUtils.copyFile(source, dest); + } + return true; + } + private boolean checkPermission(String permission) { return ContextCompat.checkSelfPermission(mContext, permission) == PackageManager.PERMISSION_GRANTED; } diff --git a/ring-android/app/src/main/java/cx/ring/services/NotificationServiceImpl.java b/ring-android/app/src/main/java/cx/ring/services/NotificationServiceImpl.java index d7e19aef8..0a833c99b 100644 --- a/ring-android/app/src/main/java/cx/ring/services/NotificationServiceImpl.java +++ b/ring-android/app/src/main/java/cx/ring/services/NotificationServiceImpl.java @@ -118,7 +118,7 @@ public class NotificationServiceImpl implements NotificationService { private static final String NOTIF_CALL_GROUP = "calls"; - private static final int NOTIF_CALL_ID = 1001; + public static final int NOTIF_CALL_ID = 1001; private final SparseArray<NotificationCompat.Builder> mNotificationBuilders = new SparseArray<>(); @Inject @@ -133,10 +133,11 @@ public class NotificationServiceImpl implements NotificationService { protected HistoryService mHistoryService; @Inject protected DeviceRuntimeService mDeviceRuntimeService; + private NotificationManagerCompat notificationManager; private final Random random = new Random(); private int avatarSize; - private final LinkedHashMap<Integer, Conference> currentCalls = new LinkedHashMap<>(); + private final LinkedHashMap<String, Conference> currentCalls = new LinkedHashMap<>(); private final ConcurrentHashMap<Integer, Notification> callNotifications = new ConcurrentHashMap<>(); private final ConcurrentHashMap<Integer, Notification> dataTransferNotifications = new ConcurrentHashMap<>(); @@ -249,9 +250,6 @@ public class NotificationServiceImpl implements NotificationService { } Call call = conference.getParticipants().get(0); - - notificationManager.cancel(NOTIF_CALL_ID); - PendingIntent gotoIntent = PendingIntent.getService(mContext, random.nextInt(), new Intent(DRingService.ACTION_CALL_VIEW) @@ -269,6 +267,8 @@ public class NotificationServiceImpl implements NotificationService { .setSound(null) .setVibrate(null) .setColorized(true) + .setUsesChronometer(true) + .setWhen(conference.getTimestampStart()) .setColor(mContext.getResources().getColor(R.color.color_primary_light)) .addAction(R.drawable.baseline_call_end_24, mContext.getText(R.string.action_call_hangup), PendingIntent.getService(mContext, random.nextInt(), @@ -409,7 +409,7 @@ public class NotificationServiceImpl implements NotificationService { Notification notification = null; // Build notification - int id = conference.getId().hashCode(); + String id = conference.getId(); currentCalls.remove(id); if (!remove) { currentCalls.put(id, conference); @@ -464,31 +464,37 @@ public class NotificationServiceImpl implements NotificationService { */ @Override public void handleDataTransferNotification(DataTransfer transfer, Conversation conversation, boolean remove) { - Log.d(TAG, "handleDataTransferNotification, a data transfer event is in progress"); + Log.d(TAG, "handleDataTransferNotification, a data transfer event is in progress " + remove); if (DeviceUtils.isTv(mContext)) { return; } if (!remove) { showFileTransferNotification(conversation, transfer); } else { - removeTransferNotification(transfer.getDaemonId()); + removeTransferNotification(ConversationPath.toUri(conversation), transfer.getDaemonId()); } } + @Override + public void removeTransferNotification(String accountId, Uri conversationUri, long transferId) { + removeTransferNotification(ConversationPath.toUri(accountId, conversationUri), transferId); + } + /** * Cancels a data transfer notification and removes it from the list of notifications * * @param transferId the transfer id which is required to generate the notification id */ - @Override - public void removeTransferNotification(long transferId) { - int id = getFileTransferNotificationId(transferId); + public void removeTransferNotification(android.net.Uri path, long transferId) { + int id = getFileTransferNotificationId(path, transferId); dataTransferNotifications.remove(id); cancelFileNotification(id, false); - if (dataTransferNotifications.isEmpty()) - mContext.stopService(new Intent(mContext, DataTransferService.class)); - else { - startForegroundService(dataTransferNotifications.keySet().iterator().next(), DataTransferService.class); + if (dataTransferNotifications.isEmpty()) { + mContext.startService(new Intent(DataTransferService.ACTION_STOP, path, mContext, DataTransferService.class) + .putExtra(KEY_NOTIFICATION_ID, id)); + } else { + ContextCompat.startForegroundService(mContext, new Intent(DataTransferService.ACTION_STOP, path, mContext, DataTransferService.class) + .putExtra(KEY_NOTIFICATION_ID, id)); } } @@ -505,11 +511,11 @@ public class NotificationServiceImpl implements NotificationService { public void showTextNotification(String accountId, Conversation conversation) { TreeMap<Long, TextMessage> texts = conversation.getUnreadTextMessages(); - Log.w(TAG, "showTextNotification start " + accountId + " " + conversation.getUri() + " " + texts.size()); + //Log.w(TAG, "showTextNotification start " + accountId + " " + conversation.getUri() + " " + texts.size()); //TODO handle groups if (texts.isEmpty() || conversation.isVisible()) { - cancelTextNotification(conversation.getUri()); + cancelTextNotification(conversation.getAccountId(), conversation.getUri()); return; } if (texts.lastEntry().getValue().isNotified()) { @@ -523,7 +529,8 @@ public class NotificationServiceImpl implements NotificationService { } private void textNotification(String accountId, TreeMap<Long, TextMessage> texts, Conversation conversation) { - android.net.Uri path = ConversationPath.toUri(conversation.getAccountId(), conversation.getUri()); + ConversationPath cpath = new ConversationPath(conversation); + android.net.Uri path = cpath.toUri(); Pair<Bitmap, String> conversationProfile = getProfile(conversation); int notificationVisibility = mPreferencesService.getSettings().getNotificationVisibility(); @@ -557,9 +564,9 @@ public class NotificationServiceImpl implements NotificationService { .setAutoCancel(true) .setColor(ResourcesCompat.getColor(mContext.getResources(), R.color.color_primary_dark, null)); - String key = ConversationPath.toKey(accountId, conversation.getUri()); + String key = cpath.toKey(); - Person contactPerson = new Person.Builder() + Person conversationPerson = new Person.Builder() .setKey(key) .setName(conversationProfile.second) .setIcon(conversationProfile.first == null ? null : IconCompat.createWithBitmap(conversationProfile.first)) @@ -600,7 +607,7 @@ public class NotificationServiceImpl implements NotificationService { Contact contact = textMessage.getContact(); Bitmap contactPicture = getContactPicture(contact); Person contactPerson = new Person.Builder() - .setKey(textMessage.getAuthor()) + .setKey(ConversationPath.toKey(cpath.getAccountId(), contact.getUri().getUri())) .setName(contact.getDisplayName()) .setIcon(contactPicture == null ? null : IconCompat.createWithBitmap(contactPicture)) .build(); @@ -613,7 +620,7 @@ public class NotificationServiceImpl implements NotificationService { messageNotificationBuilder.setStyle(history); } - int notificationId = getTextNotificationId(conversation.getUri()); + int notificationId = getTextNotificationId(conversation.getAccountId(), conversation.getUri()); int replyId = notificationId + 1; int markAsReadId = notificationId + 2; @@ -677,8 +684,10 @@ public class NotificationServiceImpl implements NotificationService { Set<String> notifiedRequests = mPreferencesService.loadRequestsPreferences(account.getAccountID()); Collection<Conversation> requests = account.getPending(); - if (requests.isEmpty()) + if (requests.isEmpty()) { + notificationManager.cancel(notificationId); return; + } if (requests.size() == 1) { Conversation request = requests.iterator().next(); String contactKey = request.getUri().getRawUriString(); @@ -723,7 +732,7 @@ public class NotificationServiceImpl implements NotificationService { } if (!newRequest) return; - builder.setContentText(String.format(mContext.getString(R.string.contact_request_msg), Integer.toString(requests.size()))); + builder.setContentText(String.format(mContext.getString(R.string.contact_request_msg), requests.size())); builder.setLargeIcon(null); notificationManager.notify(notificationId, builder.build()); } @@ -738,17 +747,16 @@ public class NotificationServiceImpl implements NotificationService { if (event == null) { return; } + android.net.Uri path = ConversationPath.toUri(conversation); + Log.d(TAG, "showFileTransferNotification " + path); long dataTransferId = info.getDaemonId(); - int notificationId = getFileTransferNotificationId(dataTransferId); - - android.net.Uri path = ConversationPath.toUri(info.getAccount(), conversation.getUri()); + int notificationId = getFileTransferNotificationId(path, dataTransferId); Intent intentConversation = new Intent(DRingService.ACTION_CONV_ACCEPT, path, mContext, DRingService.class); if (event.isOver()) { - removeTransferNotification(dataTransferId); - - if (info.isOutgoing()) { + removeTransferNotification(path, dataTransferId); + if (info.isOutgoing() || info.isError()) { return; } @@ -816,7 +824,7 @@ public class NotificationServiceImpl implements NotificationService { if (event == Interaction.InteractionStatus.TRANSFER_CREATED) { messageNotificationBuilder.setDefaults(NotificationCompat.DEFAULT_VIBRATE); mNotificationBuilders.put(notificationId, messageNotificationBuilder); - updateNotification(messageNotificationBuilder.build(), notificationId); + // updateNotification(messageNotificationBuilder.build(), notificationId); return; } else { messageNotificationBuilder.setDefaults(NotificationCompat.DEFAULT_LIGHTS); @@ -826,12 +834,12 @@ public class NotificationServiceImpl implements NotificationService { messageNotificationBuilder .addAction(R.drawable.baseline_call_received_24, mContext.getText(R.string.accept), PendingIntent.getService(mContext, random.nextInt(), - new Intent(DRingService.ACTION_FILE_ACCEPT, ConversationPath.toUri(conversation), mContext, DRingService.class) + new Intent(DRingService.ACTION_FILE_ACCEPT, path, mContext, DRingService.class) .putExtra(DRingService.KEY_TRANSFER_ID, dataTransferId), PendingIntent.FLAG_ONE_SHOT)) .addAction(R.drawable.baseline_cancel_24, mContext.getText(R.string.refuse), PendingIntent.getService(mContext, random.nextInt(), - new Intent(DRingService.ACTION_FILE_CANCEL, ConversationPath.toUri(conversation), mContext, DRingService.class) + new Intent(DRingService.ACTION_FILE_CANCEL, path, mContext, DRingService.class) .putExtra(DRingService.KEY_TRANSFER_ID, dataTransferId), PendingIntent.FLAG_ONE_SHOT)); mNotificationBuilders.put(notificationId, messageNotificationBuilder); @@ -841,14 +849,15 @@ public class NotificationServiceImpl implements NotificationService { messageNotificationBuilder .addAction(R.drawable.baseline_cancel_24, mContext.getText(android.R.string.cancel), PendingIntent.getService(mContext, random.nextInt(), - new Intent(DRingService.ACTION_FILE_CANCEL) - .setClass(mContext, DRingService.class) + new Intent(DRingService.ACTION_FILE_CANCEL, path, mContext, DRingService.class) .putExtra(DRingService.KEY_TRANSFER_ID, dataTransferId), PendingIntent.FLAG_ONE_SHOT)); } mNotificationBuilders.put(notificationId, messageNotificationBuilder); dataTransferNotifications.put(notificationId, messageNotificationBuilder.build()); - startForegroundService(notificationId, DataTransferService.class); + ContextCompat.startForegroundService(mContext, new Intent(DataTransferService.ACTION_START, path, mContext, DataTransferService.class) + .putExtra(KEY_NOTIFICATION_ID, notificationId)); + //startForegroundService(notificationId, DataTransferService.class); } @Override @@ -898,17 +907,8 @@ public class NotificationServiceImpl implements NotificationService { } @Override - public void cancelTextNotification(Uri contact) { - if (contact == null) { - return; - } - int notificationId = getTextNotificationId(contact); - notificationManager.cancel(notificationId); - mNotificationBuilders.remove(notificationId); - } - public void cancelTextNotification(String accountId, Uri contact) { - int notificationId = getTextNotificationId(contact); + int notificationId = getTextNotificationId(accountId, contact); notificationManager.cancel(notificationId); mNotificationBuilders.remove(notificationId); } @@ -926,6 +926,7 @@ public class NotificationServiceImpl implements NotificationService { public void cancelCallNotification() { notificationManager.cancel(NOTIF_CALL_ID); mNotificationBuilders.remove(NOTIF_CALL_ID); + callNotifications.clear(); } /**\ @@ -950,12 +951,12 @@ public class NotificationServiceImpl implements NotificationService { return (NOTIF_TRUST_REQUEST + accountId).hashCode(); } - private int getTextNotificationId(Uri contact) { - return (NOTIF_MSG + contact.toString()).hashCode(); + private int getTextNotificationId(String accountId, Uri contact) { + return (NOTIF_MSG + accountId + contact.toString()).hashCode(); } - private int getFileTransferNotificationId(long dataTransferId) { - return (NOTIF_FILE_TRANSFER + dataTransferId).hashCode(); + private int getFileTransferNotificationId(android.net.Uri path, long dataTransferId) { + return (NOTIF_FILE_TRANSFER + path.toString() + dataTransferId).hashCode(); } private Bitmap getContactPicture(Contact contact) { diff --git a/ring-android/app/src/main/java/cx/ring/services/SharedPreferencesServiceImpl.java b/ring-android/app/src/main/java/cx/ring/services/SharedPreferencesServiceImpl.java index bc75a3806..355717553 100644 --- a/ring-android/app/src/main/java/cx/ring/services/SharedPreferencesServiceImpl.java +++ b/ring-android/app/src/main/java/cx/ring/services/SharedPreferencesServiceImpl.java @@ -39,6 +39,7 @@ import javax.inject.Inject; import cx.ring.R; import cx.ring.application.JamiApplication; import net.jami.model.Settings; +import net.jami.model.Uri; import net.jami.services.PreferencesService; import cx.ring.utils.DeviceUtils; @@ -194,6 +195,10 @@ public class SharedPreferencesServiceImpl extends PreferencesService { .getInt(PREF_ACCEPT_IN_MAX_SIZE, 30) * 1024 * 1024; } + public static SharedPreferences getConversationPreferences(@NonNull Context context, String accountId, Uri conversationUri) { + return context.getSharedPreferences(accountId + "_" + conversationUri.getUri(), Context.MODE_PRIVATE); + } + private void applyDarkMode(boolean enabled) { AppCompatDelegate.setDefaultNightMode( enabled ? AppCompatDelegate.MODE_NIGHT_YES diff --git a/ring-android/app/src/main/java/cx/ring/services/VCardServiceImpl.java b/ring-android/app/src/main/java/cx/ring/services/VCardServiceImpl.java index e074f6e56..ee957e584 100644 --- a/ring-android/app/src/main/java/cx/ring/services/VCardServiceImpl.java +++ b/ring-android/app/src/main/java/cx/ring/services/VCardServiceImpl.java @@ -38,6 +38,7 @@ import ezvcard.VCard; import ezvcard.parameter.ImageType; import ezvcard.property.Photo; import ezvcard.property.RawProperty; +import io.reactivex.Maybe; import io.reactivex.Single; import io.reactivex.schedulers.Schedulers; @@ -63,9 +64,9 @@ public class VCardServiceImpl extends VCardService { } @Override - public Single<VCard> loadSmallVCard(String accountId, int maxSize) { + public Maybe<VCard> loadSmallVCard(String accountId, int maxSize) { return VCardUtils.loadLocalProfileFromDisk(mContext.getFilesDir(), accountId) - .filter( vcard -> !VCardUtils.isEmpty(vcard)).toSingle() + .filter(vcard -> !VCardUtils.isEmpty(vcard)) .map(vcard -> { if (!vcard.getPhotos().isEmpty()) { // Reduce photo to fit in maxSize, assuming JPEG compress with ratio of at least 8 diff --git a/ring-android/app/src/main/java/cx/ring/settings/AccountFragment.java b/ring-android/app/src/main/java/cx/ring/settings/AccountFragment.java index 2a7af1458..cb7f393d9 100644 --- a/ring-android/app/src/main/java/cx/ring/settings/AccountFragment.java +++ b/ring-android/app/src/main/java/cx/ring/settings/AccountFragment.java @@ -40,12 +40,11 @@ import cx.ring.account.JamiAccountSummaryFragment; import cx.ring.application.JamiApplication; import cx.ring.client.HomeActivity; import cx.ring.databinding.FragAccountBinding; -import net.jami.model.Account; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; + import net.jami.services.AccountService; -/** - * TODO: improvements : handle multiples permissions for feature. - */ public class AccountFragment extends Fragment implements ViewTreeObserver.OnScrollChangedListener { private static final int SCROLL_DIRECTION_UP = -1; diff --git a/ring-android/app/src/main/java/cx/ring/share/ShareFragment.java b/ring-android/app/src/main/java/cx/ring/share/ShareFragment.java index b59da7a03..8749cd56a 100644 --- a/ring-android/app/src/main/java/cx/ring/share/ShareFragment.java +++ b/ring-android/app/src/main/java/cx/ring/share/ShareFragment.java @@ -30,16 +30,17 @@ import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import cx.ring.R; -import cx.ring.application.JamiApplication; -import cx.ring.databinding.FragShareBinding; -import cx.ring.mvp.BaseSupportFragment; import net.jami.mvp.GenericView; import net.jami.share.SharePresenter; import net.jami.share.ShareViewModel; import net.jami.utils.QRCodeUtils; -public class ShareFragment extends BaseSupportFragment<SharePresenter> implements GenericView<net.jami.share.ShareViewModel> { +import cx.ring.R; +import cx.ring.application.JamiApplication; +import cx.ring.databinding.FragShareBinding; +import cx.ring.mvp.BaseSupportFragment; + +public class ShareFragment extends BaseSupportFragment<SharePresenter> implements GenericView<ShareViewModel> { private String mUriToShow; private boolean isShareLocked = false; diff --git a/ring-android/app/src/main/java/cx/ring/tv/call/TVCallFragment.java b/ring-android/app/src/main/java/cx/ring/tv/call/TVCallFragment.java index aac77e416..a38fd4d13 100644 --- a/ring-android/app/src/main/java/cx/ring/tv/call/TVCallFragment.java +++ b/ring-android/app/src/main/java/cx/ring/tv/call/TVCallFragment.java @@ -86,6 +86,7 @@ import net.jami.model.Conference; import net.jami.model.Call; import cx.ring.mvp.BaseSupportFragment; +import net.jami.model.Uri; import net.jami.services.DeviceRuntimeService; import net.jami.services.HardwareService; import cx.ring.tv.main.HomeActivity; @@ -705,7 +706,7 @@ public class TVCallFragment extends BaseSupportFragment<CallPresenter> implement } @Override - public void goToConversation(String accountId, String conversationId) { + public void goToConversation(String accountId, Uri conversationId) { } diff --git a/ring-android/app/src/main/java/cx/ring/tv/cards/contacts/ContactCard.java b/ring-android/app/src/main/java/cx/ring/tv/cards/contacts/ContactCard.java index a1194a17b..af459b173 100644 --- a/ring-android/app/src/main/java/cx/ring/tv/cards/contacts/ContactCard.java +++ b/ring-android/app/src/main/java/cx/ring/tv/cards/contacts/ContactCard.java @@ -41,9 +41,9 @@ public class ContactCard extends Card { public void setModel(SmartListViewModel model) { mModel = model; setTitle(mModel.getContactName()); - String username = mModel.getContact().get(0).getRingUsername(); + String username = mModel.getContacts().get(0).getRingUsername(); setDescription(username); - boolean isOnline = mModel.getContact().get(0).isOnline(); + boolean isOnline = mModel.getContacts().get(0).isOnline(); if (mModel.getContactName().equals(username)) { if (isOnline) { setType(Type.CONTACT_ONLINE); diff --git a/ring-android/app/src/main/java/cx/ring/tv/contact/TVContactPresenter.java b/ring-android/app/src/main/java/cx/ring/tv/contact/TVContactPresenter.java index bd3a469e2..cfd0056e1 100644 --- a/ring-android/app/src/main/java/cx/ring/tv/contact/TVContactPresenter.java +++ b/ring-android/app/src/main/java/cx/ring/tv/contact/TVContactPresenter.java @@ -100,7 +100,7 @@ public class TVContactPresenter extends RootPresenter<TVContactView> { private void sendTrustRequest(String accountId, Uri conversationUri) { Conversation conversation = mAccountService.getAccount(accountId).getByUri(conversationUri); - mVCardService.loadSmallVCard(accountId, VCardService.MAX_SIZE_REQUEST) + mVCardService.loadSmallVCardWithDefault(accountId, VCardService.MAX_SIZE_REQUEST) .subscribe(vCard -> mAccountService.sendTrustRequest(conversation, conversationUri, Blob.fromString(VCardUtils.vcardToString(vCard))), e -> mAccountService.sendTrustRequest(conversation, conversationUri, null)); } diff --git a/ring-android/app/src/main/java/cx/ring/tv/contactrequest/TVContactRequestDetailPresenter.java b/ring-android/app/src/main/java/cx/ring/tv/contactrequest/TVContactRequestDetailPresenter.java index c2c3c2a40..6157c803d 100644 --- a/ring-android/app/src/main/java/cx/ring/tv/contactrequest/TVContactRequestDetailPresenter.java +++ b/ring-android/app/src/main/java/cx/ring/tv/contactrequest/TVContactRequestDetailPresenter.java @@ -29,7 +29,7 @@ public class TVContactRequestDetailPresenter extends AbstractDetailsDescriptionP protected void onBindDescription(ViewHolder viewHolder, Object item) { SmartListViewModel viewModel = (SmartListViewModel) item; if (viewModel != null) { - String id = viewModel.getContact().get(0).getRingUsername(); + String id = viewModel.getContacts().get(0).getRingUsername(); String displayName = viewModel.getContactName(); viewHolder.getTitle().setText(displayName); if (!displayName.equals(id)) diff --git a/ring-android/app/src/main/java/cx/ring/tv/conversation/TvConversationAdapter.java b/ring-android/app/src/main/java/cx/ring/tv/conversation/TvConversationAdapter.java index 1fb83aa49..7fa1975dd 100644 --- a/ring-android/app/src/main/java/cx/ring/tv/conversation/TvConversationAdapter.java +++ b/ring-android/app/src/main/java/cx/ring/tv/conversation/TvConversationAdapter.java @@ -57,6 +57,17 @@ import com.bumptech.glide.load.resource.bitmap.CenterInside; import com.bumptech.glide.load.resource.bitmap.RoundedCorners; import com.bumptech.glide.request.target.DrawableImageViewTarget; +import net.jami.conversation.ConversationPresenter; +import net.jami.model.Call; +import net.jami.model.Contact; +import net.jami.model.ContactEvent; +import net.jami.model.DataTransfer; +import net.jami.model.Interaction; +import net.jami.model.Interaction.InteractionStatus; +import net.jami.model.Interaction.InteractionType; +import net.jami.model.TextMessage; +import net.jami.utils.StringUtils; + import java.io.File; import java.text.DateFormat; import java.util.ArrayList; @@ -67,22 +78,12 @@ import java.util.concurrent.TimeUnit; import cx.ring.R; import cx.ring.client.MediaViewerActivity; -import net.jami.conversation.ConversationPresenter; -import net.jami.model.Contact; -import net.jami.model.ContactEvent; -import net.jami.model.DataTransfer; -import net.jami.model.Interaction; -import net.jami.model.Interaction.InteractionStatus; -import net.jami.model.Interaction.InteractionType; -import net.jami.model.Call; -import net.jami.model.TextMessage; import cx.ring.service.DRingService; import cx.ring.utils.AndroidFileUtils; import cx.ring.utils.ContentUriHandler; import cx.ring.utils.GlideApp; import cx.ring.utils.GlideOptions; import cx.ring.utils.ResourceMapper; -import net.jami.utils.StringUtils; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; @@ -103,9 +104,9 @@ public class TvConversationAdapter extends RecyclerView.Adapter<TvConversationVi private int expandedItemPosition = -1; private int lastDeliveredPosition = -1; - private Observable<Long> timestampUpdateTimer; + private final Observable<Long> timestampUpdateTimer; - private static int[] msgBGLayouts = new int[] { + private static final int[] msgBGLayouts = new int[] { R.drawable.textmsg_bg_out_last, R.drawable.textmsg_bg_out_middle, R.drawable.textmsg_bg_out_first, @@ -216,7 +217,6 @@ public class TvConversationAdapter extends RecyclerView.Adapter<TvConversationVi } Interaction interaction = mInteractions.get(position); - if (interaction != null) { switch (interaction.getType()) { case CONTACT: @@ -557,12 +557,9 @@ public class TvConversationAdapter extends RecyclerView.Adapter<TvConversationVi } else if (type == TransferMsgType.AUDIO) { Context context = viewHolder.itemView.getContext(); - viewHolder.itemView.setOnFocusChangeListener(new View.OnFocusChangeListener() { - @Override - public void onFocusChange(View v, boolean hasFocus) { - viewHolder.itemView.setBackgroundResource(hasFocus? R.drawable.tv_item_selected_background : R.drawable.tv_item_unselected_background); - viewHolder.mAudioInfoLayout.animate().scaleY(hasFocus? 1.1f : 1f).scaleX(hasFocus? 1.1f : 1f); - } + viewHolder.itemView.setOnFocusChangeListener((v, hasFocus) -> { + viewHolder.itemView.setBackgroundResource(hasFocus? R.drawable.tv_item_selected_background : R.drawable.tv_item_unselected_background); + viewHolder.mAudioInfoLayout.animate().scaleY(hasFocus? 1.1f : 1f).scaleX(hasFocus? 1.1f : 1f); }); try { diff --git a/ring-android/app/src/main/java/cx/ring/tv/main/HomeActivity.java b/ring-android/app/src/main/java/cx/ring/tv/main/HomeActivity.java index 6a469b257..e24566ffe 100644 --- a/ring-android/app/src/main/java/cx/ring/tv/main/HomeActivity.java +++ b/ring-android/app/src/main/java/cx/ring/tv/main/HomeActivity.java @@ -23,15 +23,16 @@ package cx.ring.tv.main; import android.content.Intent; import android.os.Bundle; +import androidx.fragment.app.FragmentActivity; import androidx.leanback.app.BackgroundManager; import androidx.leanback.app.GuidedStepSupportFragment; -import androidx.fragment.app.FragmentActivity; + +import net.jami.services.AccountService; import javax.inject.Inject; import cx.ring.R; import cx.ring.application.JamiApplication; -import net.jami.services.AccountService; import cx.ring.tv.account.TVAccountWizard; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; diff --git a/ring-android/app/src/main/java/cx/ring/tv/main/MainFragment.java b/ring-android/app/src/main/java/cx/ring/tv/main/MainFragment.java index cf38bd204..94878c8b3 100644 --- a/ring-android/app/src/main/java/cx/ring/tv/main/MainFragment.java +++ b/ring-android/app/src/main/java/cx/ring/tv/main/MainFragment.java @@ -302,7 +302,7 @@ public class MainFragment extends BaseBrowseFragment<MainPresenter> implements M .setChannelId(channelId) .setType(TvContractCompat.PreviewPrograms.TYPE_CLIP) .setTitle(vm.getContactName()) - .setAuthor(vm.getContact().get(0).getRingUsername()) + .setAuthor(vm.getContacts().get(0).getRingUsername()) .setPosterArtAspectRatio(TvContractCompat.PreviewPrograms.ASPECT_RATIO_1_1) .setPosterArtUri(uri) .setIntentUri(new Uri.Builder() diff --git a/ring-android/app/src/main/java/cx/ring/utils/AndroidFileUtils.java b/ring-android/app/src/main/java/cx/ring/utils/AndroidFileUtils.java index ade26eda7..f59739659 100644 --- a/ring-android/app/src/main/java/cx/ring/utils/AndroidFileUtils.java +++ b/ring-android/app/src/main/java/cx/ring/utils/AndroidFileUtils.java @@ -361,7 +361,7 @@ public class AndroidFileUtils { return context.getFileStreamPath(filename); } - public static File getConversationPath(Context context, String conversationId, String name) { + public static File getConversationDir(Context context, String conversationId) { File conversationsDir = getFilePath(context, "conversation_data"); if (!conversationsDir.exists()) @@ -371,7 +371,11 @@ public class AndroidFileUtils { if (!conversationDir.exists()) conversationDir.mkdir(); - return new File(conversationDir, name); + return conversationDir; + } + + public static File getConversationPath(Context context, String conversationId, String name) { + return new File(getConversationDir(context, conversationId), name); } public static File getTempPath(Context context, String conversationId, String name) { diff --git a/ring-android/app/src/main/java/cx/ring/utils/BluetoothWrapper.java b/ring-android/app/src/main/java/cx/ring/utils/BluetoothWrapper.java index 9f8be66bf..baf5db9bf 100644 --- a/ring-android/app/src/main/java/cx/ring/utils/BluetoothWrapper.java +++ b/ring-android/app/src/main/java/cx/ring/utils/BluetoothWrapper.java @@ -35,8 +35,7 @@ import java.util.List; import java.util.Set; public class BluetoothWrapper { - - private static String TAG = BluetoothWrapper.class.getSimpleName(); + private static final String TAG = BluetoothWrapper.class.getSimpleName(); private static final boolean DBG = false; private final Context mContext; diff --git a/ring-android/app/src/main/java/cx/ring/utils/ClipboardHelper.java b/ring-android/app/src/main/java/cx/ring/utils/ClipboardHelper.java index 9c92591b0..73e8b90a4 100644 --- a/ring-android/app/src/main/java/cx/ring/utils/ClipboardHelper.java +++ b/ring-android/app/src/main/java/cx/ring/utils/ClipboardHelper.java @@ -19,44 +19,30 @@ */ package cx.ring.utils; -import android.app.Activity; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.text.TextUtils; import android.util.Log; +import androidx.annotation.NonNull; + import cx.ring.BuildConfig; +import cx.ring.R; public class ClipboardHelper { public static final String TAG = ClipboardHelper.class.getSimpleName(); - public static final String COPY_CALL_CONTACT_NUMBER_CLIP_LABEL = - BuildConfig.APPLICATION_ID + ".clipboard.contactNumber"; - public static void copyNumberToClipboard(final Activity activity, - final String number, - final ClipboardHelperCallback callback) { - if (TextUtils.isEmpty(number)) { + public static void copyToClipboard(final @NonNull Context context, + final String text) { + if (TextUtils.isEmpty(text)) { Log.d(TAG, "copyNumberToClipboard: number is null"); return; } - if (activity == null) { - Log.d(TAG, "copyNumberToClipboard: activity is null"); - return; - } - - ClipboardManager clipboard = (ClipboardManager) activity + ClipboardManager clipboard = (ClipboardManager) context .getSystemService(Context.CLIPBOARD_SERVICE); - ClipData clip = android.content.ClipData.newPlainText(COPY_CALL_CONTACT_NUMBER_CLIP_LABEL, - number); + ClipData clip = android.content.ClipData.newPlainText(context.getText(R.string.clip_contact_uri), text); clipboard.setPrimaryClip(clip); - if (callback != null) { - callback.clipBoardDidCopyNumber(number); - } - } - - public interface ClipboardHelperCallback { - void clipBoardDidCopyNumber(String copiedNumber); } } diff --git a/ring-android/app/src/main/java/cx/ring/utils/ConversationPath.java b/ring-android/app/src/main/java/cx/ring/utils/ConversationPath.java index c8f695cf7..d3c2f9294 100644 --- a/ring-android/app/src/main/java/cx/ring/utils/ConversationPath.java +++ b/ring-android/app/src/main/java/cx/ring/utils/ConversationPath.java @@ -8,12 +8,14 @@ import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import java.util.Arrays; import java.util.List; import java.util.Objects; import cx.ring.fragments.ConversationFragment; import net.jami.model.Conversation; import net.jami.model.Interaction; +import net.jami.utils.StringUtils; import net.jami.utils.Tuple; public class ConversationPath { @@ -33,12 +35,20 @@ public class ConversationPath { conversationId = path.second; } + public ConversationPath(Conversation conversation) { + accountId = conversation.getAccountId(); + conversationId = conversation.getUri().getUri(); + } + public String getAccountId() { return accountId; } public String getConversationId() { return conversationId; } + public net.jami.model.Uri getConversationUri() { + return net.jami.model.Uri.fromString(conversationId); + } @Deprecated public String getContactId() { @@ -64,7 +74,10 @@ public class ConversationPath { return toUri(conversation.getAccountId(), conversation.getUri()); } public static Uri toUri(@NonNull Interaction interaction) { - return toUri(interaction.getAccount(), net.jami.model.Uri.fromString(interaction.getConversation().getParticipant())); + if (interaction.getConversation() instanceof Conversation) + return toUri(interaction.getAccount(), ((Conversation) interaction.getConversation()).getUri()); + else + return toUri(interaction.getAccount(), net.jami.model.Uri.fromString(interaction.getConversation().getParticipant())); } public Bundle toBundle() { @@ -74,6 +87,7 @@ public class ConversationPath { bundle.putString(ConversationFragment.KEY_CONTACT_RING_ID, conversationId); bundle.putString(ConversationFragment.KEY_ACCOUNT_ID, accountId); } + public static Bundle toBundle(String accountId, String uri) { Bundle bundle = new Bundle(); bundle.putString(ConversationFragment.KEY_CONTACT_RING_ID, uri); @@ -87,7 +101,9 @@ public class ConversationPath { public static String toKey(String accountId, String uri) { return TextUtils.join(",", Arrays.asList(accountId, uri)); } - + public String toKey() { + return toKey(accountId, conversationId); + } public static ConversationPath fromKey(String key) { if (key != null) { String[] keys = TextUtils.split(key, ","); @@ -160,7 +176,4 @@ public class ConversationPath { return Objects.hash(accountId, conversationId); } - public net.jami.model.Uri getConversationUri() { - return net.jami.model.Uri.fromString(conversationId); - } } diff --git a/ring-android/app/src/main/java/cx/ring/utils/ResourceMapper.java b/ring-android/app/src/main/java/cx/ring/utils/ResourceMapper.java index 5096cc29d..3e340332b 100644 --- a/ring-android/app/src/main/java/cx/ring/utils/ResourceMapper.java +++ b/ring-android/app/src/main/java/cx/ring/utils/ResourceMapper.java @@ -28,33 +28,27 @@ import net.jami.model.Interaction.InteractionStatus; public class ResourceMapper { public static String getReadableFileTransferStatus(Context context, InteractionStatus transferStatus) { - if (transferStatus == InteractionStatus.TRANSFER_CREATED) { - return context.getString(R.string.file_transfer_status_created); + switch (transferStatus) { + case TRANSFER_CREATED: + return context.getString(R.string.file_transfer_status_created); + case TRANSFER_AWAITING_PEER: + return context.getString(R.string.file_transfer_status_wait_peer_acceptance); + case TRANSFER_AWAITING_HOST: + return context.getString(R.string.file_transfer_status_wait_host_acceptance); + case TRANSFER_ONGOING: + return context.getString(R.string.file_transfer_status_ongoing); + case TRANSFER_FINISHED: + return context.getString(R.string.file_transfer_status_finished); + case TRANSFER_CANCELED: + return context.getString(R.string.file_transfer_status_cancelled); + case TRANSFER_UNJOINABLE_PEER: + return context.getString(R.string.file_transfer_status_unjoinable_peer); + case TRANSFER_ERROR: + return context.getString(R.string.file_transfer_status_error); + case TRANSFER_TIMEOUT_EXPIRED: + return context.getString(R.string.file_transfer_status_timed_out); + default: + return ""; } - if (transferStatus == InteractionStatus.TRANSFER_AWAITING_PEER) { - return context.getString(R.string.file_transfer_status_wait_peer_acceptance); - } - if (transferStatus == InteractionStatus.TRANSFER_AWAITING_HOST) { - return context.getString(R.string.file_transfer_status_wait_host_acceptance); - } - if (transferStatus == InteractionStatus.TRANSFER_ONGOING) { - return context.getString(R.string.file_transfer_status_ongoing); - } - if (transferStatus == InteractionStatus.TRANSFER_FINISHED) { - return context.getString(R.string.file_transfer_status_finished); - } - if (transferStatus == InteractionStatus.TRANSFER_CANCELED) { - return context.getString(R.string.file_transfer_status_cancelled); - } - if (transferStatus == InteractionStatus.TRANSFER_UNJOINABLE_PEER) { - return context.getString(R.string.file_transfer_status_unjoinable_peer); - } - if (transferStatus == InteractionStatus.TRANSFER_ERROR) { - return context.getString(R.string.file_transfer_status_error); - } - if (transferStatus == InteractionStatus.TRANSFER_TIMEOUT_EXPIRED) { - return context.getString(R.string.file_transfer_status_timed_out); - } - return ""; } } diff --git a/ring-android/app/src/main/java/cx/ring/views/AvatarDrawable.java b/ring-android/app/src/main/java/cx/ring/views/AvatarDrawable.java index e2e87e923..38ea7c6b5 100644 --- a/ring-android/app/src/main/java/cx/ring/views/AvatarDrawable.java +++ b/ring-android/app/src/main/java/cx/ring/views/AvatarDrawable.java @@ -33,29 +33,33 @@ import android.graphics.RectF; import android.graphics.Shader; import android.graphics.Typeface; import android.graphics.drawable.Drawable; +import android.graphics.drawable.VectorDrawable; +import android.text.TextUtils; +import android.util.Log; +import android.util.TypedValue; import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; -import cx.ring.R; + import net.jami.model.Account; import net.jami.model.Contact; import net.jami.model.Conversation; -import cx.ring.services.VCardServiceImpl; import net.jami.smartlist.SmartListViewModel; -import cx.ring.utils.DeviceUtils; import net.jami.utils.HashUtils; -import io.reactivex.Single; - -import android.graphics.drawable.VectorDrawable; -import android.text.TextUtils; -import android.util.TypedValue; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import cx.ring.R; +import cx.ring.services.VCardServiceImpl; +import cx.ring.utils.DeviceUtils; +import io.reactivex.Single; + public class AvatarDrawable extends Drawable { + private static final String TAG = AvatarDrawable.class.getSimpleName(); + private static final int SIZE_AB = 36; private static final float SIZE_BORDER = 2f; @@ -76,7 +80,11 @@ public class AvatarDrawable extends Drawable { R.color.brown_500, R.color.blue_grey_500 }; - private static class PresenceIndicatorInfo { int cx, cy, radius; }; + private static class PresenceIndicatorInfo { + int cx, cy, radius; + } + + ; private final PresenceIndicatorInfo presence = new PresenceIndicatorInfo(); private boolean update = true; @@ -101,6 +109,7 @@ public class AvatarDrawable extends Drawable { private final Paint presenceStrokePaint; private final Paint checkedPaint; private static final Paint drawPaint = new Paint(); + static { drawPaint.setAntiAlias(true); drawPaint.setFilterBitmap(true); @@ -122,36 +131,44 @@ public class AvatarDrawable extends Drawable { private boolean isChecked = false; private boolean isGroup = false; - public Builder() {} + public Builder() { + } public Builder withId(String id) { this.id = id; return this; } + public Builder withPhoto(Bitmap photo) { this.photos = photo == null ? null : Arrays.asList(photo); // list elements must be mutable return this; } + public Builder withPhotos(List<Bitmap> photos) { this.photos = photos.isEmpty() ? null : photos; return this; } + public Builder withName(String name) { this.name = name; return this; } + public Builder withCircleCrop(boolean crop) { this.circleCrop = crop; return this; } + public Builder withOnlineState(boolean isOnline) { this.isOnline = isOnline; return this; } + public Builder withPresence(boolean showPresence) { this.showPresence = showPresence; return this; } + public Builder withCheck(boolean checked) { this.isChecked = checked; return this; @@ -160,10 +177,11 @@ public class AvatarDrawable extends Drawable { public Builder withNameData(String profileName, String username) { return withName(TextUtils.isEmpty(profileName) ? username : profileName); } - public Builder withContact(Contact contact){ + + public Builder withContact(Contact contact) { if (contact == null) return this; - return withPhoto((Bitmap)contact.getPhoto()) + return withPhoto((Bitmap) contact.getPhoto()) .withId(contact.getPrimaryNumber()) .withOnlineState(contact.isOnline()) .withNameData(contact.getProfileName(), contact.getUsername()); @@ -181,24 +199,24 @@ public class AvatarDrawable extends Drawable { bitmaps.add(bitmap); } if (bitmaps.size() == 4) - break;; + break; } - if (bitmaps.isEmpty()) { - if (notTheUser == 1) { - for (Contact contact : contacts) { - if (!contact.isUser()) - return withContact(contact); - } - } else if (notTheUser == 0) { - // Fallback to the user avatar - for (Contact contact : contacts) + if (notTheUser == 1) { + for (Contact contact : contacts) { + if (!contact.isUser()) return withContact(contact); } - return this; + } + if (bitmaps.isEmpty()) { + // Fallback to the user avatar + for (Contact contact : contacts) + return withContact(contact); } else { return withPhotos(bitmaps); } + return this; } + public Builder withConversation(Conversation conversation) { return conversation.isSwarm() ? withContacts(conversation.getContacts()).setGroup() @@ -213,9 +231,10 @@ public class AvatarDrawable extends Drawable { public Builder withViewModel(SmartListViewModel vm) { boolean isSwarm = vm.getUri().isSwarm(); return (isSwarm - ? withContacts(vm.getContact()).setGroup() - : withContact(vm.getContact().isEmpty() ? null : vm.getContact().get(vm.getContact().size() - 1))) + ? withContacts(vm.getContacts()).setGroup() + : withContact(vm.getContacts().isEmpty() ? null : vm.getContacts().get(vm.getContacts().size() - 1))) .withPresence(vm.showPresence()) + .withOnlineState(vm.isOnline()) .withCheck(vm.isChecked()); } @@ -236,12 +255,13 @@ public class AvatarDrawable extends Drawable { public static Single<AvatarDrawable> load(Context context, Account account, boolean crop) { return VCardServiceImpl.loadProfile(account) .map(data -> new Builder() - .withPhoto((Bitmap)data.second) + .withPhoto((Bitmap) data.second) .withNameData(data.first, account.getRegisteredName()) .withId(account.getUri()) .withCircleCrop(crop) .build(context)); } + public static Single<AvatarDrawable> load(Context context, Account account) { return load(context, account, true); } @@ -252,19 +272,22 @@ public class AvatarDrawable extends Drawable { avatarText = convertNameToAvatarText( TextUtils.isEmpty(profileName) ? username : profileName); if (bitmaps != null) { - bitmaps.set(0, (Bitmap)contact.getPhoto()); + bitmaps.set(0, (Bitmap) contact.getPhoto()); } isOnline = contact.isOnline(); update = true; } + public void setName(String name) { avatarText = convertNameToAvatarText(name); update = true; } + public void setPhoto(Bitmap photo) { bitmaps.set(0, photo); update = true; } + public void setOnline(boolean online) { isOnline = online; } @@ -281,6 +304,9 @@ public class AvatarDrawable extends Drawable { TypedValue.COMPLEX_UNIT_DIP, SIZE_AB, context.getResources().getDisplayMetrics()); float borderSize = TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, SIZE_BORDER, context.getResources().getDisplayMetrics()); + if (cropCircle) { + inSize = minSize; + } if (photos != null && photos.size() > 0) { avatarText = null; bitmaps = photos; @@ -288,12 +314,12 @@ public class AvatarDrawable extends Drawable { backgroundBounds = Collections.singletonList(new RectF()); inBounds = Collections.singletonList(null); clipPaint = cropCircle ? Collections.singletonList(new Paint()) : null; - workspace = Arrays.asList((Bitmap)null); + workspace = Arrays.asList((Bitmap) null); } else { backgroundBounds = new ArrayList<>(bitmaps.size()); inBounds = new ArrayList<>(bitmaps.size()); clipPaint = cropCircle ? new ArrayList<>(bitmaps.size()) : null; - workspace = cropCircle ? new ArrayList<>(bitmaps.size()) : Arrays.asList((Bitmap)null); + workspace = cropCircle ? new ArrayList<>(bitmaps.size()) : Arrays.asList((Bitmap) null); for (Bitmap ignored : bitmaps) { backgroundBounds.add(new RectF()); inBounds.add(cropCircle ? null : new Rect()); @@ -308,7 +334,7 @@ public class AvatarDrawable extends Drawable { } } } else { - workspace = Arrays.asList((Bitmap)null); + workspace = Arrays.asList((Bitmap) null); bitmaps = null; backgroundBounds = null; inBounds = null; @@ -359,7 +385,7 @@ public class AvatarDrawable extends Drawable { bitmaps = other.bitmaps; backgroundBounds = other.backgroundBounds == null ? null : new ArrayList<>(other.backgroundBounds.size()); if (backgroundBounds != null) { - for (int i=0, n=other.backgroundBounds.size(); i<n; i++) { + for (int i = 0, n = other.backgroundBounds.size(); i < n; i++) { backgroundBounds.add(new RectF()); } } @@ -369,12 +395,12 @@ public class AvatarDrawable extends Drawable { placeholder = other.placeholder; avatarText = other.avatarText; workspace = new ArrayList<>(other.workspace.size()); - for (int i=0, n=other.workspace.size(); i<n; i++) { + for (int i = 0, n = other.workspace.size(); i < n; i++) { workspace.add(null); } clipPaint = other.clipPaint == null ? null : new ArrayList<>(other.clipPaint.size()); if (clipPaint != null) { - for (int i=0, n=other.clipPaint.size(); i<n; i++) { + for (int i = 0, n = other.clipPaint.size(); i < n; i++) { clipPaint.add(new Paint(other.clipPaint.get(i))); clipPaint.get(i).setShader(null); } @@ -402,18 +428,20 @@ public class AvatarDrawable extends Drawable { update = false; } if (cropCircle) { - float r = Math.min(getBounds().width(), getBounds().height()) / 2; - int cx = getBounds().centerX(); - float cy = getBounds().height() / 2; + finalCanvas.save(); + finalCanvas.translate((getBounds().width() - workspace.get(0).getWidth()) / 2.f, (getBounds().height() - workspace.get(0).getHeight()) / 2.f); + float r = Math.min(workspace.get(0).getWidth(), workspace.get(0).getHeight()) / 2; + int cx = workspace.get(0).getWidth()/2;//getBounds().centerX(); + float cy = workspace.get(0).getHeight()/2;//getBounds().height() / 2; int i = 0; final float ratio = 1.333333f; for (Paint paint : clipPaint) { - finalCanvas.drawCircle(cx, getBounds().bottom - cy, r, paint); + finalCanvas.drawCircle(cx, workspace.get(0).getHeight() - cy, r, paint); if (i != 0) { Shader s = paint.getShader(); paint.setShader(null); paint.setStyle(Paint.Style.STROKE); - finalCanvas.drawCircle(cx, getBounds().bottom - cy, r, paint); + finalCanvas.drawCircle(cx, workspace.get(0).getHeight() - cy, r, paint); paint.setShader(s); paint.setStyle(Paint.Style.FILL); } @@ -421,6 +449,8 @@ public class AvatarDrawable extends Drawable { r /= ratio; cy /= ratio; } + + finalCanvas.restore(); } else { finalCanvas.drawBitmap(workspace.get(0), null, getBounds(), drawPaint); } @@ -463,13 +493,14 @@ public class AvatarDrawable extends Drawable { presence.radius -= presenceStrokeWidth * 0.5; if (checkedIcon != null) - checkedIcon.setBounds(presence.cx - presence.radius, presence.cy - presence.radius, presence.cx + presence.radius, presence.cy + presence.radius); + checkedIcon.setBounds(presence.cx - presence.radius, presence.cy - presence.radius, presence.cx + presence.radius, presence.cy + presence.radius); } private void drawPresence(@NonNull Canvas canvas) { canvas.drawCircle(presence.cx, presence.cy, presence.radius - 1, presenceFillPaint); canvas.drawCircle(presence.cx, presence.cy, presence.radius, presenceStrokePaint); } + private void drawChecked(@NonNull Canvas canvas) { if (checkedIcon != null) { canvas.drawCircle(presence.cx, presence.cy, presence.radius, checkedPaint); @@ -477,7 +508,7 @@ public class AvatarDrawable extends Drawable { } } - private static Rect getSubBounds(@NonNull Rect bounds, int total, int i) { + private static Rect getSubBounds(@NonNull Rect bounds, int total, int i) { if (total == 1) return bounds; @@ -505,7 +536,7 @@ public class AvatarDrawable extends Drawable { return null; } - private static <T> void fit(int iw, int ih, int bw, int bh, boolean outfit, T ret) { + private static <T> void fit(int iw, int ih, int bw, int bh, boolean outfit, T ret) { int a = bw * ih; int b = bh * iw; int w; @@ -520,9 +551,9 @@ public class AvatarDrawable extends Drawable { int x = (iw - w) / 2; int y = (ih - h) / 2; if (ret instanceof Rect) - ((Rect)ret).set(x, y, x + w, y + h); + ((Rect) ret).set(x, y, x + w, y + h); else if (ret instanceof RectF) - ((RectF)ret).set(x, y, x + w, y + h); + ((RectF) ret).set(x, y, x + w, y + h); } @Override @@ -531,21 +562,20 @@ public class AvatarDrawable extends Drawable { setupPresenceIndicator(bounds); int d = Math.min(bounds.width(), bounds.height()); if (placeholder != null) { - int cx = (bounds.width()-d)/2; - int cy = (bounds.height()-d)/2; + int cx = (bounds.width() - d) / 2; + int cy = (bounds.height() - d) / 2; placeholder.setBounds(cx, cy, cx + d, cy + d); } int iw = cropCircle ? d : bounds.width(); int ih = cropCircle ? d : bounds.height(); - for (int i=0, n=workspace.size(); i<n; i++) { + for (int i = 0, n = workspace.size(); i < n; i++) { if (workspace.get(i) != null) { workspace.get(i).recycle(); workspace.set(i, null); + clipPaint.get(i).setShader(null); } } if (iw <= 0 || ih <= 0) { - for (Paint p : clipPaint) - p.setShader(null); return; } if (cropCircle) { @@ -560,13 +590,13 @@ public class AvatarDrawable extends Drawable { if (bitmaps != null) { if (bitmaps.size() == 1 || cropCircle) { - for (int i=0; i<bitmaps.size(); i++) { + for (int i = 0; i < bitmaps.size(); i++) { Bitmap bitmap = bitmaps.get(i); fit(iw, ih, bitmap.getWidth(), bitmap.getHeight(), true, backgroundBounds.get(i)); } } else { Rect realBounds = cropCircle ? new Rect(0, 0, iw, ih) : bounds; - for (int i=0; i<bitmaps.size(); i++) { + for (int i = 0; i < bitmaps.size(); i++) { Bitmap bitmap = bitmaps.get(i); Rect subBounds = getSubBounds(realBounds, bitmaps.size(), i); if (subBounds != null) { diff --git a/ring-android/app/src/main/res/layout/activity_contact_details.xml b/ring-android/app/src/main/res/layout/activity_contact_details.xml index d85976f65..8f6c66278 100644 --- a/ring-android/app/src/main/res/layout/activity_contact_details.xml +++ b/ring-android/app/src/main/res/layout/activity_contact_details.xml @@ -57,10 +57,11 @@ app:titleTextColor="@color/colorOnPrimary" /> </com.google.android.material.appbar.CollapsingToolbarLayout> + </com.google.android.material.appbar.AppBarLayout> <com.google.android.material.floatingactionbutton.FloatingActionButton - android:id="@+id/fab" + android:id="@+id/sendMessage" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="@dimen/fab_margin" @@ -68,4 +69,17 @@ app:layout_anchorGravity="bottom|end" app:srcCompat="@drawable/baseline_chat_24" /> + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/contactList" + android:background="@color/surface" + android:elevation="4dp" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="bottom" + android:orientation="horizontal" + android:visibility="gone" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" + tools:listitem="@layout/item_contact_horizontal" + tools:visibility="visible"/> + </androidx.coordinatorlayout.widget.CoordinatorLayout> diff --git a/ring-android/app/src/main/res/layout/frag_conversation.xml b/ring-android/app/src/main/res/layout/frag_conversation.xml index b680bcf78..291987c6d 100644 --- a/ring-android/app/src/main/res/layout/frag_conversation.xml +++ b/ring-android/app/src/main/res/layout/frag_conversation.xml @@ -52,7 +52,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@id/ongoingcall_pane" - android:orientation="vertical"> + android:orientation="vertical" + android:visibility="gone"> <TextView android:id="@+id/tvTrustRequestMessage" diff --git a/ring-android/app/src/main/res/layout/item_contact_horizontal.xml b/ring-android/app/src/main/res/layout/item_contact_horizontal.xml new file mode 100644 index 000000000..fa4d37a3b --- /dev/null +++ b/ring-android/app/src/main/res/layout/item_contact_horizontal.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?><!-- +Copyright (C) 2004-2016 Savoir-faire Linux Inc. + +Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com> + Adrien Beraud <adrien.beraud@savoirfairelinux.com> + Romain Bertozzi <romain.bertozzi@savoirfairelinux.com> + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +--> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="112dp" + android:layout_height="wrap_content" + android:background="?android:attr/selectableItemBackground" + android:descendantFocusability="blocksDescendants" + android:padding="8dp"> + + <ImageView + android:id="@+id/photo" + android:layout_width="60dp" + android:layout_height="60dp" + android:layout_centerHorizontal="true" + android:layout_margin="8dp" + android:contentDescription="@string/contact_picture_description" + android:scaleType="fitCenter" + tools:src="@drawable/baseline_person_24" /> + + <TextView + android:id="@+id/display_name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@+id/photo" + android:ellipsize="end" + android:gravity="center_horizontal" + android:layout_centerHorizontal="true" + android:maxLines="1" + android:singleLine="true" + android:textAlignment="center" + android:textColor="@color/textColorPrimary" + android:textIsSelectable="false" + android:textSize="16sp" + tools:text="Thomas" /> + +</RelativeLayout> diff --git a/ring-android/app/src/main/res/values/strings.xml b/ring-android/app/src/main/res/values/strings.xml index 66a1fe34d..97cc1c45b 100644 --- a/ring-android/app/src/main/res/values/strings.xml +++ b/ring-android/app/src/main/res/values/strings.xml @@ -238,6 +238,7 @@ along with this program; if not, write to the Free Software <string name="block_contact_dialog_message">%1$s won\'t be able to contact you until you unblock it.</string> <string name="block_contact_completed">%1$s was blocked.</string> <string name="conversation_contact_is_typing">Contact is typing…</string> + <string name="conversation_info_contact_you">You</string> <string name="conversation_preference_color">Change conversation color</string> <string name="conversation_preference_emoji">Change conversation emoji</string> diff --git a/ring-android/libringclient/src/main/java/net/jami/call/CallPresenter.java b/ring-android/libringclient/src/main/java/net/jami/call/CallPresenter.java index 208bf9b16..6120ebd47 100644 --- a/ring-android/libringclient/src/main/java/net/jami/call/CallPresenter.java +++ b/ring-android/libringclient/src/main/java/net/jami/call/CallPresenter.java @@ -25,6 +25,7 @@ import net.jami.facades.ConversationFacade; import net.jami.model.Call; import net.jami.model.Conference; import net.jami.model.Conversation; +import net.jami.model.ConversationHistory; import net.jami.model.Uri; import net.jami.mvp.RootPresenter; import net.jami.services.AccountService; @@ -152,8 +153,8 @@ public class CallPresenter extends RootPresenter<CallView> { }));*/ } - public void initOutGoing(String accountId, Uri conversationUri, String contactId, boolean audioOnly) { - if (accountId == null || contactId == null) { + public void initOutGoing(String accountId, Uri conversationUri, String contactUri, boolean audioOnly) { + if (accountId == null || contactUri == null) { Log.e(TAG, "initOutGoing: null account or contact"); hangupCall(); return; @@ -164,9 +165,9 @@ public class CallPresenter extends RootPresenter<CallView> { //getView().blockScreenRotation(); Observable<Conference> callObservable = mCallService - .placeCall(accountId, conversationUri, Uri.fromString(StringUtils.toNumber(contactId)), audioOnly) + .placeCall(accountId, conversationUri, Uri.fromString(StringUtils.toNumber(contactUri)), audioOnly) //.map(mCallService::getConference) - .flatMapObservable(call -> mCallService.getConfUpdates(call)) + .flatMapObservable(mCallService::getConfUpdates) .share(); mCompositeDisposable.add(callObservable @@ -176,7 +177,7 @@ public class CallPresenter extends RootPresenter<CallView> { confUpdate(conference); }, e -> { hangupCall(); - net.jami.utils.Log.e(TAG, "Error with initOutgoing: " + e.getMessage()); + Log.e(TAG, "Error with initOutgoing: " + e.getMessage()); })); showConference(callObservable); @@ -210,7 +211,7 @@ public class CallPresenter extends RootPresenter<CallView> { } }, e -> { hangupCall(); - net.jami.utils.Log.e(TAG, "Error with initIncoming, preparing call flow :" , e); + Log.e(TAG, "Error with initIncoming, preparing call flow :" , e); })); // Handles retrieving call updates. Items emitted are only used if call is already in process or if user is returning to a call. @@ -222,7 +223,7 @@ public class CallPresenter extends RootPresenter<CallView> { } }, e -> { hangupCall(); - net.jami.utils.Log.e(TAG, "Error with initIncoming, action view flow: ", e); + Log.e(TAG, "Error with initIncoming, action view flow: ", e); })); showConference(callObservable); @@ -260,13 +261,16 @@ public class CallPresenter extends RootPresenter<CallView> { return; } Call firstCall = mConference.getParticipants().get(0); - if (firstCall == null - || firstCall.getContact() == null - || firstCall.getContact().getIds() == null - || firstCall.getContact().getIds().isEmpty()) { + if (firstCall == null) { return; } - getView().goToConversation(firstCall.getAccount(), firstCall.getContact().getIds().get(0)); + ConversationHistory c = firstCall.getConversation(); + if (c instanceof Conversation) { + Conversation conversation = ((Conversation) c); + getView().goToConversation(conversation.getAccountId(), conversation.getUri()); + } else if (firstCall.getContact() != null) { + getView().goToConversation(firstCall.getAccount(), firstCall.getContact().getConversationUri().blockingFirst()); + } } public void speakerClick(boolean checked) { @@ -427,7 +431,7 @@ public class CallPresenter extends RootPresenter<CallView> { // Updates of participant (and pending participant) list Observable<List<Call>> callsObservable = mPendingSubject .map(pendingList -> { - net.jami.utils.Log.w(TAG, "mPendingSubject onNext " + pendingList.size() + " " + conference.getParticipants().size()); + Log.w(TAG, "mPendingSubject onNext " + pendingList.size() + " " + conference.getParticipants().size()); if (pendingList.isEmpty()) return conference.getParticipants(); List<Call> newList = new ArrayList<>(conference.getParticipants().size() + pendingList.size()); @@ -448,7 +452,7 @@ public class CallPresenter extends RootPresenter<CallView> { Observable<List<Call>> contactUpdates = contactsObservable .switchMap(list -> Observable .combineLatest(list, objects -> { - net.jami.utils.Log.w(TAG, "flatMapObservable " + objects.length); + Log.w(TAG, "flatMapObservable " + objects.length); ArrayList<Call> calls = new ArrayList<>(objects.length); for (Object call : objects) calls.add((Call)call); @@ -458,7 +462,7 @@ public class CallPresenter extends RootPresenter<CallView> { contactDisposable = contactUpdates .observeOn(mUiScheduler) - .subscribe(cs -> getView().updateContactBubble(cs), e -> net.jami.utils.Log.e(TAG, "Error updating contact data", e)); + .subscribe(cs -> getView().updateContactBubble(cs), e -> Log.e(TAG, "Error updating contact data", e)); mCompositeDisposable.add(contactDisposable); } mPendingSubject.onNext(mPendingCalls); diff --git a/ring-android/libringclient/src/main/java/net/jami/call/CallView.java b/ring-android/libringclient/src/main/java/net/jami/call/CallView.java index 21eca8e29..f82a97544 100644 --- a/ring-android/libringclient/src/main/java/net/jami/call/CallView.java +++ b/ring-android/libringclient/src/main/java/net/jami/call/CallView.java @@ -25,6 +25,7 @@ import java.util.Set; import net.jami.model.Call; import net.jami.model.Contact; import net.jami.model.Conference; +import net.jami.model.Uri; import net.jami.services.HardwareService; public interface CallView { @@ -62,7 +63,7 @@ public interface CallView { void resetPluginPreviewVideoSize(int previewWidth, int previewHeight, int rot); void resetVideoSize(int videoWidth, int videoHeight); - void goToConversation(String accountId, String conversationId); + void goToConversation(String accountId, Uri conversationId); void goToAddContact(Contact contact); diff --git a/ring-android/libringclient/src/main/java/net/jami/conversation/ConversationPresenter.java b/ring-android/libringclient/src/main/java/net/jami/conversation/ConversationPresenter.java index 260cf8f1f..9624e69f2 100644 --- a/ring-android/libringclient/src/main/java/net/jami/conversation/ConversationPresenter.java +++ b/ring-android/libringclient/src/main/java/net/jami/conversation/ConversationPresenter.java @@ -20,21 +20,16 @@ */ package net.jami.conversation; -import java.io.File; - -import javax.inject.Inject; -import javax.inject.Named; - import net.jami.daemon.Blob; import net.jami.facades.ConversationFacade; import net.jami.model.Account; -import net.jami.model.Contact; +import net.jami.model.Call; import net.jami.model.Conference; +import net.jami.model.Contact; import net.jami.model.Conversation; import net.jami.model.DataTransfer; import net.jami.model.Error; import net.jami.model.Interaction; -import net.jami.model.Call; import net.jami.model.TrustRequest; import net.jami.model.Uri; import net.jami.mvp.RootPresenter; @@ -48,6 +43,12 @@ import net.jami.utils.Log; import net.jami.utils.StringUtils; import net.jami.utils.Tuple; import net.jami.utils.VCardUtils; + +import java.io.File; + +import javax.inject.Inject; +import javax.inject.Named; + import io.reactivex.Observable; import io.reactivex.Scheduler; import io.reactivex.disposables.CompositeDisposable; @@ -96,33 +97,22 @@ public class ConversationPresenter extends RootPresenter<ConversationView> { mCompositeDisposable.add(mVisibilityDisposable); } - /*@Override - public void bindView(ConversationView view) { - super.bindView(view); - mCompositeDisposable.add(mVisibilityDisposable); - if (mConversationDisposable == null && mConversation != null) - initView(mAccountService.getAccount(mConversation.getAccountId()), mConversation, view); - }*/ - public void init(Uri conversationUri, String accountId) { Log.w(TAG, "init " + conversationUri + " " + accountId); if (conversationUri.equals(mConversationUri)) return; mConversationUri = conversationUri; - Account account = mAccountService.getAccount(accountId); - if (account != null) { - initContact(account, account.getByUri(mConversationUri), getView()); - mCompositeDisposable.add(mConversationFacade.loadConversationHistory(account, conversationUri) - .observeOn(mUiScheduler) - .subscribe(c -> setConversation(account, c), e -> { - Log.e(TAG, "Error loading conversation", e); - getView().goToHome(); - })); - } else { - getView().goToHome(); - return; - } - + mCompositeDisposable.add(mConversationFacade.getAccountSubject(accountId) + .observeOn(mUiScheduler) + .flatMap(account -> mConversationFacade.loadConversationHistory(account, conversationUri) + .map(c -> { + setConversation(account, c); + return c; + })) + .subscribe(c -> {}, e -> { + Log.e(TAG, "Error loading conversation", e); + getView().goToHome(); + })); getView().setReadIndicatorStatus(showReadIndicator()); } @@ -186,11 +176,11 @@ public class ConversationPresenter extends RootPresenter<ConversationView> { mConversationDisposable.clear(); view.hideNumberSpinner(); - if (account.isJami() && !c.isSwarm()) { - String accountId = account.getAccountID(); - mConversationDisposable.add(c.getContact().getConversationUri() + if (account.isJami()) { + mConversationDisposable.add(c.getContact() + .getConversationUri() .observeOn(mUiScheduler) - .subscribe(uri -> init(uri, accountId))); + .subscribe(uri -> init(uri, account.getAccountID()))); } mConversationDisposable.add(Observable.combineLatest( @@ -277,7 +267,8 @@ public class ConversationPresenter extends RootPresenter<ConversationView> { } public void loadMore() { - mConversationFacade.loadMore(mConversation); + mConversationDisposable.add(mAccountService.loadMore(mConversation) + .subscribe(c -> {}, e-> {})); } public void openContact() { @@ -354,12 +345,11 @@ public class ConversationPresenter extends RootPresenter<ConversationView> { } private void sendTrustRequest() { - //final Uri contactId = mConversationUri; - Contact contact = mConversation.getContact();//mAccountService.getAccount(accountId).getContactFromCache(contactId); + Contact contact = mConversation.getContact(); if (contact != null) { contact.setStatus(Contact.Status.REQUEST_SENT); } - mVCardService.loadSmallVCard(mConversation.getAccountId(), VCardService.MAX_SIZE_REQUEST) + mVCardService.loadSmallVCardWithDefault(mConversation.getAccountId(), VCardService.MAX_SIZE_REQUEST) .subscribeOn(Schedulers.computation()) .subscribe(vCard -> mAccountService.sendTrustRequest(mConversation, contact.getUri(), Blob.fromString(VCardUtils.vcardToString(vCard))), e -> mAccountService.sendTrustRequest(mConversation, contact.getUri(), null)); @@ -462,7 +452,7 @@ public class ConversationPresenter extends RootPresenter<ConversationView> { } public void showPluginListHandlers() { - getView().showPluginListHandlers(mAccountId, mContactUri.getUri()); + getView().showPluginListHandlers(mConversation.getAccountId(), mConversationUri.getUri()); } public Tuple<String, String> getPath() { diff --git a/ring-android/libringclient/src/main/java/net/jami/facades/ConversationFacade.java b/ring-android/libringclient/src/main/java/net/jami/facades/ConversationFacade.java index da8d04ed7..ec34df577 100644 --- a/ring-android/libringclient/src/main/java/net/jami/facades/ConversationFacade.java +++ b/ring-android/libringclient/src/main/java/net/jami/facades/ConversationFacade.java @@ -38,7 +38,6 @@ import net.jami.services.HistoryService; import net.jami.services.NotificationService; import net.jami.services.PreferencesService; import net.jami.smartlist.SmartListViewModel; -import net.jami.utils.FileUtils; import net.jami.utils.Log; import net.jami.utils.Tuple; @@ -147,7 +146,7 @@ public class ConversationFacade { }) .distinctUntilChanged() .subscribe(t -> { - net.jami.utils.Log.e(TAG, "Location reception started for " + t.second.contact); + Log.e(TAG, "Location reception started for " + t.second.contact); mNotificationService.showLocationNotification(t.first, t.second.contact); mDisposableBag.add(t.second.location.doOnComplete(() -> mNotificationService.cancelLocationNotification(t.first, t.second.contact)).subscribe()); @@ -155,11 +154,11 @@ public class ConversationFacade { mDisposableBag.add(mAccountService .getMessageStateChanges() - .concatMapSingle(txt -> getAccountSubject(txt.getAccount()) - .map(a -> txt.getConversation() == null ? a.getSwarm(txt.getConversationId()) : a.getByUri(txt.getConversation().getParticipant())) - .doOnSuccess(conversation -> conversation.updateTextMessage(txt))) + .concatMapSingle(e -> getAccountSubject(e.getAccount()) + .map(a -> e.getConversation() == null ? a.getSwarm(e.getConversationId()) : a.getByUri(e.getConversation().getParticipant())) + .doOnSuccess(conversation -> conversation.updateInteraction(e))) .subscribe(c -> { - }, e -> net.jami.utils.Log.e(TAG, "Error updating text message", e))); + }, e -> Log.e(TAG, "Error updating text message", e))); mDisposableBag.add(mAccountService .getDataTransfers() @@ -203,7 +202,7 @@ public class ConversationFacade { if (lastMessage != null) { account.refreshed(conversation); if (mPreferencesService.getSettings().isAllowReadIndicator()) { - mAccountService.setMessageDisplayed(account.getAccountID(), conversation.getUri().getRawRingId(), lastMessage); + mAccountService.setMessageDisplayed(account.getAccountID(), conversation.getUri(), lastMessage); } if (cancelNotification) { mNotificationService.cancelTextNotification(account.getAccountID(), conversation.getUri()); @@ -264,13 +263,13 @@ public class ConversationFacade { } public void setIsComposing(String accountId, Uri conversationUri, boolean isComposing) { - mCallService.setIsComposing(accountId, conversationUri.getRawRingId(), isComposing); + mCallService.setIsComposing(accountId, conversationUri.getUri(), isComposing); } public Completable sendFile(Conversation conversation, Uri to, File file) { return Single.fromCallable(() -> { if (file == null || !file.exists() || !file.canRead()) { - net.jami.utils.Log.w(TAG, "sendFile: file not found or not readable: " + file); + Log.w(TAG, "sendFile: file not found or not readable: " + file); return null; } @@ -281,20 +280,19 @@ public class ConversationFacade { mHistoryService.insertInteraction(conversation.getAccountId(), conversation, transfer).blockingAwait(); } - File dest = mDeviceRuntimeService.getConversationPath(conversation.getUri().getRawRingId(), transfer.getStoragePath()); - if (!FileUtils.moveFile(file, dest)) { - net.jami.utils.Log.e(TAG, "sendFile: can't move file to " + dest); - return null; - } - - transfer.destination = dest; + transfer.destination = mDeviceRuntimeService.getConversationDir(conversation.getUri().getRawRingId()); return transfer; }) - .subscribeOn(Schedulers.io()) - .flatMapCompletable(mAccountService::sendFile); + .flatMap(t -> mAccountService.sendFile(file, t)) + .flatMapCompletable(transfer -> Completable.fromAction(() -> { + File destination = new File(transfer.destination, transfer.getStoragePath()); + if (!mDeviceRuntimeService.hardLinkOrCopy(file, destination)) { + Log.e(TAG, "sendFile: can't move file to " + destination); + } + })) + .subscribeOn(Schedulers.io()); } - public void deleteConversationItem(Conversation conversation, Interaction element) { if (element.getType() == Interaction.InteractionType.DATA_TRANSFER) { DataTransfer transfer = (DataTransfer) element; @@ -307,14 +305,13 @@ public class ConversationFacade { Completable.fromAction(file::delete) .subscribeOn(Schedulers.io())) .subscribe(() -> conversation.removeInteraction(transfer), - e -> net.jami.utils.Log.e(TAG, "Can't delete file transfer", e))); + e -> Log.e(TAG, "Can't delete file transfer", e))); } } else { // handling is the same for calls and texts - mDisposableBag.add(Completable.mergeArrayDelayError(mHistoryService.deleteInteraction(element.getId(), element.getAccount()).subscribeOn(Schedulers.io())) - .andThen(startConversation(element.getAccount(), Uri.fromString(element.getConversation().getParticipant()))) - .subscribe(c -> c.removeInteraction(element), - e -> net.jami.utils.Log.e(TAG, "Can't delete message", e))); + mDisposableBag.add(mHistoryService.deleteInteraction(element.getId(), element.getAccount()).subscribeOn(Schedulers.io()) + .subscribe(() -> conversation.removeInteraction(element), + e -> Log.e(TAG, "Can't delete message", e))); } } @@ -323,7 +320,7 @@ public class ConversationFacade { mCallService.cancelMessage(message.getAccount(), message.getId()).subscribeOn(Schedulers.io())) .andThen(startConversation(message.getAccount(), Uri.fromString(message.getConversation().getParticipant()))) .subscribe(c -> c.removeInteraction(message), - e -> net.jami.utils.Log.e(TAG, "Can't cancel message sending", e))); + e -> Log.e(TAG, "Can't cancel message sending", e))); } /** @@ -335,7 +332,7 @@ public class ConversationFacade { private Single<Account> loadSmartlist(final Account account) { synchronized (account) { if (account.historyLoader == null) { - net.jami.utils.Log.d(TAG, "loadSmartlist(): start loading"); + Log.d(TAG, "loadSmartlist(): start loading"); account.historyLoader = getSmartlist(account); } return account.historyLoader; @@ -354,31 +351,25 @@ public class ConversationFacade { if (conversation == null) return Single.error(new RuntimeException("Can't get conversation")); synchronized (conversation) { - if (conversation.isSwarm()) { - loadMore(conversation); - Single<Conversation> ret = Single.just(conversation); - conversation.setLoaded(ret); - return ret; - } - if (conversation.getId() == null) { + if (!conversation.isSwarm() && conversation.getId() == null) { return Single.just(conversation); } Single<Conversation> ret = conversation.getLoaded(); if (ret == null) { - ret = getConversationHistory(conversation); + ret = conversation.isSwarm() ? mAccountService.loadMore(conversation) : getConversationHistory(conversation); conversation.setLoaded(ret); } return ret; } } - private Observable<net.jami.smartlist.SmartListViewModel> observeConversation(Account account, Conversation conversation, boolean hasPresence) { + private Observable<SmartListViewModel> observeConversation(Account account, Conversation conversation, boolean hasPresence) { return Observable.merge(account.getConversationSubject() .filter(c -> c == conversation) .startWith(conversation), mContactService .observeContact(conversation.getAccountId(), conversation.getContacts(), hasPresence)) - .map(e -> new net.jami.smartlist.SmartListViewModel(conversation, hasPresence)); + .map(e -> new SmartListViewModel(conversation, hasPresence)); /*return account.getConversationSubject() .filter(c -> c == conversation) .startWith(conversation) @@ -387,49 +378,49 @@ public class ConversationFacade { .map(contact -> new SmartListViewModel(c, hasPresence)));*/ } - public Observable<List<Observable<net.jami.smartlist.SmartListViewModel>>> getSmartList(Observable<Account> currentAccount, boolean hasPresence) { + public Observable<List<Observable<SmartListViewModel>>> getSmartList(Observable<Account> currentAccount, boolean hasPresence) { return currentAccount.switchMap(account -> account.getConversationsSubject() .switchMapSingle(conversations -> Observable.fromIterable(conversations) .map(conversation -> observeConversation(account, conversation, hasPresence)) .toList())); } - public Observable<List<net.jami.smartlist.SmartListViewModel>> getContactList(Observable<Account> currentAccount) { + public Observable<List<SmartListViewModel>> getContactList(Observable<Account> currentAccount) { return currentAccount.switchMap(account -> account.getConversationsSubject() .switchMapSingle(conversations -> Observable.fromIterable(conversations) .filter(conversation -> !conversation.isSwarm()) - .map(conversation -> new net.jami.smartlist.SmartListViewModel(conversation, false)) + .map(conversation -> new SmartListViewModel(conversation, false)) .toList())); } - public Observable<List<Observable<net.jami.smartlist.SmartListViewModel>>> getPendingList(Observable<Account> currentAccount) { + public Observable<List<Observable<SmartListViewModel>>> getPendingList(Observable<Account> currentAccount) { return currentAccount.switchMap(account -> account.getPendingSubject() .switchMapSingle(conversations -> Observable.fromIterable(conversations) .map(conversation -> observeConversation(account, conversation, false)) .toList())); } - public Observable<List<Observable<net.jami.smartlist.SmartListViewModel>>> getSmartList(boolean hasPresence) { + public Observable<List<Observable<SmartListViewModel>>> getSmartList(boolean hasPresence) { return getSmartList(mAccountService.getCurrentAccountSubject(), hasPresence); } - public Observable<List<Observable<net.jami.smartlist.SmartListViewModel>>> getPendingList() { + public Observable<List<Observable<SmartListViewModel>>> getPendingList() { return getPendingList(mAccountService.getCurrentAccountSubject()); } - public Observable<List<net.jami.smartlist.SmartListViewModel>> getContactList() { + public Observable<List<SmartListViewModel>> getContactList() { return getContactList(mAccountService.getCurrentAccountSubject()); } - private Single<List<Observable<net.jami.smartlist.SmartListViewModel>>> getSearchResults(Account account, String query) { + private Single<List<Observable<SmartListViewModel>>> getSearchResults(Account account, String query) { Uri uri = Uri.fromString(query); if (account.isSip()) { Contact contact = account.getContactFromCache(uri); return mContactService.loadContactData(contact, account.getAccountID()) - .andThen(Single.just(Collections.singletonList(Observable.just(new net.jami.smartlist.SmartListViewModel(account.getAccountID(), contact, contact.getPrimaryNumber(), null))))); + .andThen(Single.just(Collections.singletonList(Observable.just(new SmartListViewModel(account.getAccountID(), contact, contact.getPrimaryNumber(), null))))); } else if (uri.isHexId()) { return mContactService.getLoadedContact(account.getAccountID(), account.getContactFromCache(uri)) - .map(contact -> Collections.singletonList(Observable.just(new net.jami.smartlist.SmartListViewModel(account.getAccountID(), contact, contact.getPrimaryNumber(), null)))); + .map(contact -> Collections.singletonList(Observable.just(new SmartListViewModel(account.getAccountID(), contact, contact.getPrimaryNumber(), null)))); } else if (account.canSearch() && !query.contains("@")) { return mAccountService.searchUser(account.getAccountID(), query) .map(AccountService.UserSearchResult::getResultsViewModels); @@ -439,22 +430,22 @@ public class ConversationFacade { } } - private Observable<List<Observable<net.jami.smartlist.SmartListViewModel>>> getSearchResults(Account account, Observable<String> query) { + private Observable<List<Observable<SmartListViewModel>>> getSearchResults(Account account, Observable<String> query) { return query.switchMapSingle(q -> q.isEmpty() - ? net.jami.smartlist.SmartListViewModel.EMPTY_LIST + ? SmartListViewModel.EMPTY_LIST : getSearchResults(account, q)) .distinctUntilChanged(); } - public Observable<List<Observable<net.jami.smartlist.SmartListViewModel>>> getFullList(Observable<Account> currentAccount, Observable<String> query, boolean hasPresence) { + public Observable<List<Observable<SmartListViewModel>>> getFullList(Observable<Account> currentAccount, Observable<String> query, boolean hasPresence) { return currentAccount.switchMap(account -> Observable.combineLatest( account.getConversationsSubject(), getSearchResults(account, query), query, (conversations, searchResults, q) -> { - List<Observable<net.jami.smartlist.SmartListViewModel>> newList = new ArrayList<>(conversations.size() + searchResults.size() + 2); + List<Observable<SmartListViewModel>> newList = new ArrayList<>(conversations.size() + searchResults.size() + 2); if (!searchResults.isEmpty()) { - newList.add(net.jami.smartlist.SmartListViewModel.TITLE_PUBLIC_DIR); + newList.add(SmartListViewModel.TITLE_PUBLIC_DIR); newList.addAll(searchResults); } if (!conversations.isEmpty()) { @@ -485,8 +476,13 @@ public class ConversationFacade { * @param account the user account */ private Single<Account> getSmartlist(final Account account) { - return mHistoryService.getSmartlist(account.getAccountID()) - .map(conversationHistoryList -> { + List<Completable> actions = new ArrayList<>(account.getConversations().size() + 1); + for (Conversation c : account.getConversations()) { + if (c.isSwarm()) + actions.add(c.getLastElementLoaded()); + } + actions.add(mHistoryService.getSmartlist(account.getAccountID()) + .flatMapCompletable(conversationHistoryList -> Completable.fromAction(() -> { List<Conversation> conversations = new ArrayList<>(); for (Interaction e : conversationHistoryList) { Conversation conversation = account.getByUri(e.getConversation().getParticipant()); @@ -497,8 +493,9 @@ public class ConversationFacade { conversations.add(conversation); } account.setHistoryLoaded(conversations); - return account; - }) + }))); + return Completable.merge(actions) + .andThen(Single.just(account)) .cache(); } @@ -547,7 +544,7 @@ public class ConversationFacade { } public void updateTextNotifications(String accountId, List<Conversation> conversations) { - net.jami.utils.Log.d(TAG, "updateTextNotifications() " + accountId + " " + conversations.size()); + Log.d(TAG, "updateTextNotifications() " + accountId + " " + conversations.size()); for (Conversation conversation : conversations) { mNotificationService.showTextNotification(accountId, conversation); @@ -561,9 +558,9 @@ public class ConversationFacade { } if (mPreferencesService.getSettings().isAllowReadIndicator()) { if (txt.getMessageId() != null) { - mAccountService.setMessageDisplayed(txt.getAccount(), txt.getConversationId(), txt.getMessageId()); + mAccountService.setMessageDisplayed(txt.getAccount(), new Uri(Uri.SWARM_SCHEME, txt.getConversationId()), txt.getMessageId()); } else { - mAccountService.setMessageDisplayed(txt.getAccount(), txt.getAuthor(), Long.toString(txt.getDaemonId(), 16)); + mAccountService.setMessageDisplayed(txt.getAccount(), new Uri(Uri.JAMI_URI_SCHEME, txt.getAuthor()), txt.getDaemonIdString()); } } } @@ -592,6 +589,7 @@ public class ConversationFacade { if (transfer.getStatus() == Interaction.InteractionStatus.TRANSFER_CREATED && !transfer.isOutgoing()) { if (transfer.canAutoAccept(mPreferencesService.getMaxFileAutoAccept(transfer.getAccount()))) { mAccountService.acceptFileTransfer(conversation, transfer); + return; } } mNotificationService.handleDataTransferNotification(transfer, conversation, conversation.isVisible()); @@ -614,13 +612,9 @@ public class ConversationFacade { String conversationId = call.getConversationId(); Log.w(TAG, "CallStateChange " + call.getId() + " conversationId:" + conversationId); - Conversation conversation = account == null - ? null - : (conversationId == null - ? (contact == null - ? null - : account.getByUri(contact.getUri())) - : account.getSwarm(conversationId)); + Conversation conversation = conversationId == null + ? (contact == null ? null : account.getByUri(contact.getUri())) + : account.getSwarm(conversationId); Conference conference = null; if (conversation != null) { conference = conversation.getConference(call.getDaemonIdString()); @@ -686,36 +680,48 @@ public class ConversationFacade { public void cancelFileTransfer(String accountId, Uri conversationId, long id) { mAccountService.cancelDataTransfer(accountId, conversationId.isSwarm() ? conversationId.getRawRingId() : "", id); - mNotificationService.removeTransferNotification(id); + mNotificationService.removeTransferNotification(accountId, conversationId, id); DataTransfer transfer = mAccountService.getAccount(accountId).getDataTransfer(id); if (transfer != null) deleteConversationItem((Conversation) transfer.getConversation(), transfer); } - public Completable removeConversation(String accountId, Uri contact) { - return mHistoryService - .clearHistory(contact.getUri(), accountId, true) - .doOnSubscribe(s -> { - Account account = mAccountService.getAccount(accountId); - account.clearHistory(contact, true); - mAccountService.removeContact(accountId, contact.getRawRingId(), false); - }); + public Completable removeConversation(String accountId, Uri conversationUri) { + if (conversationUri.isSwarm()) { + return mAccountService.removeConversation(accountId, conversationUri); + } else { + return mHistoryService + .clearHistory(conversationUri.getUri(), accountId, true) + .doOnSubscribe(s -> { + Account account = mAccountService.getAccount(accountId); + account.clearHistory(conversationUri, true); + mAccountService.removeContact(accountId, conversationUri.getRawRingId(), false); + }); + } } + public void banConversation(String accountId, Uri conversationUri) { + if (conversationUri.isSwarm()) { + startConversation(accountId, conversationUri) + .subscribe(conversation -> { + try { + Contact contact = conversation.getContact(); + mAccountService.removeContact(accountId, contact.getUri().getRawUriString(), true); + } catch (Exception e) { + mAccountService.removeConversation(accountId, conversationUri); + } + }); + //return mAccountService.removeConversation(accountId, conversationUri); + } else { + mAccountService.removeContact(accountId, conversationUri.getRawUriString(), true); + } + } + + public Single<Conversation> createConversation(String accountId, Collection<Contact> currentSelection) { List<String> contactIds = new ArrayList<>(currentSelection.size()); for (Contact contact : currentSelection) contactIds.add(contact.getPrimaryNumber()); return mAccountService.startConversation(accountId, contactIds); } - - public void loadMore(Conversation conversation) { - Collection<String> roots = conversation.getSwarmRoot(); - if (roots.isEmpty()) - mAccountService.loadConversationHistory(conversation.getAccountId(), conversation.getUri(), "", 16); - else { - for (String root : roots) - mAccountService.loadConversationHistory(conversation.getAccountId(), conversation.getUri(), root, 16); - } - } } \ No newline at end of file diff --git a/ring-android/libringclient/src/main/java/net/jami/model/Account.java b/ring-android/libringclient/src/main/java/net/jami/model/Account.java index 3e5d703dd..96e1e0927 100644 --- a/ring-android/libringclient/src/main/java/net/jami/model/Account.java +++ b/ring-android/libringclient/src/main/java/net/jami/model/Account.java @@ -132,16 +132,25 @@ public class Account { } public void removeSwarm(String conversationId) { + Log.w(TAG, "removeSwarm " + conversationId); Conversation conversation = swarmConversations.remove(conversationId); if (conversation != null) { synchronized (conversations) { - conversations.remove(conversation.getUri().getUri()); + Conversation c = conversations.remove(conversation.getUri().getUri()); + try { + Contact contact = c.getContact(); + Log.w(TAG, "removeSwarm: adding back contact conversation " + contact + " " + contact.getConversationUri().blockingFirst() + " " + c.getUri()); + if (contact.getConversationUri().blockingFirst().equals(c.getUri())) { + contact.setConversationUri(contact.getUri()); + contactAdded(contact); + } + } catch (Exception ignored) { + } } conversationChanged(); } } - public static class ContactLocation { public double latitude; public double longitude; @@ -913,8 +922,8 @@ public class Account { private void contactAdded(Contact contact) { Uri uri = contact.getUri(); String key = uri.getUri(); - //Log.w(TAG, "contactAdded " + getAccountID() + " " + key + " " + contact.getConversationUri()); - if (!contact.getConversationUri().blockingFirst().equals(contact.getUri())) { + //Log.w(TAG, "contactAdded " + getAccountID() + " " + uri + " " + contact.getConversationUri().blockingFirst()); + if (!contact.getConversationUri().blockingFirst().equals(uri)) { // Don't add conversation if we have a swarm conversation return; } diff --git a/ring-android/libringclient/src/main/java/net/jami/model/Call.java b/ring-android/libringclient/src/main/java/net/jami/model/Call.java index 3a3c52bc9..0e9a14cd8 100644 --- a/ring-android/libringclient/src/main/java/net/jami/model/Call.java +++ b/ring-android/libringclient/src/main/java/net/jami/model/Call.java @@ -22,6 +22,7 @@ package net.jami.model; import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import net.jami.utils.Log; import net.jami.utils.ProfileChunk; import net.jami.utils.StringUtils; import net.jami.utils.VCardUtils; @@ -35,6 +36,7 @@ import ezvcard.Ezvcard; import ezvcard.VCard; public class Call extends Interaction { + public final static String TAG = Call.class.getSimpleName(); public final static String KEY_ACCOUNT_ID = "ACCOUNTID"; public final static String KEY_AUDIO_ONLY = "AUDIO_ONLY"; @@ -50,6 +52,8 @@ public class Call extends Interaction { public final static String KEY_DURATION = "duration"; public final static String KEY_CONF_ID = "CONF_ID"; + private final String mIdDaemon; + private boolean isPeerHolding = false; private boolean isAudioMuted = false; private boolean isVideoMuted = false; @@ -69,7 +73,12 @@ public class Call extends Interaction { private ProfileChunk mProfileChunk = null; public Call(String daemonId, String author, String account, ConversationHistory conversation, Contact contact, Direction direction) { - mDaemonId = daemonId == null ? null : Long.parseLong(daemonId); + mIdDaemon = daemonId; + try { + mDaemonId = daemonId == null ? null : Long.parseLong(daemonId); + } catch (Exception e) { + Log.e(TAG, "Can't parse CallId " + mDaemonId); + } mAuthor = direction == Direction.INCOMING ? author : null; mAccount = account; mConversation = conversation; @@ -89,6 +98,7 @@ public class Call extends Interaction { mType = InteractionType.CALL.toString(); mStatus = interaction.getStatus().toString(); mDaemonId = interaction.getDaemonId(); + mIdDaemon = super.getDaemonIdString(); mIsRead = interaction.isRead() ? 1 : 0; mAccount = interaction.getAccount(); mExtraFlag = fromJson(interaction.getExtraFlag()); @@ -98,12 +108,17 @@ public class Call extends Interaction { } public Call(String daemonId, String account, String contactNumber, Direction direction, long timestamp) { - mDaemonId = daemonId == null ? null : Long.parseLong(daemonId); + mIdDaemon = daemonId; + try { + mDaemonId = daemonId == null ? null : Long.parseLong(daemonId); + } catch (Exception e) { + Log.e(TAG, "Can't parse CallId " + mDaemonId); + } mIsIncoming = direction == Direction.INCOMING; mAccount = account; mAuthor = direction == Direction.INCOMING ? contactNumber : null; mContactNumber = contactNumber; - mTimestamp = System.currentTimeMillis(); + mTimestamp = timestamp; mType = InteractionType.CALL.toString(); mIsRead = 1; } @@ -125,6 +140,11 @@ public class Call extends Interaction { mConfId = StringUtils.isEmpty(confId) ? null : confId; } + @Override + public String getDaemonIdString() { + return mIdDaemon; + } + public boolean isConferenceParticipant() { return mConfId != null; } @@ -249,12 +269,12 @@ public class Call extends Interaction { public VCard appendToVCard(Map<String, String> messages) { for (Map.Entry<String, String> message : messages.entrySet()) { - HashMap<String, String> messageKeyValue = net.jami.utils.VCardUtils.parseMimeAttributes(message.getKey()); - String mimeType = messageKeyValue.get(net.jami.utils.VCardUtils.VCARD_KEY_MIME_TYPE); - if (!net.jami.utils.VCardUtils.MIME_PROFILE_VCARD.equals(mimeType)) { + HashMap<String, String> messageKeyValue = VCardUtils.parseMimeAttributes(message.getKey()); + String mimeType = messageKeyValue.get(VCardUtils.VCARD_KEY_MIME_TYPE); + if (!VCardUtils.MIME_PROFILE_VCARD.equals(mimeType)) { continue; } - int part = Integer.parseInt(messageKeyValue.get(net.jami.utils.VCardUtils.VCARD_KEY_PART)); + int part = Integer.parseInt(messageKeyValue.get(VCardUtils.VCARD_KEY_PART)); int nbPart = Integer.parseInt(messageKeyValue.get(VCardUtils.VCARD_KEY_OF)); if (null == mProfileChunk) { mProfileChunk = new ProfileChunk(nbPart); diff --git a/ring-android/libringclient/src/main/java/net/jami/model/Contact.java b/ring-android/libringclient/src/main/java/net/jami/model/Contact.java index fdecb7a18..996a40607 100644 --- a/ring-android/libringclient/src/main/java/net/jami/model/Contact.java +++ b/ring-android/libringclient/src/main/java/net/jami/model/Contact.java @@ -89,7 +89,6 @@ public class Contact { public void setConversationUri(Uri conversationUri) { mConversationUri.onNext(conversationUri); - //mConversationUri = conversationUri; } public Observable<Uri> getConversationUri() { @@ -143,10 +142,9 @@ public class Contact { } public void setOnline(boolean present) { + mOnline = present; if (mContactPresenceEmitter != null) mContactPresenceEmitter.onNext(present); - mOnline = present; - mContactUpdates.onNext(this); } public void setSystemId(long id) { diff --git a/ring-android/libringclient/src/main/java/net/jami/model/Conversation.java b/ring-android/libringclient/src/main/java/net/jami/model/Conversation.java index b716d051e..37a24d1a1 100644 --- a/ring-android/libringclient/src/main/java/net/jami/model/Conversation.java +++ b/ring-android/libringclient/src/main/java/net/jami/model/Conversation.java @@ -36,10 +36,12 @@ import java.util.NavigableMap; import java.util.Set; import java.util.TreeMap; +import io.reactivex.Completable; import io.reactivex.Observable; import io.reactivex.Single; import io.reactivex.subjects.BehaviorSubject; import io.reactivex.subjects.PublishSubject; +import io.reactivex.subjects.SingleSubject; import io.reactivex.subjects.Subject; public class Conversation extends ConversationHistory { @@ -64,9 +66,9 @@ public class Conversation extends ConversationHistory { private final Subject<List<Contact>> mContactSubject = BehaviorSubject.create(); private Single<Conversation> isLoaded = null; + private Completable lastElementLoaded = null; private final Set<String> mRoots = new HashSet<>(2); - private final Set<String> mBranches = new HashSet<>(2); private final Map<String, Interaction> mMessages = new HashMap<>(16); private String lastRead = null; private final Mode mMode; @@ -76,6 +78,7 @@ public class Conversation extends ConversationHistory { // indicate the list needs sorting private boolean mDirty = false; + private SingleSubject<Conversation> mLoadingSubject = null; public Conversation(String accountId, Contact contact) { mAccountId = accountId; @@ -143,7 +146,6 @@ public class Conversation extends ConversationHistory { } else if (mContacts.size() == 1) { return mContacts.get(0).getDisplayName(); } - StringBuilder ret = new StringBuilder(); ArrayList<String> names = new ArrayList<>(mContacts.size()); int target = mContacts.size(); for (Contact c : mContacts) { @@ -152,12 +154,13 @@ public class Conversation extends ConversationHistory { continue; } String displayName = c.getDisplayName(); - if (!net.jami.utils.StringUtils.isEmpty(displayName)) { + if (!StringUtils.isEmpty(displayName)) { names.add(displayName); if (names.size() == 3) break; } } + StringBuilder ret = new StringBuilder(); ret.append(StringUtils.join(", ", names)); if (!names.isEmpty() && names.size() < target) { ret.append(" + ").append(mContacts.size() - names.size()); @@ -172,37 +175,33 @@ public class Conversation extends ConversationHistory { } else if (mContacts.size() == 1) { return mContacts.get(0).getRingUsername(); } - StringBuilder ret = new StringBuilder(); - Iterator<Contact> it = mContacts.iterator(); - while (it.hasNext()) { - Contact c = it.next(); + ArrayList<String> names = new ArrayList<>(mContacts.size()); + for (Contact c : mContacts) { if (c.isUser()) continue; - ret.append(c.getRingUsername()); - if (it.hasNext()) - ret.append(", "); + names.add(c.getRingUsername()); } - return ret.toString(); + return StringUtils.join(", ", names); } public Observable<List<Contact>> getContactUpdates() { return mContactSubject; } - public String readMessages() { + public synchronized String readMessages() { Interaction interaction = null; - for (String branch : mBranches) { - Interaction i = mMessages.get(branch); + //for (String branch : mBranches) { + Interaction i = mAggregateHistory.get(mAggregateHistory.size() - 1); if (i != null && !i.isRead()) { i.read(); interaction = i; lastRead = i.getMessageId(); } - } + //} return interaction == null ? null : interaction.getMessageId(); } - public Interaction getMessage(String messageId) { + public synchronized Interaction getMessage(String messageId) { return mMessages.get(messageId); } @@ -214,11 +213,41 @@ public class Conversation extends ConversationHistory { return lastRead; } + public SingleSubject<Conversation> getLoading() { + return mLoadingSubject; + } + + public boolean stopLoading() { + SingleSubject<Conversation> ret = mLoadingSubject; + mLoadingSubject = null; + if (ret != null) { + ret.onSuccess(this); + return true; + } + return false; + } + + public void setLoading(SingleSubject<Conversation> l) { + if (mLoadingSubject != null) { + if (!mLoadingSubject.hasValue() && !mLoadingSubject.hasThrowable()) + mLoadingSubject.onError(new IllegalStateException()); + } + mLoadingSubject = l; + } + + public Completable getLastElementLoaded() { + return lastElementLoaded; + } + + public void setLastElementLoaded(Completable c) { + lastElementLoaded = c; + } + public enum ElementStatus { UPDATE, REMOVE, ADD } - public Observable<net.jami.utils.Tuple<Interaction, ElementStatus>> getUpdatedElements() { + public Observable<Tuple<Interaction, ElementStatus>> getUpdatedElements() { return updatedElementSubject; } @@ -301,7 +330,7 @@ public class Conversation extends ConversationHistory { } mDirty = true; mAggregateHistory.add(call); - updatedElementSubject.onNext(new net.jami.utils.Tuple<>(call, ElementStatus.ADD)); + updatedElementSubject.onNext(new Tuple<>(call, ElementStatus.ADD)); } private void setInteractionProperties(Interaction interaction) { @@ -328,13 +357,13 @@ public class Conversation extends ConversationHistory { txt.read(); } if (txt.getConversation() == null) { - net.jami.utils.Log.e(TAG, "Error in conversation class... No conversation is attached to this interaction"); + Log.e(TAG, "Error in conversation class... No conversation is attached to this interaction"); } setInteractionProperties(txt); mHistory.put(txt.getTimestamp(), txt); mDirty = true; mAggregateHistory.add(txt); - updatedElementSubject.onNext(new net.jami.utils.Tuple<>(txt, ElementStatus.ADD)); + updatedElementSubject.onNext(new Tuple<>(txt, ElementStatus.ADD)); } public void addRequestEvent(TrustRequest request, Contact contact) { @@ -343,20 +372,20 @@ public class Conversation extends ConversationHistory { ContactEvent event = new ContactEvent(contact, request); mDirty = true; mAggregateHistory.add(event); - updatedElementSubject.onNext(new net.jami.utils.Tuple<>(event, ElementStatus.ADD)); + updatedElementSubject.onNext(new Tuple<>(event, ElementStatus.ADD)); } public void addContactEvent(Contact contact) { ContactEvent event = new ContactEvent(contact); mDirty = true; mAggregateHistory.add(event); - updatedElementSubject.onNext(new net.jami.utils.Tuple<>(event, ElementStatus.ADD)); + updatedElementSubject.onNext(new Tuple<>(event, ElementStatus.ADD)); } public void addContactEvent(ContactEvent contactEvent) { mDirty = true; mAggregateHistory.add(contactEvent); - updatedElementSubject.onNext(new net.jami.utils.Tuple<>(contactEvent, ElementStatus.ADD)); + updatedElementSubject.onNext(new Tuple<>(contactEvent, ElementStatus.ADD)); } public void addFileTransfer(DataTransfer dataTransfer) { @@ -365,42 +394,56 @@ public class Conversation extends ConversationHistory { } mDirty = true; mAggregateHistory.add(dataTransfer); - updatedElementSubject.onNext(new net.jami.utils.Tuple<>(dataTransfer, ElementStatus.ADD)); + updatedElementSubject.onNext(new Tuple<>(dataTransfer, ElementStatus.ADD)); } - public void updateTextMessage(TextMessage text) { + boolean isAfter(Interaction previous, Interaction query) { if (isSwarm()) { - TextMessage txt = (TextMessage) mMessages.get(text.getMessageId()); - if (txt != null) { - txt.setStatus(text.getStatus()); - updatedElementSubject.onNext(new net.jami.utils.Tuple<>(txt, ElementStatus.UPDATE)); - if (text.getStatus() == Interaction.InteractionStatus.DISPLAYED) { - if (lastDisplayed == null || lastDisplayed.getTimestamp() < text.getTimestamp()) { - lastDisplayed = text; - lastDisplayedSubject.onNext(text); + while (query != null && query.getParentIds() != null && !query.getParentIds().isEmpty()) { + if (query.getParentIds().contains(previous.getMessageId())) + return true; + query = mMessages.get(query.getParentIds().get(0)); + } + return false; + } else { + return previous.getTimestamp() < query.getTimestamp(); + } + } + + public void updateInteraction(Interaction element) { + Log.e(TAG, "updateInteraction: " + element.getMessageId() + " " + element.getStatus()); + if (isSwarm()) { + Interaction e = mMessages.get(element.getMessageId()); + if (e != null) { + e.setStatus(element.getStatus()); + updatedElementSubject.onNext(new Tuple<>(e, ElementStatus.UPDATE)); + if (e.getStatus() == Interaction.InteractionStatus.DISPLAYED) { + if (lastDisplayed == null || isAfter(lastDisplayed, e)) { + lastDisplayed = e; + lastDisplayedSubject.onNext(e); } } } else { - net.jami.utils.Log.e(TAG, "Can't find swarm message to update: " + text.getMessageId()); + Log.e(TAG, "Can't find swarm message to update: " + element.getMessageId()); } } else { - setInteractionProperties(text); - long time = text.getTimestamp(); + setInteractionProperties(element); + long time = element.getTimestamp(); NavigableMap<Long, Interaction> msgs = mHistory.subMap(time, true, time, true); for (Interaction txt : msgs.values()) { - if (txt.getId() == text.getId()) { - txt.setStatus(text.getStatus()); - updatedElementSubject.onNext(new net.jami.utils.Tuple<>(txt, ElementStatus.UPDATE)); - if (text.getStatus() == Interaction.InteractionStatus.DISPLAYED) { - if (lastDisplayed == null || lastDisplayed.getTimestamp() < text.getTimestamp()) { - lastDisplayed = text; - lastDisplayedSubject.onNext(text); + if (txt.getId() == element.getId()) { + txt.setStatus(element.getStatus()); + updatedElementSubject.onNext(new Tuple<>(txt, ElementStatus.UPDATE)); + if (element.getStatus() == Interaction.InteractionStatus.DISPLAYED) { + if (lastDisplayed == null || isAfter(lastDisplayed, element)) { + lastDisplayed = element; + lastDisplayedSubject.onNext(element); } } return; } } - net.jami.utils.Log.e(TAG, "Can't find message to update: " + text.getId()); + Log.e(TAG, "Can't find message to update: " + element.getId()); } } @@ -429,9 +472,7 @@ public class Conversation extends ConversationHistory { public Interaction getLastEvent() { sortHistory(); - if (mAggregateHistory.isEmpty()) - return null; - return mAggregateHistory.get(mAggregateHistory.size() - 1); + return mAggregateHistory.isEmpty() ? null : mAggregateHistory.get(mAggregateHistory.size() - 1); } public Conference getCurrentCall() { @@ -579,53 +620,67 @@ public class Conversation extends ConversationHistory { } } - private boolean isNewLeaf(List<String> roots) { - if (mBranches.isEmpty()) - return true; - boolean addLeaf = false; - for (String root : roots) { - if (mBranches.remove(root)) - addLeaf = true; - } - return addLeaf; - } - public boolean addSwarmElement(Interaction interaction) { if (mMessages.containsKey(interaction.getMessageId())) { return false; } - boolean newMessage = false; mMessages.put(interaction.getMessageId(), interaction); - if (mRoots.isEmpty() || mRoots.contains(interaction.getMessageId())) { - mRoots.remove(interaction.getMessageId()); - mRoots.addAll(interaction.getParentIds()); - // Log.w(TAG, "Found new roots for " + getUri() + " " + mRoots); - } + mRoots.remove(interaction.getMessageId()); + for (String parent : interaction.getParentIds()) + if (!mMessages.containsKey(parent)) { + mRoots.add(parent); + // Log.w(TAG, "@@@ Found new root for " + getUri() + " " + parent + " -> " + mRoots); + } if (lastRead != null && lastRead.equals(interaction.getMessageId())) interaction.read(); - if (isNewLeaf(interaction.getParentIds())) { - mBranches.add(interaction.getMessageId()); - if (isVisible()) { - interaction.read(); - setLastMessageRead(interaction.getMessageId()); - } - newMessage = true; - } + boolean newLeaf = false; + boolean added = false; if (mAggregateHistory.isEmpty() || interaction.getParentIds().contains(mAggregateHistory.get(mAggregateHistory.size()-1).getMessageId())) { // New leaf + // Log.w(TAG, "@@@ New end LEAF"); + added = true; + newLeaf = true; mAggregateHistory.add(interaction); - updatedElementSubject.onNext(new net.jami.utils.Tuple<>(interaction, ElementStatus.ADD)); + updatedElementSubject.onNext(new Tuple<>(interaction, ElementStatus.ADD)); } else { // New root or normal node for (int i = 0; i < mAggregateHistory.size(); i++) { if (mAggregateHistory.get(i).getParentIds() != null && mAggregateHistory.get(i).getParentIds().contains(interaction.getMessageId())) { + //Log.w(TAG, "@@@ New root node at " + i); mAggregateHistory.add(i, interaction); - updatedElementSubject.onNext(new net.jami.utils.Tuple<>(interaction, ElementStatus.ADD)); + updatedElementSubject.onNext(new Tuple<>(interaction, ElementStatus.ADD)); + added = true; break; } } + if (!added) { + for (int i = mAggregateHistory.size()-1; i >= 0; i--) { + if (interaction.getParentIds().contains(mAggregateHistory.get(i).getMessageId())) { + //Log.w(TAG, "@@@ New leaf at " + (i+1)); + added = true; + newLeaf = true; + mAggregateHistory.add(i+1, interaction); + updatedElementSubject.onNext(new Tuple<>(interaction, ElementStatus.ADD)); + break; + } + } + + } } - return newMessage; + if (newLeaf) { + if (isVisible()) { + interaction.read(); + setLastMessageRead(interaction.getMessageId()); + } + } + if (!added) { + Log.e(TAG, "Can't attach interaction " + interaction.getMessageId() + " with parents " + interaction.getParentIds()); + } + return newLeaf; + } + + public boolean isLoaded() { + return !mMessages.isEmpty() && mRoots.isEmpty(); } public Collection<String> getSwarmRoot() { @@ -636,7 +691,7 @@ public class Conversation extends ConversationHistory { DataTransfer dataTransfer = (DataTransfer) findConversationElement(transfer.getId()); if (dataTransfer != null) { dataTransfer.setStatus(eventCode); - updatedElementSubject.onNext(new net.jami.utils.Tuple<>(dataTransfer, ElementStatus.UPDATE)); + updatedElementSubject.onNext(new Tuple<>(dataTransfer, ElementStatus.UPDATE)); } } diff --git a/ring-android/libringclient/src/main/java/net/jami/model/DataTransfer.java b/ring-android/libringclient/src/main/java/net/jami/model/DataTransfer.java index 93a104521..079d8793f 100644 --- a/ring-android/libringclient/src/main/java/net/jami/model/DataTransfer.java +++ b/ring-android/libringclient/src/main/java/net/jami/model/DataTransfer.java @@ -45,7 +45,6 @@ public class DataTransfer extends Interaction { mAuthor = isOutgoing ? null : peer; mAccount = account; mConversation = conversation; - mAuthor = peer; mTotalSize = totalSize; mBytesProgress = bytesProgress; mBody = displayName; @@ -115,13 +114,18 @@ public class DataTransfer extends Interaction { } public String getStoragePath() { - if (net.jami.utils.StringUtils.isEmpty(mBody)) { + if (StringUtils.isEmpty(mBody)) { return getMessageId(); + } else { + String ext = StringUtils.getFileExtension(mBody); + if (ext.length() > 8) + ext = ext.substring(0, 8); + if (mDaemonId == null || mDaemonId == 0) { + return Long.toString(mId) + '_' + HashUtils.sha1(mBody) + '.' + ext; + } else { + return Long.toString(mDaemonId) + '_' + HashUtils.sha1(mBody) + '.' + ext; + } } - String ext = StringUtils.getFileExtension(mBody); - if (ext.length() > 8) - ext = ext.substring(0, 8); - return Long.toString(mId) + '_' + HashUtils.sha1(mBody) + '.' + ext; } public void setSize(long size) { diff --git a/ring-android/libringclient/src/main/java/net/jami/model/Interaction.java b/ring-android/libringclient/src/main/java/net/jami/model/Interaction.java index 07bfc9d14..b4608a6e7 100644 --- a/ring-android/libringclient/src/main/java/net/jami/model/Interaction.java +++ b/ring-android/libringclient/src/main/java/net/jami/model/Interaction.java @@ -73,6 +73,16 @@ public class Interaction { /* Needed by ORMLite */ public Interaction() { } + public Interaction(String accountId) { + mAccount = accountId; + setType(InteractionType.INVALID); + } + + public Interaction(Conversation conversation, InteractionType type) { + mConversation = conversation; + mAccount = conversation.getAccountId(); + mType = type.toString(); + } public Interaction(String id, String author, ConversationHistory conversation, String timestamp, String body, String type, String status, String daemonId, String isRead, String extraFlag) { mId = Integer.parseInt(id); @@ -152,6 +162,8 @@ public class Interaction { } public void setStatus(InteractionStatus status) { + if (status == InteractionStatus.DISPLAYED) + mIsRead = 1; mStatus = status.toString(); } @@ -175,6 +187,10 @@ public class Interaction { return mDaemonId == null ? null : Long.toString(mDaemonId); } + public void setDaemonId(long daemonId) { + mDaemonId = daemonId; + } + public String getMessageId() { return mMessageId; } @@ -237,7 +253,7 @@ public class Interaction { return INVALID; } - static InteractionStatus fromIntTextMessage(int n) { + public static InteractionStatus fromIntTextMessage(int n) { try { return values()[n]; } catch (ArrayIndexOutOfBoundsException e) { diff --git a/ring-android/libringclient/src/main/java/net/jami/model/TextMessage.java b/ring-android/libringclient/src/main/java/net/jami/model/TextMessage.java index 20053a668..e9a4d87cd 100644 --- a/ring-android/libringclient/src/main/java/net/jami/model/TextMessage.java +++ b/ring-android/libringclient/src/main/java/net/jami/model/TextMessage.java @@ -77,13 +77,4 @@ public class TextMessage extends Interaction { public void setNotified(boolean notified) { mNotified = notified; } - - public void setStatus(int status) { - if (status == 3) - mIsRead = 1; - - mStatus = InteractionStatus.fromIntTextMessage(status).toString(); - } - - } diff --git a/ring-android/libringclient/src/main/java/net/jami/services/AccountService.java b/ring-android/libringclient/src/main/java/net/jami/services/AccountService.java index 47d4e3c93..2d6323f90 100644 --- a/ring-android/libringclient/src/main/java/net/jami/services/AccountService.java +++ b/ring-android/libringclient/src/main/java/net/jami/services/AccountService.java @@ -59,7 +59,6 @@ import java.io.UnsupportedEncodingException; import java.net.SocketException; import java.net.URLEncoder; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; @@ -83,6 +82,7 @@ import io.reactivex.Single; import io.reactivex.schedulers.Schedulers; import io.reactivex.subjects.BehaviorSubject; import io.reactivex.subjects.PublishSubject; +import io.reactivex.subjects.SingleSubject; import io.reactivex.subjects.Subject; /** @@ -221,7 +221,7 @@ public class AccountService { }) .share(); - private final Subject<TextMessage> textMessageSubject = PublishSubject.create(); + private final Subject<Interaction> messageSubject = PublishSubject.create(); private final Subject<DataTransfer> dataTransferSubject = PublishSubject.create(); private final Subject<TrustRequest> incomingRequestsSubject = PublishSubject.create(); @@ -309,8 +309,8 @@ public class AccountService { return incomingLocationSubject; } - public Observable<TextMessage> getMessageStateChanges() { - return textMessageSubject; + public Observable<Interaction> getMessageStateChanges() { + return messageSubject; } public Observable<TrustRequest> getIncomingRequests() { @@ -402,11 +402,6 @@ public class AccountService { Conversation conversation = account.newSwarm(conversationId, mode); conversation.setLastMessageRead(mHistoryService.getLastMessageRead(accountId, conversation.getUri())); for (Map<String, String> member : JamiService.getConversationMembers(accountId, conversationId)) { -/* - for (Map.Entry<String, String> minfo : member.entrySet()) { - Log.w(TAG, accountId + " " + conversationId + " member " + minfo.getKey() + " -> " + minfo.getValue()); - } - //conversation.addContact(account.getContactFromCache(member.get("uri")));*/ Uri uri = Uri.fromId(member.get("uri")); Contact contact = conversation.findContact(uri); if (contact == null) { @@ -414,7 +409,7 @@ public class AccountService { conversation.addContact(contact); } } - Log.w(TAG, accountId + " " + conversation.getUri().getRawUriString() + " " + conversation.getContacts().size()); + conversation.setLastElementLoaded(Completable.defer(() -> loadMore(conversation, 2).ignoreElement()).cache()); account.conversationStarted(conversation); //account.addSwarmConversation(conversationId, members); } @@ -431,10 +426,6 @@ public class AccountService { account.addRequest(new TrustRequest(account.getAccountID(), from, conversationId)); } } - mExecutor.execute(() -> { - for (String conversationId : conversations) - JamiService.loadConversationMessages(accountId, conversationId, "", 2); - }); if (enabled) { for (Contact contact : account.getContacts().values()) { @@ -565,8 +556,7 @@ public class AccountService { .firstOrError() .map(accounts -> { for (Account account : accounts) { - String accountID = account.getAccountID(); - if (accountID.equals(accountId)) { + if (account.getAccountID().equals(accountId)) { return account; } } @@ -668,8 +658,8 @@ public class AccountService { }, e -> Log.w(TAG, "Not sending empty profile", e)); } - public void setMessageDisplayed(String accountId, String contactId, String messageId) { - mExecutor.execute(() -> JamiService.setMessageDisplayed(accountId, contactId, messageId, 3)); + public void setMessageDisplayed(String accountId, Uri conversationUri, String messageId) { + mExecutor.execute(() -> JamiService.setMessageDisplayed(accountId, conversationUri.getUri(), messageId, 3)); } public Single<Conversation> startConversation(String accountId, Collection<String> initialMembers) { @@ -685,15 +675,49 @@ public class AccountService { } account.conversationStarted(conversation); Log.w(TAG, "loadConversationMessages"); - JamiService.loadConversationMessages(accountId, id, id, 2); + //loadMore(conversation); + //JamiService.loadConversationMessages(accountId, id, id, 2); return conversation; }).subscribeOn(Schedulers.from(mExecutor)); } + public Completable removeConversation(String accountId, Uri conversationUri) { + return Completable.fromAction(() -> JamiService.removeConversation(accountId, conversationUri.getRawRingId())) + .subscribeOn(Schedulers.from(mExecutor)); + } + public void loadConversationHistory(String accountId, Uri conversationUri, String root, long n) { JamiService.loadConversationMessages(accountId, conversationUri.getRawRingId(), root, n); } + public Single<Conversation> loadMore(Conversation conversation) { + return loadMore(conversation, 16); + } + public Single<Conversation> loadMore(Conversation conversation, int n) { + synchronized (conversation) { + if (conversation.isLoaded()) { + Log.w(TAG, "loadMore: conversation already fully loaded"); + return Single.just(conversation); + } + + SingleSubject<Conversation> ret = conversation.getLoading(); + if (ret != null) + return ret; + ret = SingleSubject.create(); + Collection<String> roots = conversation.getSwarmRoot(); + Log.w(TAG, "loadMore " + conversation.getUri() + " " + roots); + + conversation.setLoading(ret); + if (roots.isEmpty()) + loadConversationHistory(conversation.getAccountId(), conversation.getUri(), "", n); + else { + for (String root : roots) + loadConversationHistory(conversation.getAccountId(), conversation.getUri(), root, n); + } + return ret; + } + } + public void sendConversationMessage(String accountId, Uri conversationUri, String txt) { mExecutor.execute(() -> { Log.w(TAG, "sendConversationMessages " + conversationUri.getRawRingId() + " : " + txt); @@ -1316,16 +1340,17 @@ public class AccountService { } void accountMessageStatusChanged(String accountId, String conversationId, String messageId, String peer, int status) { - Log.d(TAG, "accountMessageStatusChanged: " + accountId + ", " + conversationId + ", " + messageId + ", " + peer + ", " + status); + InteractionStatus newStatus = InteractionStatus.fromIntTextMessage(status); + Log.d(TAG, "accountMessageStatusChanged: " + accountId + ", " + conversationId + ", " + messageId + ", " + peer + ", " + newStatus); if (StringUtils.isEmpty(conversationId)) { mHistoryService - .accountMessageStatusChanged(accountId, messageId, peer, status) - .subscribe(textMessageSubject::onNext, e -> Log.e(TAG, "Error updating message: " + e.getLocalizedMessage())); + .accountMessageStatusChanged(accountId, messageId, peer, newStatus) + .subscribe(messageSubject::onNext, e -> Log.e(TAG, "Error updating message: " + e.getLocalizedMessage())); } else { - TextMessage msg = new TextMessage(peer, accountId, messageId, null, null); - msg.setStatus(status); + Interaction msg = new Interaction(accountId); + msg.setStatus(newStatus); msg.setSwarmInfo(conversationId, messageId, null); - textMessageSubject.onNext(msg); + messageSubject.onNext(msg); } } @@ -1492,11 +1517,13 @@ public class AccountService { private Interaction addMessage(Account account, Conversation conversation, Map<String, String> message) { String id = message.get("id"); - List<String> parents = Arrays.asList(message.get("parents").split(",")); - if (parents.size() == 1 && parents.get(0).isEmpty()) - parents = Collections.emptyList(); + //List<String> parents = Arrays.asList(message.get("parents").split(",")); + //if (parents.size() == 1 && parents.get(0).isEmpty()) + // parents = Collections.emptyList(); String type = message.get("type"); String author = message.get("author"); + String parent = message.get("linearizedParent"); + List<String> parents = StringUtils.isEmpty(parent) ? Collections.emptyList() : Collections.singletonList(parent); Uri authorUri = Uri.fromId(author); //Log.w(TAG, "addMessage2 " + type + " " + author + " id:" + id + " parents:" + parents); @@ -1516,13 +1543,22 @@ public class AccountService { interaction = new TextMessage(author, account.getAccountID(), timestamp, conversation, message.get("body"), !contact.isUser()); break; case "application/data-transfer+json": { - String transferId = message.get("tid"); - String fileName = message.get("displayName"); - long fileSize = Long.parseLong(message.get("totalSize")); - long tid = Long.parseUnsignedLong(transferId); - interaction = account.getDataTransfer(tid); - if (interaction == null) { - interaction = new DataTransfer(tid, account.getAccountID(), author, fileName, contact.isUser(), timestamp, fileSize, 0); + try { + String transferId = message.get("tid"); + long tid = Long.parseLong(transferId); + String fileName = message.get("displayName"); + long fileSize = Long.parseLong(message.get("totalSize")); + interaction = account.getDataTransfer(tid); + if (interaction == null) { + interaction = new DataTransfer(tid, account.getAccountID(), author, fileName, contact.isUser(), timestamp, fileSize, 0); + File path = mDeviceRuntimeService.getConversationPath(conversation.getUri().getRawRingId(), ((DataTransfer) interaction).getStoragePath()); + boolean exists = path.exists(); + if (exists) + ((DataTransfer) interaction).setBytesProgress(path.length()); + interaction.setStatus(exists ? InteractionStatus.TRANSFER_FINISHED : InteractionStatus.TRANSFER_TIMEOUT_EXPIRED); + } + } catch (Exception e) { + interaction = new Interaction(conversation, Interaction.InteractionType.INVALID); } break; } @@ -1532,10 +1568,7 @@ public class AccountService { break; case "merge": default: - interaction = new Interaction(); - interaction.setAccount(account.getAccountID()); - interaction.setConversation(conversation); - interaction.setType(Interaction.InteractionType.INVALID); + interaction = new Interaction(conversation, Interaction.InteractionType.INVALID); break; } interaction.setContact(contact); @@ -1549,15 +1582,18 @@ public class AccountService { public void conversationLoaded(String accountId, String conversationId, List<Map<String, String>> messages) { try { - Log.w(TAG, "ConversationCallback: conversationLoaded " + accountId + "/" + conversationId + " " + messages.size()); + // Log.w(TAG, "ConversationCallback: conversationLoaded " + accountId + "/" + conversationId + " " + messages.size()); Account account = getAccount(accountId); if (account == null) { Log.w(TAG, "conversationLoaded: can't find account"); return; } Conversation conversation = account.getSwarm(conversationId); - for (Map<String, String> message : messages) { - addMessage(account, conversation, message); + synchronized (conversation) { + for (Map<String, String> message : messages) { + addMessage(account, conversation, message); + } + conversation.stopLoading(); } account.conversationChanged(); } catch (Exception e) { @@ -1581,8 +1617,10 @@ public class AccountService { switch (ConversationMemberEvent.values()[event]) { case Add: case Join: { - Contact contact = account.getContactFromCache(uri); - conversation.addContact(contact); + Contact contact = conversation.findContact(uri); + if (contact == null) { + conversation.addContact(account.getContactFromCache(uri)); + } break; } case Remove: @@ -1646,8 +1684,8 @@ public class AccountService { Log.w(TAG, "ConversationCallback: messageReceived " + accountId + "/" + conversationId + " " + message.size()); Account account = getAccount(accountId); Conversation conversation = account.getSwarm(conversationId); - Interaction interaction = addMessage(account, conversation, message); - if (interaction != null) { + synchronized (conversation) { + Interaction interaction = addMessage(account, conversation, message); account.conversationUpdated(conversation); boolean isIncoming = !interaction.getContact().isUser(); if (isIncoming) { @@ -1656,22 +1694,32 @@ public class AccountService { } } - public Completable sendFile(final DataTransfer dataTransfer) { - return Completable.fromAction(() -> { + public Single<DataTransfer> sendFile(final File file, final DataTransfer dataTransfer) { + return Single.fromCallable(() -> { mStartingTransfer = dataTransfer; DataTransferInfo dataTransferInfo = new DataTransferInfo(); dataTransferInfo.setAccountId(dataTransfer.getAccount()); - dataTransferInfo.setConversationId(dataTransfer.getConversationId()); - dataTransferInfo.setPeer(dataTransfer.getConversationId()); - dataTransferInfo.setPath(dataTransfer.destination.getAbsolutePath()); + + String conversationId = dataTransfer.getConversationId(); + if (!StringUtils.isEmpty(conversationId)) + dataTransferInfo.setConversationId(conversationId); + else + dataTransferInfo.setPeer(dataTransfer.getConversation().getParticipant()); + + dataTransferInfo.setPath(file.getAbsolutePath()); dataTransferInfo.setDisplayName(dataTransfer.getDisplayName()); Log.i(TAG, "sendFile() id=" + dataTransfer.getId() + " accountId=" + dataTransferInfo.getAccountId() + ", peer=" + dataTransferInfo.getPeer() + ", filePath=" + dataTransferInfo.getPath()); - DataTransferError err = getDataTransferError(JamiService.sendFile(dataTransferInfo, 0)); + long[] id = new long[1]; + DataTransferError err = getDataTransferError(JamiService.sendFile(dataTransferInfo, id)); if (err != DataTransferError.SUCCESS) { throw new IOException(err.name()); + } else { + Log.e(TAG, "sendFile: got ID " + id[0]); + dataTransfer.setDaemonId(id[0]); } + return dataTransfer; }).subscribeOn(Schedulers.from(mExecutor)); } diff --git a/ring-android/libringclient/src/main/java/net/jami/services/CallService.java b/ring-android/libringclient/src/main/java/net/jami/services/CallService.java index e5f7f0190..7a6d4e64c 100644 --- a/ring-android/libringclient/src/main/java/net/jami/services/CallService.java +++ b/ring-android/libringclient/src/main/java/net/jami/services/CallService.java @@ -508,16 +508,18 @@ public class CallService { } call.setCallState(callState); + Account account = mAccountService.getAccount(call.getAccount()); - Contact contact = mContactService.findContact(mAccountService.getAccount(call.getAccount()), Uri.fromString(call.getContactNumber())); + Contact contact = mContactService.findContact(account, Uri.fromString(call.getContactNumber())); String registeredName = callDetails.get(Call.KEY_REGISTERED_NAME); if (registeredName != null && !registeredName.isEmpty()) { contact.setUsername(registeredName); } - call.setContact(contact); - Account account = mAccountService.getAccount(call.getAccount()); - call.setConversation(account.getByUri(contact.getUri())); + Conversation conversation = account.getByUri(contact.getConversationUri().blockingFirst()); + call.setContact(contact); + call.setConversation(conversation); + Log.w(TAG, "parseCallState " + contact + " " + contact.getConversationUri().blockingFirst() + " " + conversation + " " + conversation.getParticipant()); currentCalls.put(callId, call); updateConnectionCount(); @@ -525,7 +527,6 @@ public class CallService { return call; } - public void connectionUpdate(String id, int state) { // Log.d(TAG, "connectionUpdate: " + id + " " + state); /*switch(state) { diff --git a/ring-android/libringclient/src/main/java/net/jami/services/ContactService.java b/ring-android/libringclient/src/main/java/net/jami/services/ContactService.java index f5a863a8f..c9f2c22b4 100644 --- a/ring-android/libringclient/src/main/java/net/jami/services/ContactService.java +++ b/ring-android/libringclient/src/main/java/net/jami/services/ContactService.java @@ -119,11 +119,14 @@ public abstract class ContactService { }) .filter(c -> c.isUsernameLoaded() && c.detailsLoaded) .replay(1) - .refCount(5, TimeUnit.SECONDS)); + .refCount()); } return withPresence - ? Observable.combineLatest(contact.getUpdates(), contact.getPresenceUpdates(), (c, p) -> c) + ? Observable.combineLatest(contact.getUpdates(), contact.getPresenceUpdates(), (c, p) -> { + //Log.w(TAG, "observeContact UPDATE " + c + " " + p); + return c; + }) : contact.getUpdates(); } } diff --git a/ring-android/libringclient/src/main/java/net/jami/services/DeviceRuntimeService.java b/ring-android/libringclient/src/main/java/net/jami/services/DeviceRuntimeService.java index 92d718b22..4f551e896 100644 --- a/ring-android/libringclient/src/main/java/net/jami/services/DeviceRuntimeService.java +++ b/ring-android/libringclient/src/main/java/net/jami/services/DeviceRuntimeService.java @@ -31,6 +31,7 @@ public abstract class DeviceRuntimeService implements DaemonService.SystemInfoCa public abstract File getFilePath(String name); public abstract File getConversationPath(String conversationId, String name); public abstract File getTemporaryPath(String conversationId, String name); + public abstract File getConversationDir(String conversationId); public abstract String getPushToken(); @@ -56,4 +57,5 @@ public abstract class DeviceRuntimeService implements DaemonService.SystemInfoCa public abstract String getProfileName(); + public abstract boolean hardLinkOrCopy(File source, File dest); } diff --git a/ring-android/libringclient/src/main/java/net/jami/services/HistoryService.java b/ring-android/libringclient/src/main/java/net/jami/services/HistoryService.java index 4579c2444..02ba0257a 100644 --- a/ring-android/libringclient/src/main/java/net/jami/services/HistoryService.java +++ b/ring-android/libringclient/src/main/java/net/jami/services/HistoryService.java @@ -220,7 +220,7 @@ public abstract class HistoryService { } - Single<TextMessage> accountMessageStatusChanged(String accountId, String daemonId, String peer, int status) { + Single<TextMessage> accountMessageStatusChanged(String accountId, String daemonId, String peer, Interaction.InteractionStatus status) { return Single.fromCallable(() -> { List<Interaction> textList = getInteractionDataDao(accountId).queryForEq(Interaction.COLUMN_DAEMON_ID, daemonId); if (textList == null || textList.isEmpty()) { diff --git a/ring-android/libringclient/src/main/java/net/jami/services/NotificationService.java b/ring-android/libringclient/src/main/java/net/jami/services/NotificationService.java index 2eb00b6f8..76e5f4d60 100644 --- a/ring-android/libringclient/src/main/java/net/jami/services/NotificationService.java +++ b/ring-android/libringclient/src/main/java/net/jami/services/NotificationService.java @@ -37,38 +37,30 @@ public interface NotificationService { String KEY_NOTIFICATION_ID = "notificationId"; Object showCallNotification(int callId); - - void showTextNotification(String accountId, Conversation conversation); - void cancelCallNotification(); + void handleCallNotification(Conference conference, boolean remove); + void showMissedCallNotification(Call call); - void cancelTextNotification(Uri contact); - - void cancelTextNotification(String accountId, Uri conversationUri); + void showTextNotification(String accountId, Conversation conversation); + void cancelTextNotification(String accountId, Uri contact); void cancelAll(); void showIncomingTrustRequestNotification(Account account); - void cancelTrustRequestNotification(String accountID); void showFileTransferNotification(Conversation conversation, DataTransfer info); - - void showMissedCallNotification(Call call); - void cancelFileNotification(int id, boolean isMigratingToService); + void handleDataTransferNotification(DataTransfer transfer, Conversation contact, boolean remove); + void removeTransferNotification(String accountId, Uri conversationUri, long transferId); + Object getDataTransferNotification(int notificationId); void updateNotification(Object notification, int notificationId); Object getServiceNotification(); - void handleCallNotification(Conference conference, boolean remove); - - void handleDataTransferNotification(DataTransfer transfer, Conversation contact, boolean remove); - void removeTransferNotification(long transferId); - Object getDataTransferNotification(int notificationId); void onConnectionUpdate(Boolean b); diff --git a/ring-android/libringclient/src/main/java/net/jami/services/VCardService.java b/ring-android/libringclient/src/main/java/net/jami/services/VCardService.java index 1d3948371..ab078dc79 100644 --- a/ring-android/libringclient/src/main/java/net/jami/services/VCardService.java +++ b/ring-android/libringclient/src/main/java/net/jami/services/VCardService.java @@ -23,6 +23,7 @@ import java.io.File; import net.jami.utils.Tuple; import ezvcard.VCard; +import io.reactivex.Maybe; import io.reactivex.Single; public abstract class VCardService { @@ -30,7 +31,12 @@ public abstract class VCardService { public static final int MAX_SIZE_SIP = 256 * 1024; public static final int MAX_SIZE_REQUEST = 16 * 1024; - public abstract Single<VCard> loadSmallVCard(String accountId, int maxSize); + public abstract Maybe<VCard> loadSmallVCard(String accountId, int maxSize); + public Single<VCard> loadSmallVCardWithDefault(String accountId, int maxSize) { + return loadSmallVCard(accountId, maxSize) + .switchIfEmpty(Single.fromCallable(VCard::new)); + } + public abstract Single<VCard> saveVCardProfile(String accountId, String uri, String displayName, String picture); public abstract Single<Tuple<String, Object>> loadVCardProfile(VCard vcard); public abstract Single<Tuple<String, Object>> peerProfileReceived(String accountId, String peerId, File vcard); diff --git a/ring-android/libringclient/src/main/java/net/jami/smartlist/SmartListPresenter.java b/ring-android/libringclient/src/main/java/net/jami/smartlist/SmartListPresenter.java index 88c597e1a..a643e2427 100644 --- a/ring-android/libringclient/src/main/java/net/jami/smartlist/SmartListPresenter.java +++ b/ring-android/libringclient/src/main/java/net/jami/smartlist/SmartListPresenter.java @@ -20,13 +20,6 @@ */ package net.jami.smartlist; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import javax.inject.Inject; -import javax.inject.Named; - import net.jami.facades.ConversationFacade; import net.jami.model.Account; import net.jami.model.Uri; @@ -34,6 +27,14 @@ import net.jami.mvp.RootPresenter; import net.jami.services.AccountService; import net.jami.services.ContactService; import net.jami.utils.Log; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import javax.inject.Inject; +import javax.inject.Named; + import io.reactivex.Observable; import io.reactivex.Scheduler; import io.reactivex.disposables.CompositeDisposable; @@ -101,11 +102,11 @@ public class SmartListPresenter extends RootPresenter<SmartListView> { } } - public void conversationClicked(net.jami.smartlist.SmartListViewModel viewModel) { + public void conversationClicked(SmartListViewModel viewModel) { startConversation(viewModel.getAccountId(), viewModel.getUri()); } - public void conversationLongClicked(net.jami.smartlist.SmartListViewModel smartListViewModel) { + public void conversationLongClicked(SmartListViewModel smartListViewModel) { getView().displayConversationDialog(smartListViewModel); } @@ -129,11 +130,11 @@ public class SmartListPresenter extends RootPresenter<SmartListView> { getView().goToConversation(mAccount.getAccountID(), uri); } - public void copyNumber(net.jami.smartlist.SmartListViewModel smartListViewModel) { + public void copyNumber(SmartListViewModel smartListViewModel) { getView().copyNumber(smartListViewModel.getUri()); } - public void clearConversation(net.jami.smartlist.SmartListViewModel smartListViewModel) { + public void clearConversation(SmartListViewModel smartListViewModel) { getView().displayClearDialog(smartListViewModel.getUri()); } @@ -143,30 +144,33 @@ public class SmartListPresenter extends RootPresenter<SmartListView> { .subscribeOn(Schedulers.computation()).subscribe()); } - public void removeConversation(net.jami.smartlist.SmartListViewModel smartListViewModel) { + public void removeConversation(SmartListViewModel smartListViewModel) { getView().displayDeleteDialog(smartListViewModel.getUri()); } public void removeConversation(Uri uri) { mConversationDisposable.add(mConversationFacade .removeConversation(mAccount.getAccountID(), uri) - .subscribeOn(Schedulers.computation()).subscribe()); + .subscribe()); } + public void banContact(SmartListViewModel smartListViewModel) { + mConversationFacade.banConversation(smartListViewModel.getAccountId(), smartListViewModel.getUri()); + } public void clickQRSearch() { getView().goToQRFragment(); } - void showConversations(Observable<List<Observable<net.jami.smartlist.SmartListViewModel>>> conversations) { + void showConversations(Observable<List<Observable<SmartListViewModel>>> conversations) { mConversationDisposable.clear(); getView().setLoading(true); mConversationDisposable.add(conversations - .switchMap(viewModels -> viewModels.isEmpty() ? net.jami.smartlist.SmartListViewModel.EMPTY_RESULTS + .switchMap(viewModels -> viewModels.isEmpty() ? SmartListViewModel.EMPTY_RESULTS : Observable.combineLatest(viewModels, obs -> { - List<net.jami.smartlist.SmartListViewModel> vms = new ArrayList<>(obs.length); + List<SmartListViewModel> vms = new ArrayList<>(obs.length); for (Object ob : obs) - vms.add((net.jami.smartlist.SmartListViewModel) ob); + vms.add((SmartListViewModel) ob); return vms; })) .throttleLatest(150, TimeUnit.MILLISECONDS, mUiScheduler) @@ -188,9 +192,4 @@ public class SmartListPresenter extends RootPresenter<SmartListView> { showConversations(mConversationFacade.getFullList(accountSubject, mCurrentQuery, true)); } - public void banContact(SmartListViewModel smartListViewModel) { - //CallContact contact = smartListViewModel.getContact(); - if (smartListViewModel.getContact().size() == 1) - mAccountService.removeContact(mAccount.getAccountID(), smartListViewModel.getContact().get(0).getPrimaryNumber(), true); - } } diff --git a/ring-android/libringclient/src/main/java/net/jami/smartlist/SmartListViewModel.java b/ring-android/libringclient/src/main/java/net/jami/smartlist/SmartListViewModel.java index bdf3210d0..2e9eccdfe 100644 --- a/ring-android/libringclient/src/main/java/net/jami/smartlist/SmartListViewModel.java +++ b/ring-android/libringclient/src/main/java/net/jami/smartlist/SmartListViewModel.java @@ -68,7 +68,7 @@ public class SmartListViewModel this.hasOngoingCall = false; this.lastEvent = lastEvent; showPresence = true; - //isOnline = contact.isOnline(); + isOnline = contact.isOnline(); title = Title.None; } public SmartListViewModel(String accountId, Contact contact, String id, Interaction lastEvent) { @@ -102,7 +102,6 @@ public class SmartListViewModel break; } } - //isOnline = contact.isOnline(); showPresence = presence; title = Title.None; } @@ -126,7 +125,7 @@ public class SmartListViewModel return uri; } - public List<Contact> getContact() { + public List<Contact> getContacts() { return contact; } @@ -163,6 +162,10 @@ public class SmartListViewModel return showPresence; } + public boolean isOnline() { + return isOnline; + } + public boolean isChecked() { return isChecked; } public void setChecked(boolean checked) { diff --git a/ring-android/libringclient/src/main/java/net/jami/utils/VCardUtils.java b/ring-android/libringclient/src/main/java/net/jami/utils/VCardUtils.java index 38fae1eac..b49650b31 100644 --- a/ring-android/libringclient/src/main/java/net/jami/utils/VCardUtils.java +++ b/ring-android/libringclient/src/main/java/net/jami/utils/VCardUtils.java @@ -166,7 +166,7 @@ public final class VCardUtils { */ private static VCard loadFromDisk(File path) throws IOException { if (path == null || !path.exists()) { - Log.d(TAG, "vcardPath not exist " + path); + // Log.d(TAG, "vcardPath not exist " + path); return null; } if (path.length() > VCARD_MAX_SIZE) { -- GitLab