diff --git a/build-daemon.sh b/build-daemon.sh index 60cb2fd8edfb6985e2c0a70e18609f28220f3496..8a38aa3656e62cca626ff383994ee636db71873a 100755 --- a/build-daemon.sh +++ b/build-daemon.sh @@ -171,6 +171,7 @@ STATIC_LIBS_ALL="-llog -lOpenSLES -landroid \ -lpjlib-util-${PJ_TARGET} \ -lpj-${PJ_TARGET} \ -lupnp -lixml \ + -lgit2 \ -larchive \ -lsecp256k1 \ -lgnutls -lhogweed -lnettle -lgmp \ diff --git a/ring-android/app/build.gradle b/ring-android/app/build.gradle index e72dadbc5f8bec4b6637531150101f1bcfc0bb12..7956753bdd1a784bfe232269088ddfb207c6d4b4 100644 --- a/ring-android/app/build.gradle +++ b/ring-android/app/build.gradle @@ -107,7 +107,7 @@ dependencies { implementation 'com.google.zxing:core:3.3.3' // RxBindings - implementation 'com.jakewharton.rxbinding3:rxbinding:3.1.0' + //implementation 'com.jakewharton.rxbinding3:rxbinding:3.1.0' implementation 'com.rodolfonavalon:ShapeRippleLibrary:1.0.0' diff --git a/ring-android/app/src/main/java/cx/ring/account/JamiAccountSummaryFragment.java b/ring-android/app/src/main/java/cx/ring/account/JamiAccountSummaryFragment.java index 342294bf743a96f70aa96d9f4ce986cd6272e5b3..cfcbeda721c11bd06eba1964c1e0a6eb2cd9e9af 100644 --- a/ring-android/app/src/main/java/cx/ring/account/JamiAccountSummaryFragment.java +++ b/ring-android/app/src/main/java/cx/ring/account/JamiAccountSummaryFragment.java @@ -330,8 +330,8 @@ public class JamiAccountSummaryFragment extends BaseSupportFragment<JamiAccountS boolean hasRegisteredName = !currentRegisteredName && username != null && !username.isEmpty(); mBinding.groupRegisteringName.setVisibility(currentRegisteredName ? View.VISIBLE : View.GONE); mBinding.btnShare.setOnClickListener(v -> shareAccount(hasRegisteredName? username : account.getUsername())); - mBinding.registerName.setVisibility(hasRegisteredName? View.GONE : View.VISIBLE); - mBinding.registeredName.setText(hasRegisteredName? username : getResources().getString(R.string.no_registered_name_for_account)); + mBinding.registerName.setVisibility(hasRegisteredName ? View.GONE : View.VISIBLE); + mBinding.registeredName.setText(hasRegisteredName ? username : getResources().getString(R.string.no_registered_name_for_account)); mBinding.btnQr.setOnClickListener(v -> QRCodeFragment.newInstance(QRCodeFragment.INDEX_CODE).show(getParentFragmentManager(), QRCodeFragment.TAG)); mBinding.username.setOnFocusChangeListener((v, hasFocus) -> { Editable name = mBinding.username.getText(); 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 9da488962cd31e90abcf5d23e626898c78c74b52..a28b6673cffeef6d2a48d1f290e98f223c44e8ca 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 @@ -32,6 +32,7 @@ import android.graphics.drawable.Drawable; import android.media.MediaPlayer; import android.net.Uri; import android.os.Build; +import android.text.TextUtils; import android.text.format.DateUtils; import android.text.format.Formatter; import android.util.Log; @@ -49,6 +50,7 @@ import android.view.animation.AnimationUtils; import android.widget.FrameLayout; import android.widget.ImageView; +import androidx.annotation.LayoutRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.cardview.widget.CardView; @@ -112,13 +114,13 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationViewHo private int expandedItemPosition = -1; private int lastDeliveredPosition = -1; private int lastDisplayedPosition = -1; - private Observable<Long> timestampUpdateTimer; + private final Observable<Long> timestampUpdateTimer; private int lastMsgPos = -1; private boolean isComposing = false; private boolean mShowReadIndicator = true; - private static int[] msgBGLayouts = new int[] { + private static final int[] msgBGLayouts = new int[] { R.drawable.textmsg_bg_out_first, R.drawable.textmsg_bg_out_middle, R.drawable.textmsg_bg_out_last, @@ -164,12 +166,31 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationViewHo notifyDataSetChanged(); } - public void add(Interaction e) { - boolean update = !mInteractions.isEmpty(); - mInteractions.add(e); - notifyItemInserted(mInteractions.size() - 1); - if (update) - notifyItemChanged(mInteractions.size() - 2); + public boolean add(Interaction e) { + if (!TextUtils.isEmpty(e.getMessageId())) { + if (mInteractions.isEmpty() || e.getParentIds().contains(mInteractions.get(mInteractions.size()-1).getMessageId())) { + boolean update = !mInteractions.isEmpty(); + mInteractions.add(e); + notifyItemInserted(mInteractions.size()-1); + if (update) + notifyItemChanged(mInteractions.size()-2); + return true; + } + for (int i = 0, n = mInteractions.size(); i<n; i++) { + if (mInteractions.get(i).getParentIds().contains(e.getMessageId())) { + mInteractions.add(i, e); + notifyItemInserted(i); + return i == n-1; + } + } + } else { + boolean update = !mInteractions.isEmpty(); + mInteractions.add(e); + notifyItemInserted(mInteractions.size() - 1); + if (update) + notifyItemChanged(mInteractions.size() - 2); + } + return true; } public void update(Interaction e) { @@ -253,16 +274,18 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationViewHo } } return out; + case INVALID: + return MessageType.INVALID.ordinal(); } } - return MessageType.CALL_INFORMATION.ordinal(); + return -1; } @NonNull @Override public ConversationViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { MessageType type = MessageType.values()[viewType]; - ViewGroup v = (ViewGroup) LayoutInflater.from(parent.getContext()).inflate(type.layout, parent, false); + ViewGroup v = type == MessageType.INVALID ? new FrameLayout(parent.getContext()) : (ViewGroup) LayoutInflater.from(parent.getContext()).inflate(type.layout, parent, false); return new ConversationViewHolder(v, type); } @@ -287,14 +310,20 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationViewHo conversationViewHolder.itemView.startAnimation(animation); } - if (interaction.getType() == (InteractionType.TEXT)) { - configureForTextMessage(conversationViewHolder, interaction, position); - } else if (interaction.getType() == (InteractionType.CALL)) { - configureForCallInfo(conversationViewHolder, interaction); - } else if (interaction.getType() == (InteractionType.CONTACT)) { - configureForContactEvent(conversationViewHolder, interaction); - } else if (interaction.getType() == (InteractionType.DATA_TRANSFER)) { - configureForFileInfo(conversationViewHolder, interaction, position); + //Log.w(TAG, "onBindViewHolder " + interaction.getType() + " " + interaction); + if (interaction.getType() == InteractionType.INVALID) { + conversationViewHolder.itemView.setVisibility(View.GONE); + } else { + conversationViewHolder.itemView.setVisibility(View.VISIBLE); + if (interaction.getType() == InteractionType.TEXT) { + configureForTextMessage(conversationViewHolder, interaction, position); + } else if (interaction.getType() == InteractionType.CALL) { + configureForCallInfo(conversationViewHolder, interaction); + } else if (interaction.getType() == InteractionType.CONTACT) { + configureForContactEvent(conversationViewHolder, interaction); + } else if (interaction.getType() == InteractionType.DATA_TRANSFER) { + configureForFileInfo(conversationViewHolder, interaction, position); + } } } @@ -584,7 +613,7 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationViewHo private void configureForFileInfo(@NonNull final ConversationViewHolder viewHolder, @NonNull final Interaction interaction, int position) { DataTransfer file = (DataTransfer) interaction; - File path = presenter.getDeviceRuntimeService().getConversationPath(file.getPeerId(), file.getStoragePath()); + File path = presenter.getDeviceRuntimeService().getConversationPath(interaction.getConversationId() == null ? interaction.getConversation().getParticipant() : interaction.getConversationId(), file.getStoragePath()); if (file.isComplete()) file.setSize(path.length()); @@ -697,28 +726,8 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationViewHo if (file.getStatus() == InteractionStatus.TRANSFER_AWAITING_HOST) { viewHolder.mAnswerLayout.setVisibility(View.VISIBLE); - viewHolder.btnAccept.setOnClickListener(v -> { - if (!presenter.getDeviceRuntimeService().hasWriteExternalStoragePermission()) { - conversationFragment.askWriteExternalStoragePermission(); - return; - } - Context context = v.getContext(); - File cacheDir = context.getCacheDir(); - long spaceLeft = AndroidFileUtils.getSpaceLeft(cacheDir.toString()); - if (spaceLeft == -1L || file.getTotalSize() > spaceLeft) { - presenter.noSpaceLeft(); - return; - } - context.startService(new Intent(DRingService.ACTION_FILE_ACCEPT) - .setClass(context.getApplicationContext(), DRingService.class) - .putExtra(DRingService.KEY_TRANSFER_ID, file.getDaemonId())); - }); - viewHolder.btnRefuse.setOnClickListener(v -> { - Context context = v.getContext(); - context.startService(new Intent(DRingService.ACTION_FILE_CANCEL) - .setClass(context.getApplicationContext(), DRingService.class) - .putExtra(DRingService.KEY_TRANSFER_ID, file.getDaemonId())); - }); + viewHolder.btnAccept.setOnClickListener(v -> presenter.acceptFile(file)); + viewHolder.btnRefuse.setOnClickListener(v -> presenter.refuseFile(file)); } else { viewHolder.mAnswerLayout.setVisibility(View.GONE); if (file.getStatus() == InteractionStatus.TRANSFER_ONGOING) { @@ -769,9 +778,7 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationViewHo } // Log.w(TAG, "configureForTextMessage " + position + " " + interaction.getDaemonId() + " " + interaction.getStatus()); - convViewHolder.mCid = textMessage.getConversation().getParticipant(); String message = textMessage.getBody().trim(); - View longPressView = convViewHolder.mMsgTxt; longPressView.getBackground().setTintList(null); @@ -795,13 +802,13 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationViewHo if (expandedItemPosition == position) { expandedItemPosition = -1; } - conversationFragment.updatePosition(convViewHolder.getAdapterPosition()); + conversationFragment.updatePosition(convViewHolder.getBindingAdapterPosition()); if (textMessage.isIncoming()) { longPressView.getBackground().setTint(conversationFragment.getResources().getColor(R.color.grey_500)); } else { longPressView.getBackground().setTint(conversationFragment.getResources().getColor(R.color.blue_900)); } - mCurrentLongItem = new RecyclerViewContextMenuInfo(convViewHolder.getAdapterPosition(), v.getId()); + mCurrentLongItem = new RecyclerViewContextMenuInfo(convViewHolder.getBindingAdapterPosition(), v.getId()); return false; }); @@ -850,8 +857,10 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationViewHo }); convViewHolder.mAvatar.startAnimation(animation); } else { - convViewHolder.mAvatar.setImageBitmap(null); - convViewHolder.mAvatar.setVisibility(View.INVISIBLE); + if (convViewHolder.mAvatar != null) { + convViewHolder.mAvatar.setImageBitmap(null); + convViewHolder.mAvatar.setVisibility(View.INVISIBLE); + } } } } else { @@ -938,16 +947,11 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationViewHo */ private void configureForCallInfo(@NonNull final ConversationViewHolder convViewHolder, @NonNull final Interaction interaction) { - int pictureResID; - String historyTxt; convViewHolder.mIcon.setScaleY(1); Context context = convViewHolder.itemView.getContext(); - View longPressView = convViewHolder.mCallInfoLayout; longPressView.getBackground().setTintList(null); - - longPressView.setOnCreateContextMenuListener((menu, v, menuInfo) -> { conversationFragment.onCreateContextMenu(menu, v, menuInfo); MenuInflater inflater = conversationFragment.getActivity().getMenuInflater(); @@ -958,7 +962,6 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationViewHo menu.removeItem(R.id.conv_action_copy_text); }); - longPressView.setOnLongClickListener((View v) -> { longPressView.getBackground().setTint(conversationFragment.getResources().getColor(R.color.grey_500)); conversationFragment.updatePosition(convViewHolder.getAdapterPosition()); @@ -966,9 +969,9 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationViewHo return false; }); - + int pictureResID; + String historyTxt; SipCall call = (SipCall) interaction; - if (call.isMissed()) { if (call.isIncoming()) { pictureResID = R.drawable.baseline_call_missed_24; @@ -989,7 +992,6 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationViewHo context.getString(R.string.notif_outgoing_call); } - convViewHolder.mCid = call.getConversation().getParticipant(); convViewHolder.mIcon.setImageResource(pictureResID); convViewHolder.mHistTxt.setText(historyTxt); convViewHolder.mHistDetailTxt.setText(DateFormat.getDateTimeInstance() @@ -1199,11 +1201,12 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationViewHo CALL_INFORMATION(R.layout.item_conv_call), INCOMING_TEXT_MESSAGE(R.layout.item_conv_msg_peer), OUTGOING_TEXT_MESSAGE(R.layout.item_conv_msg_me), - COMPOSING_INDICATION(R.layout.item_conv_composing); + COMPOSING_INDICATION(R.layout.item_conv_composing), + INVALID(-1); - private final int layout; + @LayoutRes private final int layout; - MessageType(int l) { + MessageType(@LayoutRes int l) { layout = l; } diff --git a/ring-android/app/src/main/java/cx/ring/adapters/NumberAdapter.java b/ring-android/app/src/main/java/cx/ring/adapters/NumberAdapter.java index 2245fb92890c2dc5136db73c00b8caef67e4c6b1..5baba2cd3079a6d2cf1be9cb81ab700086e7326a 100644 --- a/ring-android/app/src/main/java/cx/ring/adapters/NumberAdapter.java +++ b/ring-android/app/src/main/java/cx/ring/adapters/NumberAdapter.java @@ -85,7 +85,7 @@ public class NumberAdapter extends BaseAdapter { Phone number = mNumbers.get(position); ImageView numberIcon = convertView.findViewById(R.id.number_icon); - numberIcon.setImageResource(number.getNumber().isRingId() ? + numberIcon.setImageResource(number.getNumber().isHexId() ? R.drawable.ic_jami_24 : R.drawable.baseline_dialer_sip_24); if (longView) { 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 17bc1fc326fc86a370230717a6da4e4f2da6d8c7..9113f74e5d279c0cc2843afa954dd685094a09a5 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 @@ -26,6 +26,7 @@ import cx.ring.smartlist.SmartListViewModel; import cx.ring.viewholders.SmartListViewHolder; import android.os.Parcelable; +import android.util.Log; import android.view.LayoutInflater; import android.view.ViewGroup; @@ -90,6 +91,7 @@ public class SmartListAdapter extends RecyclerView.Adapter<SmartListViewHolder> } public void update(List<SmartListViewModel> viewModels) { + //Log.w("SmartListAdapter", "update " + (viewModels == null ? null : viewModels.size())); final List<SmartListViewModel> old = mSmartListViewModels; mSmartListViewModels = viewModels == null ? new ArrayList<>() : viewModels; if (old != null && viewModels != null) { 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 e30910a7c58c2eb26ea77a108566a23e4f511606..7f0c67db62fa974995ce981f38cd3a447d7ee858 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 @@ -27,8 +27,8 @@ import cx.ring.smartlist.SmartListViewModel; public class SmartListDiffUtil extends DiffUtil.Callback { - private List<SmartListViewModel> mOldList; - private List<SmartListViewModel> mNewList; + private final List<SmartListViewModel> mOldList; + private final List<SmartListViewModel> mNewList; public SmartListDiffUtil(List<SmartListViewModel> oldList, List<SmartListViewModel> newList) { mOldList = oldList; @@ -49,7 +49,17 @@ public class SmartListDiffUtil extends DiffUtil.Callback { public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { SmartListViewModel oldItem = mOldList.get(oldItemPosition); SmartListViewModel newItem = mNewList.get(newItemPosition); - return newItem.getHeaderTitle() == oldItem.getHeaderTitle() && newItem.getContact() == oldItem.getContact(); + if (newItem.getHeaderTitle() != oldItem.getHeaderTitle()) + return false; + if (newItem.getContact() != oldItem.getContact()) { + if (newItem.getContact().size() != oldItem.getContact().size()) + return false; + for (int i = 0; i < newItem.getContact().size(); i++) { + if (newItem.getContact().get(i) != oldItem.getContact().get(i)) + return false; + } + } + return true; } @Override diff --git a/ring-android/app/src/main/java/cx/ring/application/JamiApplication.java b/ring-android/app/src/main/java/cx/ring/application/JamiApplication.java index f45152ee8f39d27e00f8fbc37c16daf2287b37dc..c867059aac1209d9b33ecfb33d2e83333994c558 100644 --- a/ring-android/app/src/main/java/cx/ring/application/JamiApplication.java +++ b/ring-android/app/src/main/java/cx/ring/application/JamiApplication.java @@ -52,6 +52,8 @@ import java.util.concurrent.ScheduledExecutorService; import javax.inject.Inject; import javax.inject.Named; +import androidx.annotation.RequiresApi; + import cx.ring.BuildConfig; import cx.ring.R; import cx.ring.contacts.AvatarFactory; @@ -238,7 +240,7 @@ public abstract class JamiApplication extends Application { super.onCreate(); sInstance = this; - RxJavaPlugins.setErrorHandler(e -> Log.e(TAG, "Unhandled RxJava error", e)); + //RxJavaPlugins.setErrorHandler(e -> Log.e(TAG, "Unhandled RxJava error", e)); // building injection dependency tree mJamiInjectionComponent = DaggerJamiInjectionComponent.builder() diff --git a/ring-android/app/src/main/java/cx/ring/client/CallActivity.java b/ring-android/app/src/main/java/cx/ring/client/CallActivity.java index 40a5341c0c8301737f14adce731b47b1000f685a..4c94edc11f413afb30cbee439a59e778f309f363 100644 --- a/ring-android/app/src/main/java/cx/ring/client/CallActivity.java +++ b/ring-android/app/src/main/java/cx/ring/client/CallActivity.java @@ -42,8 +42,8 @@ import cx.ring.BuildConfig; import cx.ring.R; import cx.ring.application.JamiApplication; import cx.ring.fragments.CallFragment; -import cx.ring.fragments.ConversationFragment; import cx.ring.services.NotificationService; +import cx.ring.utils.ConversationPath; import cx.ring.utils.KeyboardVisibilityManager; import cx.ring.utils.MediaButtonsHelper; @@ -118,22 +118,16 @@ public class CallActivity extends AppCompatActivity { private void handleNewIntent(Intent intent) { String action = intent.getAction(); - if (Intent.ACTION_CALL.equals(action) || ACTION_CALL.equals(action)) { - boolean audioOnly = intent.getBooleanExtra(CallFragment.KEY_AUDIO_ONLY, true); - String accountId = intent.getStringExtra(ConversationFragment.KEY_ACCOUNT_ID); - String contactRingId = intent.getStringExtra(ConversationFragment.KEY_CONTACT_RING_ID); - // Reload a new view + String contactId = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER); CallFragment callFragment = CallFragment.newInstance(CallFragment.ACTION_PLACE_CALL, - accountId, - contactRingId, + ConversationPath.fromIntent(intent), + contactId, audioOnly); getSupportFragmentManager().beginTransaction().replace(R.id.main_call_layout, callFragment, CALL_FRAGMENT_TAG).commit(); - } else if (Intent.ACTION_VIEW.equals(action) || ACTION_CALL_ACCEPT.equals(action)) { String confId = intent.getStringExtra(NotificationService.KEY_CALL_ID); - // Reload a new view CallFragment callFragment = CallFragment.newInstance(Intent.ACTION_VIEW.equals(action) ? CallFragment.ACTION_GET_CALL : ACTION_CALL_ACCEPT, confId); getSupportFragmentManager().beginTransaction().replace(R.id.main_call_layout, callFragment, CALL_FRAGMENT_TAG).commit(); } 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 92339a9222e052fd5d88f8b01480be5b0b674331..ca63edba7caf5decef6c62c867c2b9d0b5dee39a 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 @@ -33,7 +33,9 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.snackbar.Snackbar; +import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; +import androidx.annotation.StringRes; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; @@ -43,15 +45,18 @@ import android.view.ViewGroup; import android.widget.Toast; import java.util.ArrayList; +import java.util.Map; import javax.inject.Inject; import javax.inject.Singleton; import androidx.core.widget.ImageViewCompat; -import androidx.databinding.DataBindingUtil; import androidx.recyclerview.widget.RecyclerView; + import cx.ring.R; import cx.ring.application.JamiApplication; +import cx.ring.daemon.Ringservice; +import cx.ring.daemon.RingserviceJNI; import cx.ring.databinding.ActivityContactDetailsBinding; import cx.ring.databinding.ItemContactActionBinding; import cx.ring.facades.ConversationFacade; @@ -61,6 +66,7 @@ import cx.ring.model.CallContact; import cx.ring.model.Conference; import cx.ring.model.Conversation; import cx.ring.model.SipCall; +import cx.ring.model.Uri; import cx.ring.services.AccountService; import cx.ring.services.NotificationService; import cx.ring.utils.ConversationPath; @@ -82,25 +88,27 @@ public class ContactDetailsActivity extends AppCompatActivity { private SharedPreferences mPreferences; private ActivityContactDetailsBinding binding; private Conversation mConversation; - private CallContact mContact = null; interface IContactAction { void onAction(); } static class ContactAction { + @DrawableRes final int icon; - int iconTint; - CharSequence title; + final CharSequence title; final IContactAction callback; - ContactAction(int i, int tint, CharSequence t, IContactAction cb) { + int iconTint; + + ContactAction(@DrawableRes int i, int tint, CharSequence t, IContactAction cb) { icon = i; iconTint = tint; title = t; callback = cb; } - ContactAction(int i, CharSequence t, IContactAction cb) { + + ContactAction(@DrawableRes int i, CharSequence t, IContactAction cb) { icon = i; iconTint = Color.BLACK; title = t; @@ -110,12 +118,12 @@ public class ContactDetailsActivity extends AppCompatActivity { void setIconTint(int tint) { iconTint = tint; } - void setTitle(CharSequence t) { title = t; } } static class ContactActionView extends RecyclerView.ViewHolder { final ItemContactActionBinding binding; IContactAction callback; + ContactActionView(@NonNull ItemContactActionBinding b) { super(b.getRoot()); binding = b; @@ -124,7 +132,7 @@ public class ContactDetailsActivity extends AppCompatActivity { if (callback != null) callback.onAction(); } catch (Exception e) { - Log.w(TAG, "Error performing action" ,e); + Log.w(TAG, "Error performing action", e); } }); } @@ -163,11 +171,15 @@ public class ContactDetailsActivity extends AppCompatActivity { private ContactAction colorAction; private ContactAction contactAction; private int colorActionPosition; - private int contactIdPosition; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + ConversationPath path = ConversationPath.fromIntent(getIntent()); + if (path == null) { + finish(); + return; + } binding = ActivityContactDetailsBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); JamiApplication.getInstance().getInjectionComponent().inject(this); @@ -175,95 +187,99 @@ public class ContactDetailsActivity extends AppCompatActivity { CollapsingToolbarLayout collapsingToolbarLayout = findViewById(R.id.toolbar_layout); collapsingToolbarLayout.setTitle(""); - Toolbar toolbar = findViewById(R.id.toolbar); - setSupportActionBar(toolbar); + setSupportActionBar(findViewById(R.id.toolbar)); FloatingActionButton fab = findViewById(R.id.fab); - fab.setOnClickListener(view -> goToConversationActivity(mConversation.getAccountId(), mContact.getPrimaryNumber())); + fab.setOnClickListener(view -> goToConversationActivity(mConversation.getAccountId(), mConversation.getUri())); - Intent intent = getIntent(); - ConversationPath path = ConversationPath.fromIntent(intent); - if (path != null) { - mDisposableBag.add(mConversationFacade + colorActionPosition = 1; + + mDisposableBag.add(mConversationFacade .startConversation(path.getAccountId(), path.getConversationUri()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(conversation -> { - // TODO handle group - CallContact contact = conversation.getContact(); + mConversation = conversation; mPreferences = getSharedPreferences(conversation.getAccountId() + "_" + conversation.getUri().getUri(), Context.MODE_PRIVATE); - int color = mPreferences.getInt(ConversationFragment.KEY_PREFERENCE_CONVERSATION_COLOR, getResources().getColor(R.color.color_primary_light)); - colorAction.setIconTint(color); - adapter.notifyItemChanged(colorActionPosition); - contactAction.setTitle(contact.getRingUsername()); - adapter.notifyItemChanged(contactIdPosition); - collapsingToolbarLayout.setBackgroundColor(color); - collapsingToolbarLayout.setTitle(contact.getDisplayName()); - collapsingToolbarLayout.setContentScrimColor(color); - collapsingToolbarLayout.setStatusBarScrimColor(color); - //collapsingToolbarLayout.setCollapsedTitleTextColor(); binding.contactImage.setImageDrawable( new AvatarDrawable.Builder() - .withContact(contact) + .withConversation(conversation) .withPresence(false) .withCircleCrop(false) .build(this) ); - mConversation = conversation; - mContact = contact; - })); - colorAction = new ContactAction(R.drawable.item_color_background, 0, "Choose color", () -> { - ColorChooserBottomSheet frag = new ColorChooserBottomSheet(); - frag.setCallback(color -> { + /*Map<String, String> details = Ringservice.getCertificateDetails(conversation.getContact().getUri().getRawRingId()); + for (Map.Entry<String, String> e : details.entrySet()) { + Log.w(TAG, e.getKey() + " -> " + e.getValue()); + }*/ + + @StringRes int infoString = conversation.isSwarm() + ? (conversation.getMode() == Conversation.Mode.OneToOne + ? 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), () -> {})); + + colorAction = new ContactAction(R.drawable.item_color_background, 0, getText(R.string.conversation_preference_color), () -> { + ColorChooserBottomSheet frag = new ColorChooserBottomSheet(); + frag.setCallback(color -> { + collapsingToolbarLayout.setBackgroundColor(color); + collapsingToolbarLayout.setContentScrimColor(color); + collapsingToolbarLayout.setStatusBarScrimColor(color); + colorAction.setIconTint(color); + adapter.notifyItemChanged(colorActionPosition); + mPreferences.edit().putInt(ConversationFragment.KEY_PREFERENCE_CONVERSATION_COLOR, color).apply(); + }); + frag.show(getSupportFragmentManager(), "colorChooser"); + }); + int color = mPreferences.getInt(ConversationFragment.KEY_PREFERENCE_CONVERSATION_COLOR, getResources().getColor(R.color.color_primary_light)); + colorAction.setIconTint(color); collapsingToolbarLayout.setBackgroundColor(color); + collapsingToolbarLayout.setTitle(conversation.getTitle()); collapsingToolbarLayout.setContentScrimColor(color); collapsingToolbarLayout.setStatusBarScrimColor(color); - colorAction.setIconTint(color); - adapter.notifyItemChanged(colorActionPosition); - mPreferences.edit().putInt(ConversationFragment.KEY_PREFERENCE_CONVERSATION_COLOR, color).apply(); - }); - frag.show(getSupportFragmentManager(), "colorChooser"); - }); - adapter.actions.add(colorAction); - adapter.actions.add(new ContactAction(R.drawable.baseline_call_24, getText(R.string.ab_action_audio_call), () -> - goToCallActivity(mConversation.getAccountId(), mContact.getPrimaryNumber(), true))); - adapter.actions.add(new ContactAction(R.drawable.baseline_videocam_24, getText(R.string.ab_action_video_call), () -> - goToCallActivity(mConversation.getAccountId(), mContact.getPrimaryNumber(), false))); - adapter.actions.add(new ContactAction(R.drawable.baseline_clear_all_24, getText(R.string.conversation_action_history_clear), () -> - new MaterialAlertDialogBuilder(ContactDetailsActivity.this) - .setTitle(R.string.clear_history_dialog_title) - .setMessage(R.string.clear_history_dialog_message) - .setPositiveButton(R.string.conversation_action_history_clear, (b, i) -> { - mConversationFacade.clearHistory(mConversation.getAccountId(), mContact.getPrimaryUri()).subscribe(); - Snackbar.make(binding.getRoot(), R.string.clear_history_completed, Snackbar.LENGTH_LONG).show(); - }) - .setNegativeButton(android.R.string.cancel, null) - .create() - .show())); - adapter.actions.add(new ContactAction(R.drawable.baseline_block_24, getText(R.string.conversation_action_block_this), () -> - new MaterialAlertDialogBuilder(ContactDetailsActivity.this) - .setTitle(getString(R.string.block_contact_dialog_title, contactAction.title)) - .setMessage(getString(R.string.block_contact_dialog_message, contactAction.title)) - .setPositiveButton(R.string.conversation_action_block_this, (b, i) -> { - mAccountService.removeContact(mConversation.getAccountId(), mContact.getPrimaryUri().getRawRingId(), true); - Toast.makeText(getApplicationContext(), getString(R.string.block_contact_completed, contactAction.title), Toast.LENGTH_LONG).show(); - finish(); - }) - .setNegativeButton(android.R.string.cancel, null) - .create() - .show())); - contactAction = new ContactAction(R.drawable.baseline_person_24, "", () -> { - ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); - if (clipboard != null) { - clipboard.setPrimaryClip(ClipData.newPlainText(getText(R.string.clip_contact_uri), path.getConversationId())); - Snackbar.make(binding.getRoot(), getString(R.string.conversation_action_copied_peer_number_clipboard, path.getConversationId()), Snackbar.LENGTH_LONG).show(); - } - }); - adapter.actions.add(contactAction); - colorActionPosition = 0; - contactIdPosition = adapter.actions.size() - 1; - binding.contactActionList.setAdapter(adapter); - } + adapter.actions.add(colorAction); + + if (mConversation.getContacts().size() <= 2) { + CallContact contact = mConversation.getContact(); + adapter.actions.add(new ContactAction(R.drawable.baseline_call_24, getText(R.string.ab_action_audio_call), () -> + goToCallActivity(mConversation.getAccountId(), contact.getPrimaryNumber(), true))); + adapter.actions.add(new ContactAction(R.drawable.baseline_videocam_24, getText(R.string.ab_action_video_call), () -> + goToCallActivity(mConversation.getAccountId(), contact.getPrimaryNumber(), false))); + adapter.actions.add(new ContactAction(R.drawable.baseline_clear_all_24, getText(R.string.conversation_action_history_clear), () -> + new MaterialAlertDialogBuilder(ContactDetailsActivity.this) + .setTitle(R.string.clear_history_dialog_title) + .setMessage(R.string.clear_history_dialog_message) + .setPositiveButton(R.string.conversation_action_history_clear, (b, i) -> { + mConversationFacade.clearHistory(mConversation.getAccountId(), contact.getUri()).subscribe(); + Snackbar.make(binding.getRoot(), R.string.clear_history_completed, Snackbar.LENGTH_LONG).show(); + }) + .setNegativeButton(android.R.string.cancel, null) + .create() + .show())); + adapter.actions.add(new ContactAction(R.drawable.baseline_block_24, getText(R.string.conversation_action_block_this), () -> + new MaterialAlertDialogBuilder(ContactDetailsActivity.this) + .setTitle(getString(R.string.block_contact_dialog_title, contactAction.title)) + .setMessage(getString(R.string.block_contact_dialog_message, contactAction.title)) + .setPositiveButton(R.string.conversation_action_block_this, (b, i) -> { + mAccountService.removeContact(mConversation.getAccountId(), contact.getUri().getRawRingId(), true); + Toast.makeText(getApplicationContext(), getString(R.string.block_contact_completed, contactAction.title), Toast.LENGTH_LONG).show(); + finish(); + }) + .setNegativeButton(android.R.string.cancel, null) + .create() + .show())); + } + contactAction = new ContactAction(R.drawable.baseline_person_24, conversation.getUriTitle(), () -> { + ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); + if (clipboard != null) { + clipboard.setPrimaryClip(ClipData.newPlainText(getText(R.string.clip_contact_uri), path.getConversationId())); + Snackbar.make(binding.getRoot(), getString(R.string.conversation_action_copied_peer_number_clipboard, path.getConversationId()), Snackbar.LENGTH_LONG).show(); + } + }); + adapter.actions.add(contactAction); + binding.contactActionList.setAdapter(adapter); + })); } @Override @@ -279,7 +295,6 @@ public class ContactDetailsActivity extends AppCompatActivity { private void goToCallActivity(String accountId, String contactRingId, boolean audioOnly) { Conference conf = mConversation.getCurrentCall(); - if (conf != null && !conf.getParticipants().isEmpty() && conf.getParticipants().get(0).getCallStatus() != SipCall.CallStatus.INACTIVE @@ -297,7 +312,7 @@ public class ContactDetailsActivity extends AppCompatActivity { } } - private void goToConversationActivity(String accountId, String contactRingId) { - startActivity(new Intent(Intent.ACTION_VIEW, ConversationPath.toUri(accountId, contactRingId), getApplicationContext(), ConversationActivity.class)); + private void goToConversationActivity(String accountId, Uri conversationUri) { + startActivity(new Intent(Intent.ACTION_VIEW, ConversationPath.toUri(accountId, conversationUri), getApplicationContext(), ConversationActivity.class)); } } 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 2f82a3acfd41797fa6de0a43b6433c812bd81f3d..8f3a1948fbcec5de80b5b9c42f0c7a48ca2686b2 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 @@ -77,22 +77,6 @@ public class ConversationActivity extends AppCompatActivity implements Colorable if (ab != null) ab.setDisplayHomeAsUpEnabled(true); - //getWindow().setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); - View decorView = getWindow().getDecorView(); - decorView.setSystemUiVisibility( - decorView.getSystemUiVisibility() - | View.SYSTEM_UI_FLAG_LAYOUT_STABLE - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); - - ViewCompat.setOnApplyWindowInsetsListener(binding.toolbarLayout, (v, insets) -> { - CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) binding.toolbarLayout.getLayoutParams(); - params.topMargin = insets.getSystemWindowInsetTop(); - binding.toolbarLayout.setLayoutParams(params); - insets.consumeSystemWindowInsets(); - return insets; - }); - if (mConversationFragment == null) { Bundle bundle = conversationPath.toBundle(); bundle.putBoolean(NotificationServiceImpl.EXTRA_BUBBLE, mIsBubble); 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 26305f190ce13c9e1a1d778bc0f90e97fc78e801..36bbcac66f2c42a9861d10bf9f6c2e8479431b1b 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 @@ -39,6 +39,8 @@ import cx.ring.adapters.SmartListAdapter; import cx.ring.application.JamiApplication; import cx.ring.facades.ConversationFacade; import cx.ring.fragments.CallFragment; +import cx.ring.model.Account; +import cx.ring.model.CallContact; import cx.ring.model.Conference; import cx.ring.model.SipCall; import cx.ring.services.CallService; @@ -80,7 +82,7 @@ public class ConversationSelectionActivity extends AppCompatActivity { @Override public void onItemClick(SmartListViewModel smartListViewModel) { Intent intent = new Intent(); - intent.setData(ConversationPath.toUri(smartListViewModel.getAccountId(), smartListViewModel.getContact().getPrimaryNumber())); + intent.setData(ConversationPath.toUri(smartListViewModel.getAccountId(), smartListViewModel.getUri())); setResult(Activity.RESULT_OK, intent); finish(); } @@ -115,8 +117,11 @@ public class ConversationSelectionActivity extends AppCompatActivity { return vm; List<SmartListViewModel> filteredVms = new ArrayList<>(vm.size()); models: for (SmartListViewModel v : vm) { + List<CallContact> contacts = v.getContact(); + if (contacts.size() != 1) + continue; for (SipCall call : conf.getParticipants()) { - if (call.getContact() == v.getContact()) { + if (call.getContact() == v.getContact().get(0)) { continue models; } } diff --git a/ring-android/app/src/main/java/cx/ring/client/HomeActivity.java b/ring-android/app/src/main/java/cx/ring/client/HomeActivity.java index 78bcd21b6a322b24b6834def1e57c6f1d519b8ca..03ea2b0c2986026dd18897ffae203185e02fd1e4 100644 --- a/ring-android/app/src/main/java/cx/ring/client/HomeActivity.java +++ b/ring-android/app/src/main/java/cx/ring/client/HomeActivity.java @@ -389,13 +389,14 @@ public class HomeActivity extends AppCompatActivity implements BottomNavigationV mDisposable.add(mAccountService.getCurrentAccountSubject() .firstElement() .observeOn(AndroidSchedulers.mainThread()) - .subscribe(account -> startConversation(account.getAccountID(), new cx.ring.model.Uri(conversationId)))); + .subscribe(account -> startConversation(account.getAccountID(), cx.ring.model.Uri.fromString(conversationId)))); } public void startConversation(String accountId, cx.ring.model.Uri conversationId) { + Log.w(TAG, "startConversation " + accountId + " " + conversationId); if (!DeviceUtils.isTablet(this)) { - startActivity(new Intent(Intent.ACTION_VIEW, ConversationPath.toUri(accountId, conversationId.toString()), this, ConversationActivity.class)); + startActivity(new Intent(Intent.ACTION_VIEW, ConversationPath.toUri(accountId, conversationId), this, ConversationActivity.class)); } else { - startConversationTablet(ConversationPath.toBundle(accountId, conversationId.toString())); + startConversationTablet(ConversationPath.toBundle(accountId, conversationId)); } } @@ -510,13 +511,11 @@ public class HomeActivity extends AppCompatActivity implements BottomNavigationV switch (item.getItemId()) { case R.id.navigation_requests: if (fContent instanceof ContactRequestsFragment) { - ((ContactRequestsFragment) fContent).presentForAccount(account.getAccountID()); + ((ContactRequestsFragment) fContent).presentForAccount(null); break; } popCustomBackStack(); fContent = new ContactRequestsFragment(); - bundle.putString(ContactRequestsFragment.ACCOUNT_ID, account.getAccountID()); - fContent.setArguments(bundle); getSupportFragmentManager().beginTransaction() .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) .replace(R.id.main_frame, fContent, CONTACT_REQUESTS_TAG) diff --git a/ring-android/app/src/main/java/cx/ring/contactrequests/ContactRequestsFragment.java b/ring-android/app/src/main/java/cx/ring/contactrequests/ContactRequestsFragment.java index 09ea6b507baf76e8d1892532ca7e1e2c63ec75a8..96d7415cae6a45f420708541b59fb79074f0cf06 100644 --- a/ring-android/app/src/main/java/cx/ring/contactrequests/ContactRequestsFragment.java +++ b/ring-android/app/src/main/java/cx/ring/contactrequests/ContactRequestsFragment.java @@ -41,6 +41,7 @@ import cx.ring.application.JamiApplication; import cx.ring.client.ConversationActivity; import cx.ring.client.HomeActivity; import cx.ring.databinding.FragPendingContactRequestsBinding; +import cx.ring.model.Uri; import cx.ring.mvp.BaseSupportFragment; import cx.ring.smartlist.SmartListViewModel; import cx.ring.utils.ConversationPath; @@ -74,7 +75,7 @@ public class ContactRequestsFragment extends BaseSupportFragment<ContactRequests binding = null; } - public void presentForAccount(@NonNull String accountId) { + public void presentForAccount(@Nullable String accountId) { Bundle arguments = getArguments(); if (arguments != null) arguments.putString(ACCOUNT_ID, accountId); @@ -86,9 +87,8 @@ public class ContactRequestsFragment extends BaseSupportFragment<ContactRequests public void onStart() { super.onStart(); Bundle arguments = getArguments(); - if (arguments != null && arguments.containsKey(ACCOUNT_ID)) { - presenter.updateAccount(getArguments().getString(ACCOUNT_ID)); - } + String accountId = arguments != null ? arguments.getString(ACCOUNT_ID) : null; + presenter.updateAccount(accountId); } @Override @@ -145,21 +145,13 @@ public class ContactRequestsFragment extends BaseSupportFragment<ContactRequests } @Override - public void goToConversation(String accountId, String contactId) { - Context context = requireContext(); - if (DeviceUtils.isTablet(context)) { - Activity activity = getActivity(); - if (activity instanceof HomeActivity) { - ((HomeActivity) activity).startConversationTablet(ConversationPath.toBundle(accountId, contactId)); - } - } else { - startActivity(new Intent(Intent.ACTION_VIEW, ConversationPath.toUri(accountId, contactId), context, ConversationActivity.class)); - } + public void goToConversation(String accountId, Uri contactId) { + ((HomeActivity) requireActivity()).startConversation(accountId, contactId); } @Override public void onItemClick(SmartListViewModel viewModel) { - presenter.contactRequestClicked(viewModel.getAccountId(), viewModel.getContact()); + presenter.contactRequestClicked(viewModel.getAccountId(), viewModel.getUri()); } @Override diff --git a/ring-android/app/src/main/java/cx/ring/contacts/AvatarFactory.java b/ring-android/app/src/main/java/cx/ring/contacts/AvatarFactory.java index 2288b08bbda2750b91f7ff13f6ffbfc1858e9cd7..f68a032c47496f5cff219ab4d7ff698e7eb28f39 100644 --- a/ring-android/app/src/main/java/cx/ring/contacts/AvatarFactory.java +++ b/ring-android/app/src/main/java/cx/ring/contacts/AvatarFactory.java @@ -36,6 +36,8 @@ import java.util.List; import cx.ring.model.Account; import cx.ring.model.CallContact; +import cx.ring.model.Conversation; +import cx.ring.smartlist.SmartListViewModel; import cx.ring.utils.BitmapUtils; import cx.ring.views.AvatarDrawable; import io.reactivex.Single; @@ -60,23 +62,26 @@ public class AvatarFactory { .withPresence(presence) .build(context)); } - public static Single<Drawable> getAvatar(Context context, List<CallContact> contacts, boolean presence) { + public static Single<Drawable> getAvatar(Context context, Conversation conversation, boolean presence) { return Single.fromCallable(() -> new AvatarDrawable.Builder() - .withContacts(contacts) + .withConversation(conversation) .withCircleCrop(true) .withPresence(presence) .build(context)); } + public static Single<Drawable> getAvatar(Context context, SmartListViewModel vm) { + return Single.fromCallable(() -> + new AvatarDrawable.Builder() + .withViewModel(vm) + .withCircleCrop(true) + .build(context)); + } public static Single<Drawable> getAvatar(Context context, CallContact contact) { return getAvatar(context, contact, true); } - public static Single<Drawable> getAvatar(Context context, List<CallContact> contacts) { - return getAvatar(context, contacts, true); - } - - public static Single<Bitmap> getBitmapAvatar(Context context, List<CallContact> contacts, int size, boolean presence) { - return getAvatar(context, contacts, presence) + public static Single<Bitmap> getBitmapAvatar(Context context, Conversation conversation, int size, boolean presence) { + return getAvatar(context, conversation, presence) .map(d -> BitmapUtils.drawableToBitmap(d, size)); } public static Single<Bitmap> getBitmapAvatar(Context context, CallContact contact, int size, boolean presence) { diff --git a/ring-android/app/src/main/java/cx/ring/dependencyinjection/JamiInjectionComponent.java b/ring-android/app/src/main/java/cx/ring/dependencyinjection/JamiInjectionComponent.java index 32e31932b7644683cb026b8bc03f290148e13085..05d74bcf4202a69af5a3c6a237555fa886839c3b 100755 --- a/ring-android/app/src/main/java/cx/ring/dependencyinjection/JamiInjectionComponent.java +++ b/ring-android/app/src/main/java/cx/ring/dependencyinjection/JamiInjectionComponent.java @@ -28,6 +28,7 @@ import cx.ring.account.JamiLinkAccountPasswordFragment; import cx.ring.contactrequests.BlockListFragment; import cx.ring.fragments.GeneralAccountFragment; import cx.ring.fragments.LinkDeviceFragment; +import cx.ring.fragments.ContactPickerFragment; import cx.ring.fragments.LocationSharingFragment; import cx.ring.account.AccountWizardActivity; import cx.ring.account.HomeAccountCreationFragment; @@ -234,4 +235,6 @@ public interface JamiInjectionComponent { void inject(SyncService syncService); void inject(LinkDeviceFragment linkDeviceFragment); + + void inject(ContactPickerFragment contactPickerFragment); } diff --git a/ring-android/app/src/main/java/cx/ring/dependencyinjection/JamiInjectionModule.java b/ring-android/app/src/main/java/cx/ring/dependencyinjection/JamiInjectionModule.java index 799304c123869b069004194695c0e77ded79d177..15dbe6703e4828e7db53c8bd7e80075e3add631a 100755 --- a/ring-android/app/src/main/java/cx/ring/dependencyinjection/JamiInjectionModule.java +++ b/ring-android/app/src/main/java/cx/ring/dependencyinjection/JamiInjectionModule.java @@ -37,7 +37,7 @@ public class JamiInjectionModule { } @Provides - JamiApplication provideRingApplication() { + JamiApplication provideJamiApplication() { return mJamiApplication; } 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 15b0b8b5a1e03d24f0d53a292c976f38252ba599..c6babb4f9c3c89f34386843e0d9e7f968fb8845f 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 @@ -190,11 +190,12 @@ public class CallFragment extends BaseSupportFragment<CallPresenter> implements private final CompositeDisposable mCompositeDisposable = new CompositeDisposable(); - public static CallFragment newInstance(@NonNull String action, @Nullable String accountID, @Nullable String contactRingId, boolean audioOnly) { + public static CallFragment newInstance(@NonNull String action, @Nullable ConversationPath path, @Nullable String contactId, boolean audioOnly) { Bundle bundle = new Bundle(); bundle.putString(KEY_ACTION, action); - bundle.putString(KEY_ACCOUNT_ID, accountID); - bundle.putString(ConversationFragment.KEY_CONTACT_RING_ID, contactRingId); + if (path != null) + path.toBundle(bundle); + bundle.putString(Intent.EXTRA_PHONE_NUMBER, contactId); bundle.putBoolean(KEY_AUDIO_ONLY, audioOnly); CallFragment countDownFragment = new CallFragment(); countDownFragment.setArguments(bundle); @@ -240,7 +241,6 @@ public class CallFragment extends BaseSupportFragment<CallPresenter> implements @Override protected void initPresenter(CallPresenter presenter) { - super.initPresenter(presenter); Bundle args = getArguments(); if (args != null) { String action = args.getString(KEY_ACTION); @@ -808,7 +808,7 @@ public class CallFragment extends BaseSupportFragment<CallPresenter> implements if (resultCode == Activity.RESULT_OK && data != null) { ConversationPath path = ConversationPath.fromUri(data.getData()); if (path != null) { - presenter.addConferenceParticipant(path.getAccountId(), path.getConversationId()); + presenter.addConferenceParticipant(path.getAccountId(), path.getConversationUri()); } } } else if (requestCode == REQUEST_CODE_SCREEN_SHARE) { @@ -1315,9 +1315,11 @@ public class CallFragment extends BaseSupportFragment<CallPresenter> implements Bundle args; args = getArguments(); if (args != null) { - presenter.initOutGoing(getArguments().getString(KEY_ACCOUNT_ID), - getArguments().getString(ConversationFragment.KEY_CONTACT_RING_ID), - getArguments().getBoolean(KEY_AUDIO_ONLY)); + ConversationPath conversation = ConversationPath.fromBundle(args); + presenter.initOutGoing(conversation.getAccountId(), + conversation.getConversationUri(), + args.getString(Intent.EXTRA_PHONE_NUMBER), + args.getBoolean(KEY_AUDIO_ONLY)); } } } 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 new file mode 100644 index 0000000000000000000000000000000000000000..97195ac65a7727cbe3e5bfb42c09585090dfc5e8 --- /dev/null +++ b/ring-android/app/src/main/java/cx/ring/fragments/ContactPickerFragment.java @@ -0,0 +1,174 @@ +package cx.ring.fragments; + +import android.app.Dialog; +import android.os.Bundle; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.google.android.material.bottomsheet.BottomSheetBehavior; +import com.google.android.material.bottomsheet.BottomSheetDialog; +import com.google.android.material.bottomsheet.BottomSheetDialogFragment; +import com.google.android.material.chip.Chip; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.inject.Inject; + +import cx.ring.R; +import cx.ring.adapters.SmartListAdapter; +import cx.ring.application.JamiApplication; +import cx.ring.client.HomeActivity; +import cx.ring.contacts.AvatarFactory; +import cx.ring.databinding.FragContactPickerBinding; +import cx.ring.facades.ConversationFacade; +import cx.ring.model.CallContact; +import cx.ring.smartlist.SmartListViewModel; +import cx.ring.viewholders.SmartListViewHolder; +import cx.ring.views.AvatarDrawable; +import io.reactivex.Observable; +import io.reactivex.Single; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; + +public class ContactPickerFragment extends BottomSheetDialogFragment { + + public static final String TAG = ContactPickerFragment.class.getSimpleName(); + + private FragContactPickerBinding binding = null; + private SmartListAdapter adapter; + private final CompositeDisposable mDisposableBag = new CompositeDisposable(); + + private String mAccountId = null; + private final Set<CallContact> mCurrentSelection = new HashSet<>(); + + @Inject + ConversationFacade mConversationFacade; + + public ContactPickerFragment() { + // Required empty public constructor + } + + public static ContactPickerFragment newInstance() { + return new ContactPickerFragment(); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setRetainInstance(true); + ((JamiApplication) getActivity().getApplication()).getInjectionComponent().inject(this); + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + Dialog bdialog = super.onCreateDialog(savedInstanceState); + if (bdialog instanceof BottomSheetDialog) { + BottomSheetDialog dialog = (BottomSheetDialog) bdialog; + BottomSheetBehavior<FrameLayout> behavior = dialog.getBehavior(); + behavior.setFitToContents(false); + behavior.setSkipCollapsed(true); + behavior.setState(BottomSheetBehavior.STATE_HALF_EXPANDED); + } + return bdialog; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + mDisposableBag.add(mConversationFacade.getContactList() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(conversations -> { + if (binding == null) + return; + adapter.update(conversations); + })); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + binding = FragContactPickerBinding.inflate(getLayoutInflater(), container, false); + adapter = new SmartListAdapter(null, new SmartListViewHolder.SmartListListeners() { + @Override + public void onItemClick(SmartListViewModel item) { + mAccountId = item.getAccountId(); + + boolean checked = !item.isChecked(); + item.setChecked(checked); + adapter.update(item); + + Runnable remover = () -> { + mCurrentSelection.remove(item.getContact().get(0)); + if (mCurrentSelection.isEmpty()) + binding.createGroupBtn.setEnabled(false); + item.setChecked(false); + adapter.update(item); + View v = binding.selectedContacts.findViewWithTag(item); + if (v != null) + binding.selectedContacts.removeView(v); + }; + + if (checked) { + if (mCurrentSelection.add(item.getContact().get(0))) { + Chip chip = new Chip(requireContext(), null, R.style.Widget_MaterialComponents_Chip_Entry); + chip.setText(item.getContactName()); + chip.setChipIcon(new AvatarDrawable.Builder() + .withViewModel(item) + .withCircleCrop(true) + .withCheck(false) + .build(binding.getRoot().getContext())); + chip.setCloseIconVisible(true); + chip.setTag(item); + chip.setOnCloseIconClickListener(v -> remover.run()); + binding.selectedContacts.addView(chip); + } + binding.createGroupBtn.setEnabled(true); + } else { + remover.run(); + } + } + + @Override + public void onItemLongClick(SmartListViewModel item) { + + } + }); + binding.createGroupBtn.setOnClickListener(v -> mDisposableBag.add(mConversationFacade.createConversation(mAccountId, mCurrentSelection) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(conversation -> { + ((HomeActivity) requireActivity()).startConversation(mAccountId, conversation.getUri()); + Dialog dialog = getDialog(); + if (dialog != null) + dialog.cancel(); + }))); + binding.contactList.setAdapter(adapter); + return binding.getRoot(); + } + + @Override + public void onDestroyView() { + mDisposableBag.clear(); + binding = null; + adapter = null; + super.onDestroyView(); + } + + @Override + public int getTheme() { + return R.style.BottomSheetDialogTheme; + } +} \ No newline at end of file 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 967e90ea47e708e419679d2e8c68e45914c55d76..3280919e8378f025a7f9e14f9235b769df726216 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 @@ -66,10 +66,13 @@ import androidx.appcompat.view.menu.MenuBuilder; import androidx.appcompat.view.menu.MenuPopupHelper; import androidx.appcompat.widget.PopupMenu; import androidx.appcompat.widget.Toolbar; +import androidx.core.util.Pair; import androidx.core.view.ViewCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.recyclerview.widget.DefaultItemAnimator; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import java.io.File; import java.io.IOException; @@ -80,7 +83,6 @@ import java.util.Map; import cx.ring.BuildConfig; import cx.ring.R; import cx.ring.adapters.ConversationAdapter; -import cx.ring.adapters.NumberAdapter; import cx.ring.application.JamiApplication; import cx.ring.client.CallActivity; import cx.ring.client.ContactDetailsActivity; @@ -101,6 +103,7 @@ import cx.ring.model.Phone; import cx.ring.model.Error; import cx.ring.model.Uri; import cx.ring.mvp.BaseSupportFragment; +import cx.ring.service.DRingService; import cx.ring.services.LocationSharingService; import cx.ring.services.NotificationService; import cx.ring.services.NotificationServiceImpl; @@ -112,9 +115,11 @@ import cx.ring.utils.ConversationPath; import cx.ring.utils.MediaButtonsHelper; import cx.ring.views.AvatarDrawable; import io.reactivex.Completable; +import io.reactivex.Observable; import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.schedulers.Schedulers; import static android.app.Activity.RESULT_OK; @@ -129,6 +134,7 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen public static final String KEY_ACCOUNT_ID = BuildConfig.APPLICATION_ID + ".ACCOUNT_ID"; public static final String KEY_PREFERENCE_PENDING_MESSAGE = "pendingMessage"; public static final String KEY_PREFERENCE_CONVERSATION_COLOR = "color"; + public static final String KEY_PREFERENCE_CONVERSATION_LAST_READ = "lastRead"; public static final String EXTRA_SHOW_MAP = "showMap"; private static final int REQUEST_CODE_FILE_PICKER = 1000; @@ -163,6 +169,9 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen private final Map<String, AvatarDrawable> mParticipantAvatars = new HashMap<>(); private final Map<String, AvatarDrawable> mSmallParticipantAvatars = new HashMap<>(); private int mapWidth, mapHeight; + private String mLastRead; + + private boolean loading = true; public AvatarDrawable getConversationAvatar(String uri) { return mParticipantAvatars.get(uri); @@ -190,6 +199,7 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen binding.pbLoading.setVisibility(View.GONE); if (mAdapter != null) { mAdapter.updateDataset(conversation); + loading = false; } requireActivity().invalidateOptionsMenu(); } @@ -329,6 +339,25 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen } }); + binding.histList.addOnScrollListener(new RecyclerView.OnScrollListener() { + // The minimum amount of items to have below current scroll position + // before loading more. + static private final int visibleThreshold = 3; + + @Override + public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { + } + + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager(); + if (!loading && layoutManager.findFirstVisibleItemPosition() < visibleThreshold) { + loading = true; + presenter.loadMore(); + } + } + }); + DefaultItemAnimator animator = (DefaultItemAnimator) binding.histList.getItemAnimator(); if (animator != null) animator.setSupportsChangeAnimations(false); @@ -697,8 +726,11 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen @Override public void addElement(Interaction element) { - mAdapter.add(element); - scrollToEnd(); + if (mLastRead != null && mLastRead.equals(element.getMessageId())) + element.read(); + if (mAdapter.add(element)) + scrollToEnd(); + loading = false; } @Override @@ -723,6 +755,24 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen mAdapter.setLastDisplayed(interaction); } + @Override + public void acceptFile(String accountId, Uri conversationUri, DataTransfer transfer) { + File cacheDir = requireContext().getCacheDir(); + long spaceLeft = AndroidFileUtils.getSpaceLeft(cacheDir.toString()); + if (spaceLeft == -1L || transfer.getTotalSize() > spaceLeft) { + presenter.noSpaceLeft(); + return; + } + requireActivity().startService(new Intent(DRingService.ACTION_FILE_ACCEPT, ConversationPath.toUri(accountId, conversationUri), requireContext(), DRingService.class) + .putExtra(DRingService.KEY_TRANSFER_ID, transfer.getDaemonId())); + } + + @Override + public void refuseFile(String accountId, Uri conversationUri, DataTransfer transfer) { + requireActivity().startService(new Intent(DRingService.ACTION_FILE_CANCEL, ConversationPath.toUri(accountId, conversationUri), requireContext(), DRingService.class) + .putExtra(DRingService.KEY_TRANSFER_ID, transfer.getDaemonId())); + } + @Override public void shareFile(File path) { Context c = getContext(); @@ -841,13 +891,15 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen if (path == null) return; - Uri contactUri = path.getConversationUri(); + Uri uri = path.getConversationUri(); mAdapter = new ConversationAdapter(this, presenter); - presenter.init(contactUri, path.getAccountId()); + presenter.init(uri, path.getAccountId()); try { - mPreferences = requireActivity().getSharedPreferences(path.getAccountId() + "_" + contactUri.getRawRingId(), Context.MODE_PRIVATE); + mPreferences = requireActivity().getSharedPreferences(path.getAccountId() + "_" + uri.getUri(), Context.MODE_PRIVATE); mPreferences.registerOnSharedPreferenceChangeListener(this); presenter.setConversationColor(mPreferences.getInt(KEY_PREFERENCE_CONVERSATION_COLOR, getResources().getColor(R.color.color_primary_light))); + mLastRead = mPreferences.getString(KEY_PREFERENCE_CONVERSATION_LAST_READ, null); + Log.w(TAG, "Loaded last read " + mLastRead); } catch (Exception e) { Log.e(TAG, "Can't load conversation preferences"); } @@ -861,7 +913,7 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen LocationSharingService locationService = binder.getService(); ConversationPath path = new ConversationPath(presenter.getPath()); if (locationService.isSharing(path)) { - showMap(path.getAccountId(), contactUri.getUri(), false); + showMap(path.getAccountId(), uri.getUri(), false); } try { requireContext().unbindService(locationServiceConnection); @@ -892,35 +944,33 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen } @Override - public void displayContact(final CallContact contact) { - mCompositeDisposable.clear(); - mCompositeDisposable.add(AvatarFactory.getAvatar(requireContext(), contact) - .doOnSuccess(d -> { - mConversationAvatar = (AvatarDrawable) d; - mParticipantAvatars.put(contact.getPrimaryNumber(), - new AvatarDrawable((AvatarDrawable) d)); - }) - .flatMapObservable(d -> contact.getUpdatesSubject()) + public void updateContact(CallContact contact) { + String contactKey = contact.getPrimaryNumber(); + AvatarDrawable a = mSmallParticipantAvatars.get(contactKey); + if (a != null) { + a.update(contact); + mParticipantAvatars.get(contactKey).update(contact); + mAdapter.setPhoto(); + } else { + mCompositeDisposable.add(AvatarFactory.getAvatar(requireContext(), contact, true) + .subscribeOn(Schedulers.computation()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(avatar -> { + mParticipantAvatars.put(contactKey, (AvatarDrawable) avatar); + mSmallParticipantAvatars.put(contactKey, new AvatarDrawable((AvatarDrawable) avatar)); + mAdapter.setPhoto(); + })); + } + } + + @Override + public void displayContact(Conversation conversation) { + mCompositeDisposable.add(AvatarFactory.getAvatar(requireContext(), conversation, true) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(c -> { - mConversationAvatar.update(c); - String uri = contact.getPrimaryNumber(); - AvatarDrawable ad = mParticipantAvatars.get(uri); - if (ad != null) - ad.update(c); - setupActionbar(contact); - mAdapter.setPhoto(); - })); - mCompositeDisposable.add(AvatarFactory.getAvatar(requireContext(), contact, false) - .doOnSuccess(d -> mSmallParticipantAvatars.put(contact.getPrimaryNumber(), new AvatarDrawable((AvatarDrawable) d))) - .flatMapObservable(d -> contact.getUpdatesSubject()) - .subscribe(c -> { - synchronized (mSmallParticipantAvatars) { - String uri = contact.getPrimaryNumber(); - AvatarDrawable ad = mSmallParticipantAvatars.get(uri); - if (ad != null) - ad.update(c); - } + .subscribe(d -> { + mConversationAvatar = (AvatarDrawable) d; + mParticipantAvatars.put(conversation.getUri().getRawRingId(), new AvatarDrawable((AvatarDrawable) d)); + setupActionbar(conversation); })); } @@ -932,8 +982,7 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen @Override public void displayNumberSpinner(final Conversation conversation, final Uri number) { binding.numberSelector.setVisibility(View.VISIBLE); - binding.numberSelector.setAdapter(new NumberAdapter(getActivity(), - conversation.getContact(), false)); + //binding.numberSelector.setAdapter(new NumberAdapter(getActivity(), conversation.getContact(), false)); binding.numberSelector.setSelection(getIndex(binding.numberSelector, number)); } @@ -967,22 +1016,22 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen } @Override - public void goToContactActivity(String accountId, String contactId) { - startActivity(new Intent(Intent.ACTION_VIEW, ConversationPath.toUri(accountId, contactId), - requireActivity().getApplicationContext(), ContactDetailsActivity.class)); + public void goToContactActivity(String accountId, Uri uri) { + startActivity(new Intent(Intent.ACTION_VIEW, ConversationPath.toUri(accountId, uri)) + .setClass(requireActivity().getApplicationContext(), ContactDetailsActivity.class)); } @Override - public void goToCallActivityWithResult(String accountId, String contactRingId, boolean audioOnly) { - Intent intent = new Intent(CallActivity.ACTION_CALL) - .setClass(requireActivity().getApplicationContext(), CallActivity.class) - .putExtra(KEY_ACCOUNT_ID, accountId) - .putExtra(CallFragment.KEY_AUDIO_ONLY, audioOnly) - .putExtra(KEY_CONTACT_RING_ID, contactRingId); + public void goToCallActivityWithResult(String accountId, Uri conversationUri, Uri contactUri, boolean audioOnly) { + Intent intent = new Intent(Intent.ACTION_CALL) + .setClass(requireContext(), CallActivity.class) + .putExtras(ConversationPath.toBundle(accountId, conversationUri)) + .putExtra(Intent.EXTRA_PHONE_NUMBER, contactUri.getUri()) + .putExtra(CallFragment.KEY_AUDIO_ONLY, audioOnly); startActivityForResult(intent, HomeActivity.REQUEST_CODE_CALL); } - private void setupActionbar(CallContact contact) { + private void setupActionbar(Conversation conversation) { if (!isVisible()) { return; } @@ -993,8 +1042,9 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen } Context context = actionBar.getThemedContext(); - String displayName = contact.getDisplayName(); - String identity = contact.getRingUsername(); + + String displayName = conversation.getTitle(); + String identity = conversation.getUriTitle(); Activity activity = getActivity(); if (activity instanceof HomeActivity) { @@ -1003,12 +1053,12 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen TextView subtitle = toolbar.findViewById(R.id.contact_subtitle); ImageView logo = toolbar.findViewById(R.id.contact_image); - if (!((HomeActivity) activity).isConversationSelected()) { + /*if (!((HomeActivity) activity).isConversationSelected()) { title.setText(""); subtitle.setText(""); logo.setImageDrawable(null); return; - } + }*/ logo.setVisibility(View.VISIBLE); title.setText(displayName); @@ -1217,6 +1267,14 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen } } + @Override + public void updateLastRead(String last) { + Log.w(TAG, "Updated last read " + mLastRead); + mLastRead = last; + if (mPreferences != null) + mPreferences.edit().putString(KEY_PREFERENCE_CONVERSATION_LAST_READ, last).apply(); + } + @Override public void hideErrorPanel() { if (binding != null) { diff --git a/ring-android/app/src/main/java/cx/ring/fragments/LocationSharingFragment.java b/ring-android/app/src/main/java/cx/ring/fragments/LocationSharingFragment.java index 496d745f16a1912f3af7f11222570111a13769ce..0dbd3162c6cc57ef545c64ea9b143f310956ac7a 100644 --- a/ring-android/app/src/main/java/cx/ring/fragments/LocationSharingFragment.java +++ b/ring-android/app/src/main/java/cx/ring/fragments/LocationSharingFragment.java @@ -40,7 +40,6 @@ import androidx.annotation.RequiresApi; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; -import androidx.preference.PreferenceManager; import android.os.IBinder; import android.text.format.DateUtils; @@ -326,7 +325,7 @@ public class LocationSharingFragment extends Fragment { } })); - final Uri contactUri = new Uri(mPath.getContactId()); + final Uri contactUri = mPath.getConversationUri(); mDisposableBag.add(mConversationFacade .getAccountSubject(mPath.getAccountId()) diff --git a/ring-android/app/src/main/java/cx/ring/fragments/QRCodeFragment.java b/ring-android/app/src/main/java/cx/ring/fragments/QRCodeFragment.java index 4db6ab58144fb487f15c6955c5a01972eafdfe29..cc60aa74c1d82f9d8fb2799e258fff77914d87d7 100644 --- a/ring-android/app/src/main/java/cx/ring/fragments/QRCodeFragment.java +++ b/ring-android/app/src/main/java/cx/ring/fragments/QRCodeFragment.java @@ -53,11 +53,9 @@ public class QRCodeFragment extends BottomSheetDialogFragment { public static QRCodeFragment newInstance(int startPage) { QRCodeFragment fragment = new QRCodeFragment(); - Bundle args = new Bundle(); args.putInt(ARG_START_PAGE_INDEX, startPage); fragment.setArguments(args); - return fragment; } @@ -75,7 +73,6 @@ public class QRCodeFragment extends BottomSheetDialogFragment { mBinding = FragQrcodeBinding.inflate(inflater, container, false); mBinding.viewPager.setAdapter(new SectionsPagerAdapter(getContext(), getChildFragmentManager())); mBinding.tabs.setupWithViewPager(mBinding.viewPager); - return mBinding.getRoot(); } diff --git a/ring-android/app/src/main/java/cx/ring/fragments/ShareWithFragment.java b/ring-android/app/src/main/java/cx/ring/fragments/ShareWithFragment.java index 5403e187f19acacde5ca737592faea1010285ece..7acb77a7f0a0b86479305d414f0ffd0c793b5e05 100644 --- a/ring-android/app/src/main/java/cx/ring/fragments/ShareWithFragment.java +++ b/ring-android/app/src/main/java/cx/ring/fragments/ShareWithFragment.java @@ -146,7 +146,7 @@ public class ShareWithFragment extends Fragment { intent.putExtra(Intent.EXTRA_TEXT, previewText.getText().toString()); } intent.putExtra(ConversationFragment.KEY_ACCOUNT_ID, smartListViewModel.getAccountId()); - intent.putExtra(ConversationFragment.KEY_CONTACT_RING_ID, smartListViewModel.getContact().getPrimaryNumber()); + intent.putExtra(ConversationFragment.KEY_CONTACT_RING_ID, smartListViewModel.getUri().getUri()); intent.setClass(requireActivity(), ConversationActivity.class); startActivity(intent); } 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 c01e93690f762386b9268e4f3e09672aac02d178..d8671a9a03d1bc6a81d51208edcc4fc8b6720198 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 @@ -63,10 +63,8 @@ import cx.ring.R; import cx.ring.adapters.SmartListAdapter; import cx.ring.application.JamiApplication; import cx.ring.client.CallActivity; -import cx.ring.client.ConversationActivity; import cx.ring.client.HomeActivity; import cx.ring.databinding.FragSmartlistBinding; -import cx.ring.model.CallContact; import cx.ring.model.Conversation; import cx.ring.mvp.BaseSupportFragment; import cx.ring.services.AccountService; @@ -116,6 +114,7 @@ public class SmartListFragment extends BaseSupportFragment<SmartListPresenter> i setOverflowMenuVisible(menu, true); changeSeparatorHeight(false); binding.qrCode.setVisibility(View.GONE); + binding.newGroup.setVisibility(View.GONE); setTabletQRLayout(false); return true; } @@ -127,6 +126,7 @@ public class SmartListFragment extends BaseSupportFragment<SmartListPresenter> i setOverflowMenuVisible(menu, false); changeSeparatorHeight(true); binding.qrCode.setVisibility(View.VISIBLE); + binding.newGroup.setVisibility(View.VISIBLE); setTabletQRLayout(true); return true; } @@ -245,6 +245,8 @@ public class SmartListFragment extends BaseSupportFragment<SmartListPresenter> i 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) { @@ -276,6 +278,14 @@ public class SmartListFragment extends BaseSupportFragment<SmartListPresenter> i binding.newconvFab.setOnClickListener(v -> presenter.fabButtonClicked()); } + private void startNewGroup() { + ContactPickerFragment fragment = ContactPickerFragment.newInstance(); + fragment.show(getParentFragmentManager(), ContactPickerFragment.TAG); + binding.qrCode.setVisibility(View.GONE); + binding.newGroup.setVisibility(View.GONE); + setTabletQRLayout(false); + } + @Override public void setLoading(final boolean loading) { binding.loadingIndicator.setVisibility(loading ? View.VISIBLE : View.GONE); @@ -297,12 +307,12 @@ public class SmartListFragment extends BaseSupportFragment<SmartListPresenter> i } @Override - public void removeConversation(CallContact callContact) { + public void removeConversation(cx.ring.model.Uri callContact) { presenter.removeConversation(callContact); } @Override - public void clearConversation(CallContact callContact) { + public void clearConversation(cx.ring.model.Uri callContact) { presenter.clearConversation(callContact); } @@ -370,18 +380,18 @@ public class SmartListFragment extends BaseSupportFragment<SmartListPresenter> i } @Override - public void displayClearDialog(CallContact callContact) { - ActionHelper.launchClearAction(getActivity(), callContact, SmartListFragment.this); + public void displayClearDialog(cx.ring.model.Uri uri) { + ActionHelper.launchClearAction(getActivity(), uri, SmartListFragment.this); } @Override - public void displayDeleteDialog(CallContact callContact) { - ActionHelper.launchDeleteAction(getActivity(), callContact, SmartListFragment.this); + public void displayDeleteDialog(cx.ring.model.Uri uri) { + ActionHelper.launchDeleteAction(getActivity(), uri, SmartListFragment.this); } @Override - public void copyNumber(CallContact callContact) { - ActionHelper.launchCopyNumberToClipboardFromContact(getActivity(), callContact, this); + public void copyNumber(cx.ring.model.Uri uri) { + ActionHelper.launchCopyNumberToClipboardFromContact(getActivity(), uri, this); } @Override @@ -431,33 +441,29 @@ public class SmartListFragment extends BaseSupportFragment<SmartListPresenter> i public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == HomeActivity.REQUEST_CODE_QR_CONVERSATION && data != null && resultCode == Activity.RESULT_OK) { - String contactID = data.getStringExtra(ConversationFragment.KEY_CONTACT_RING_ID); - if (contactID != null) { - presenter.startConversation(new cx.ring.model.Uri(contactID)); + String contactId = data.getStringExtra(ConversationFragment.KEY_CONTACT_RING_ID); + if (contactId != null) { + presenter.startConversation(cx.ring.model.Uri.fromString(contactId)); } } } @Override - public void goToConversation(String accountId, cx.ring.model.Uri contactId) { + public void goToConversation(String accountId, cx.ring.model.Uri conversationUri) { + Log.w(TAG, "goToConversation " + accountId + " " + conversationUri); if (mSearchMenuItem != null) { mSearchMenuItem.collapseActionView(); } - - if (!DeviceUtils.isTablet(getContext())) { - startActivity(new Intent(Intent.ACTION_VIEW, ConversationPath.toUri(accountId, contactId.toString()), requireContext(), ConversationActivity.class)); - } else { - ((HomeActivity) requireActivity()).startConversationTablet(ConversationPath.toBundle(accountId, contactId.toString())); - } + ((HomeActivity) requireActivity()).startConversation(accountId, conversationUri); } @Override - public void goToCallActivity(String accountId, String contactId) { + public void goToCallActivity(String accountId, cx.ring.model.Uri conversationUri, String contactId) { Intent intent = new Intent(CallActivity.ACTION_CALL) - .setClass(requireActivity(), CallActivity.class) + .setClass(requireContext(), CallActivity.class) + .putExtras(ConversationPath.toBundle(accountId, conversationUri)) .putExtra(CallFragment.KEY_AUDIO_ONLY, false) - .putExtra(ConversationFragment.KEY_ACCOUNT_ID, accountId) - .putExtra(ConversationFragment.KEY_CONTACT_RING_ID, contactId); + .putExtra(Intent.EXTRA_PHONE_NUMBER, contactId); startActivityForResult(intent, HomeActivity.REQUEST_CODE_CALL); } @@ -466,6 +472,7 @@ public class SmartListFragment extends BaseSupportFragment<SmartListPresenter> i QRCodeFragment qrCodeFragment = QRCodeFragment.newInstance(QRCodeFragment.INDEX_SCAN); qrCodeFragment.show(getParentFragmentManager(), QRCodeFragment.TAG); binding.qrCode.setVisibility(View.GONE); + binding.newGroup.setVisibility(View.GONE); setTabletQRLayout(false); } 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 482b24065500519cd557b0a9baf4e4b6c6e269df..e2e01279f14ffa0241fbb96c99a84cbc838bfc99 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 @@ -143,7 +143,7 @@ public class DRingService extends Service { @Override public String placeCall(final String account, final String number, final boolean video) { - return mConversationFacade.placeCall(account, number, video).blockingGet().getDaemonIdString(); + return mConversationFacade.placeCall(account, Uri.fromString(number), video).blockingGet().getDaemonIdString(); } @Override @@ -621,9 +621,7 @@ public class DRingService extends Service { case ACTION_TRUST_REQUEST_ACCEPT: case ACTION_TRUST_REQUEST_REFUSE: case ACTION_TRUST_REQUEST_BLOCK: - if (extras != null) { - handleTrustRequestAction(action, extras); - } + handleTrustRequestAction(intent.getData(), action); break; case ACTION_CALL_ACCEPT: case ACTION_CALL_HOLD_ACCEPT: @@ -639,12 +637,12 @@ public class DRingService extends Service { case ACTION_CONV_ACCEPT: case ACTION_CONV_DISMISS: case ACTION_CONV_REPLY_INLINE: - handleConvAction(intent, action, extras); + handleConvAction(intent, action); break; case ACTION_FILE_ACCEPT: case ACTION_FILE_CANCEL: if (extras != null) { - handleFileAction(action, extras); + handleFileAction(intent.getData(), action, extras); } break; default: @@ -652,30 +650,30 @@ public class DRingService extends Service { } } - private void handleFileAction(String action, Bundle extras) { - Long id = extras.getLong(KEY_TRANSFER_ID); + private void handleFileAction(android.net.Uri uri, String action, Bundle extras) { + long id = extras.getLong(KEY_TRANSFER_ID); + ConversationPath path = ConversationPath.fromUri(uri); if (action.equals(ACTION_FILE_ACCEPT)) { - mAccountService.acceptFileTransfer(id); + mAccountService.acceptFileTransfer(path.getAccountId(), path.getConversationUri(), id); } else if (action.equals(ACTION_FILE_CANCEL)) { - mConversationFacade.cancelFileTransfer(id); + mConversationFacade.cancelFileTransfer(path.getAccountId(), path.getConversationUri(), id); } } - private void handleTrustRequestAction(String action, Bundle extras) { - String account = extras.getString(NotificationService.TRUST_REQUEST_NOTIFICATION_ACCOUNT_ID); - Uri from = new Uri(extras.getString(NotificationService.TRUST_REQUEST_NOTIFICATION_FROM)); - if (account != null) { - mNotificationService.cancelTrustRequestNotification(account); + private void handleTrustRequestAction(android.net.Uri uri, String action) { + ConversationPath path = ConversationPath.fromUri(uri); + if (path != null) { + mNotificationService.cancelTrustRequestNotification(path.getAccountId()); switch (action) { case ACTION_TRUST_REQUEST_ACCEPT: - mConversationFacade.acceptRequest(account, from); + mConversationFacade.acceptRequest(path.getAccountId(), path.getConversationUri()); break; case ACTION_TRUST_REQUEST_REFUSE: - mConversationFacade.discardRequest(account, from); + mConversationFacade.discardRequest(path.getAccountId(), path.getConversationUri()); break; case ACTION_TRUST_REQUEST_BLOCK: - mConversationFacade.discardRequest(account, from); - mAccountService.removeContact(account, from.getRawRingId(), true); + mConversationFacade.discardRequest(path.getAccountId(), path.getConversationUri()); + mAccountService.removeContact(path.getAccountId(), path.getConversationUri().getRawRingId(), true); break; } } @@ -740,16 +738,15 @@ public class DRingService extends Service { } } - private void handleConvAction(Intent intent, String action, Bundle extras) { + private void handleConvAction(Intent intent, String action) { ConversationPath path = ConversationPath.fromIntent(intent); - if (path == null || path.getConversationId().isEmpty()) { return; } switch (action) { case ACTION_CONV_READ: - mConversationFacade.readMessages(path.getAccountId(), new Uri(path.getConversationId())); + mConversationFacade.readMessages(path.getAccountId(), path.getConversationUri()); break; case ACTION_CONV_DISMISS: break; @@ -758,11 +755,11 @@ public class DRingService extends Service { if (remoteInput != null) { CharSequence reply = remoteInput.getCharSequence(KEY_TEXT_REPLY); if (!TextUtils.isEmpty(reply)) { - Uri uri = new Uri(path.getConversationId()); + Uri uri = path.getConversationUri(); String message = reply.toString(); mConversationFacade.startConversation(path.getAccountId(), uri) - .flatMap(c -> mConversationFacade.sendTextMessage(path.getAccountId(), c, uri, message) - .doOnSuccess(msg -> mNotificationService.showTextNotification(path.getAccountId(), c))) + .flatMapCompletable(c -> mConversationFacade.sendTextMessage(c, uri, message) + .doOnComplete(() -> mNotificationService.showTextNotification(path.getAccountId(), c))) .subscribe(); } } diff --git a/ring-android/app/src/main/java/cx/ring/service/OutgoingCallHandler.java b/ring-android/app/src/main/java/cx/ring/service/OutgoingCallHandler.java index 366ab362495064c12a4a6bee99c8021affc89ee5..894fa5215d505bb1a6f8c9da78307edcbc9ed391 100644 --- a/ring-android/app/src/main/java/cx/ring/service/OutgoingCallHandler.java +++ b/ring-android/app/src/main/java/cx/ring/service/OutgoingCallHandler.java @@ -49,16 +49,14 @@ public class OutgoingCallHandler extends BroadcastReceiver { boolean systemDialerSip = sharedPreferences.getBoolean(KEY_CACHE_HAVE_SIPACCOUNT, false); boolean systemDialerRing = sharedPreferences.getBoolean(KEY_CACHE_HAVE_RINGACCOUNT, false); - Uri uri = new Uri(phoneNumber); - boolean isRingId = uri.isRingId(); + Uri uri = Uri.fromString(phoneNumber); + boolean isRingId = uri.isHexId(); if ((!isRingId && systemDialerSip) || (isRingId && systemDialerRing) || uri.isSingleIp()) { - Intent i = new Intent(CallActivity.ACTION_CALL) + Intent i = new Intent(Intent.ACTION_CALL) .setClass(context, CallActivity.class) .setData(android.net.Uri.parse(phoneNumber)) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(i); - setResultData(null); } } diff --git a/ring-android/app/src/main/java/cx/ring/services/ContactServiceImpl.java b/ring-android/app/src/main/java/cx/ring/services/ContactServiceImpl.java index d17ca172c33904377282aae9aa5f62dc1de14a7a..6e727d90eac9a0000ca750267335a3e538d5948d 100644 --- a/ring-android/app/src/main/java/cx/ring/services/ContactServiceImpl.java +++ b/ring-android/app/src/main/java/cx/ring/services/ContactServiceImpl.java @@ -39,7 +39,6 @@ import cx.ring.contacts.AvatarFactory; import cx.ring.model.CallContact; import cx.ring.model.Uri; import cx.ring.utils.AndroidFileUtils; -import cx.ring.utils.BitmapUtils; import cx.ring.utils.Tuple; import cx.ring.utils.VCardUtils; import ezvcard.VCard; @@ -136,21 +135,22 @@ public class ContactServiceImpl extends ContactService { while (contactCursor.moveToNext()) { long contactId = contactCursor.getLong(indexId); - CallContact contact = cache.get(contactId); + String contactNumber = contactCursor.getString(indexNumber); + int contactType = contactCursor.getInt(indexType); + String contactLabel = contactCursor.getString(indexLabel); + Uri uri = Uri.fromString(contactNumber); + + CallContact contact = cache.get(contactId); boolean isNewContact = false; if (contact == null) { - contact = new CallContact(contactId); + contact = new CallContact(uri); + contact.setSystemId(contactId); isNewContact = true; contact.setFromSystem(true); } - String contactNumber = contactCursor.getString(indexNumber); - int contactType = contactCursor.getInt(indexType); - String contactLabel = contactCursor.getString(indexLabel); - Uri uri = new Uri(contactNumber); - - if (uri.isSingleIp() || (uri.isRingId() && loadRingContacts) || loadSipContacts) { + if (uri.isSingleIp() || (uri.isHexId() && loadRingContacts) || loadSipContacts) { switch (contactCursor.getString(indexMime)) { case ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE: contact.addPhoneNumber(uri, contactType, contactLabel); @@ -159,7 +159,7 @@ public class ContactServiceImpl extends ContactService { contact.addNumber(uri, contactType, contactLabel, cx.ring.model.Phone.NumberType.SIP); break; case ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE: - if (uri.isRingId()) { + if (uri.isHexId()) { contact.addNumber(uri, contactType, contactLabel, cx.ring.model.Phone.NumberType.UNKNOWN); } break; @@ -195,7 +195,7 @@ public class ContactServiceImpl extends ContactService { if (contact == null) Log.w(TAG, "Can't find contact with ID " + contactId); else { - contact.setContactInfos(contactCursor.getString(indexKey), contactCursor.getString(indexName), contactCursor.getLong(indexPhoto)); + contact.setSystemContactInfo(contactId, contactCursor.getString(indexKey), contactCursor.getString(indexName), contactCursor.getLong(indexPhoto)); systemContacts.put(contactId, contact); } } @@ -240,7 +240,8 @@ public class ContactServiceImpl extends ContactService { Log.d(TAG, "Contact name: " + result.getString(indexName) + " id:" + contactId + " key:" + result.getString(indexKey)); - contact = new CallContact(contactId, result.getString(indexKey), result.getString(indexName), result.getLong(indexPhoto)); + contact = new CallContact(Uri.fromString(contentUri.toString())); + contact.setSystemContactInfo(contactId, result.getString(indexKey), result.getString(indexName), result.getLong(indexPhoto)); if (result.getInt(indexStared) != 0) { contact.setStared(); @@ -262,7 +263,6 @@ public class ContactServiceImpl extends ContactService { } private void fillContactDetails(@NonNull CallContact callContact) { - ContentResolver contentResolver = mContext.getContentResolver(); try { @@ -303,7 +303,7 @@ public class ContactServiceImpl extends ContactService { String contactMime = cursorSip.getString(indexMime); String contactNumber = cursorSip.getString(indexSip); - if (!contactMime.contentEquals(ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE) || new Uri(contactNumber).isRingId() || "ring".equalsIgnoreCase(cursorSip.getString(indexLabel))) { + if (!contactMime.contentEquals(ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE) || Uri.fromString(contactNumber).isHexId() || "ring".equalsIgnoreCase(cursorSip.getString(indexLabel))) { callContact.addNumber(contactNumber, cursorSip.getInt(indexType), cursorSip.getString(indexLabel), cx.ring.model.Phone.NumberType.SIP); } Log.d(TAG, "SIP phone:" + contactNumber + " " + contactMime + " "); @@ -327,7 +327,7 @@ public class ContactServiceImpl extends ContactService { if (result == null) { Log.d(TAG, "findContactBySipNumberFromSystem: " + number + " can't find contact."); - return CallContact.buildSIP(new Uri(number)); + return CallContact.buildSIP(Uri.fromString(number)); } int indexId = result.getColumnIndex(ContactsContract.RawContacts.CONTACT_ID); @@ -338,7 +338,8 @@ public class ContactServiceImpl extends ContactService { if (result.moveToFirst()) { long contactId = result.getLong(indexId); - contact = new CallContact(contactId, result.getString(indexKey), result.getString(indexName), result.getLong(indexPhoto)); + contact = new CallContact(Uri.fromString(number)); + contact.setSystemContactInfo(contactId, result.getString(indexKey), result.getString(indexName), result.getLong(indexPhoto)); if (result.getInt(indexStared) != 0) { contact.setStared(); @@ -374,7 +375,8 @@ public class ContactServiceImpl extends ContactService { int indexKey = result.getColumnIndex(ContactsContract.Data.LOOKUP_KEY); int indexName = result.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME); int indexPhoto = result.getColumnIndex(ContactsContract.Contacts.PHOTO_ID); - callContact = new CallContact(result.getLong(indexId), result.getString(indexKey), result.getString(indexName), result.getLong(indexPhoto)); + callContact = new CallContact(Uri.fromString(number)); + callContact.setSystemContactInfo(result.getLong(indexId), result.getString(indexKey), result.getString(indexName), result.getLong(indexPhoto)); fillContactDetails(callContact); Log.d(TAG, "findContactByNumberFromSystem: " + number + " found " + callContact.getDisplayName()); } 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 82c47dc6bb346cccfda474a0bc65c03b55030cb6..7e26fb49f01d3ab5b32b6b7e8a4e03b07353960d 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 @@ -29,6 +29,7 @@ import android.net.NetworkInfo; import android.os.Build; import android.provider.ContactsContract; import android.util.Log; +import android.widget.Toast; import androidx.core.content.ContextCompat; @@ -44,6 +45,7 @@ import cx.ring.daemon.StringVect; import cx.ring.utils.AndroidFileUtils; import cx.ring.utils.NetworkUtils; import cx.ring.utils.StringUtils; +import io.reactivex.android.schedulers.AndroidSchedulers; public class DeviceRuntimeServiceImpl extends DeviceRuntimeService { @@ -71,6 +73,8 @@ public class DeviceRuntimeServiceImpl extends DeviceRuntimeService { System.loadLibrary("ring"); } catch (Exception e) { Log.e(TAG, "Could not load Jami library", e); + android.os.Process.killProcess(android.os.Process.myPid()); + System.exit(0); } }); } diff --git a/ring-android/app/src/main/java/cx/ring/services/HistoryServiceImpl.java b/ring-android/app/src/main/java/cx/ring/services/HistoryServiceImpl.java index 8528d723b66ad7d921cb721302628b027ebd8967..08bfb2055a3e5a5522d2a00774551d2636542fd6 100644 --- a/ring-android/app/src/main/java/cx/ring/services/HistoryServiceImpl.java +++ b/ring-android/app/src/main/java/cx/ring/services/HistoryServiceImpl.java @@ -22,6 +22,7 @@ package cx.ring.services; import android.content.Context; +import android.content.SharedPreferences; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import android.util.Log; @@ -37,13 +38,18 @@ import java.util.concurrent.ConcurrentHashMap; import javax.inject.Inject; +import cx.ring.R; import cx.ring.history.DatabaseHelper; +import cx.ring.model.Conversation; import cx.ring.model.ConversationHistory; import cx.ring.model.Interaction; +import cx.ring.model.Uri; import io.reactivex.Observable; import io.reactivex.subjects.BehaviorSubject; import io.reactivex.subjects.Subject; +import static cx.ring.fragments.ConversationFragment.KEY_PREFERENCE_CONVERSATION_LAST_READ; + /** * Implements the necessary Android related methods for the {@link HistoryService} */ @@ -183,6 +189,18 @@ public class HistoryServiceImpl extends HistoryService { deleteFolder(accountDir); } + @Override + public void setMessageRead(String accountId, Uri conversationUri, String lastId) { + SharedPreferences preferences = mContext.getSharedPreferences(accountId + "_" + conversationUri.getUri(), Context.MODE_PRIVATE); + preferences.edit().putString(KEY_PREFERENCE_CONVERSATION_LAST_READ, lastId).apply(); + } + + @Override + public String getLastMessageRead(String accountId, Uri conversationUri) { + SharedPreferences preferences = mContext.getSharedPreferences(accountId + "_" + conversationUri.getUri(), Context.MODE_PRIVATE); + return preferences.getString(KEY_PREFERENCE_CONVERSATION_LAST_READ, null); + } + /** * Deletes a file and all its children recursively * diff --git a/ring-android/app/src/main/java/cx/ring/services/LocationSharingService.java b/ring-android/app/src/main/java/cx/ring/services/LocationSharingService.java index 11b5dbc172dc929817275c4559c99bf18a7a3c67..c849964d503596985353d59f267db1af5c4eef11 100644 --- a/ring-android/app/src/main/java/cx/ring/services/LocationSharingService.java +++ b/ring-android/app/src/main/java/cx/ring/services/LocationSharingService.java @@ -212,7 +212,7 @@ public class LocationSharingService extends Service implements LocationListener StringMap msgs = new StringMap(); msgs.setRaw(CallService.MIME_GEOLOCATION, Blob.fromString(location.toString())); for (ConversationPath p : contactLocationShare.keySet()) { - Ringservice.sendAccountTextMessage(p.getAccountId(), p.getContactId(), msgs); + Ringservice.sendAccountTextMessage(p.getAccountId(), p.getConversationId(), msgs); } })); } else { @@ -237,7 +237,7 @@ public class LocationSharingService extends Service implements LocationListener Log.w(TAG, "location send " + jsonObject + " to " + contactLocationShare.size()); StringMap msgs = new StringMap(); msgs.setRaw(CallService.MIME_GEOLOCATION, Blob.fromString(jsonObject.toString())); - Ringservice.sendAccountTextMessage(path.getAccountId(), path.getContactId(), msgs); + Ringservice.sendAccountTextMessage(path.getAccountId(), path.getConversationId(), msgs); } mContactSharingSubject.onNext(contactLocationShare.keySet()); @@ -310,7 +310,7 @@ public class LocationSharingService extends Service implements LocationListener // Log.w(TAG, "getNotification " + firsPath.getContactId()); return mConversationFacade.getAccountSubject(firsPath.getAccountId()) - .map(account -> account.getContactFromCache(new Uri(firsPath.getContactId()))) + .map(account -> account.getContactFromCache(firsPath.getConversationUri())) .map(contact -> { String title; final Intent stopIntent = new Intent(ACTION_STOP).setClass(getApplicationContext(), LocationSharingService.class); 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 64ef6dc6f50903ff9b0260d9ab70e8fc8298433e..f2b3eef53efeabf03585e6c72f5f6151ecd4c77b 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 @@ -49,6 +49,7 @@ import androidx.core.app.RemoteInput; import androidx.core.content.ContextCompat; import androidx.core.content.res.ResourcesCompat; import androidx.core.graphics.drawable.IconCompat; +import androidx.core.util.Pair; import com.bumptech.glide.Glide; @@ -339,7 +340,7 @@ public class NotificationServiceImpl implements NotificationService { @Override public void showLocationNotification(Account first, CallContact contact) { - android.net.Uri path = ConversationPath.toUri(first.getAccountID(), contact.getPrimaryUri()); + android.net.Uri path = ConversationPath.toUri(first.getAccountID(), contact.getUri()); Intent intentConversation = new Intent(Intent.ACTION_VIEW, path, mContext, ConversationActivity.class) .putExtra(ConversationFragment.EXTRA_SHOW_MAP, true); @@ -360,7 +361,7 @@ public class NotificationServiceImpl implements NotificationService { @Override public void cancelLocationNotification(Account first, CallContact contact) { - notificationManager.cancel(Objects.hash( "Location", ConversationPath.toUri(first.getAccountID(), contact.getPrimaryUri()))); + notificationManager.cancel(Objects.hash( "Location", ConversationPath.toUri(first.getAccountID(), contact.getUri()))); } /** @@ -452,17 +453,17 @@ public class NotificationServiceImpl implements NotificationService { * Handles the creation and destruction of services associated with transfers as well as displaying notifications. * * @param transfer the data transfer object - * @param contact the contact to whom the data transfer is being sent + * @param conversation the contact to whom the data transfer is being sent * @param remove true if it should be removed from current calls */ @Override - public void handleDataTransferNotification(DataTransfer transfer, CallContact contact, boolean remove) { + public void handleDataTransferNotification(DataTransfer transfer, Conversation conversation, boolean remove) { Log.d(TAG, "handleDataTransferNotification, a data transfer event is in progress"); if (DeviceUtils.isTv(mContext)) { return; } if (!remove) { - showFileTransferNotification(transfer, contact); + showFileTransferNotification(conversation, transfer); } else { removeTransferNotification(transfer.getDaemonId()); } @@ -498,25 +499,27 @@ public class NotificationServiceImpl implements NotificationService { public void showTextNotification(String accountId, Conversation conversation) { TreeMap<Long, TextMessage> texts = conversation.getUnreadTextMessages(); - CallContact contact = conversation.getContact(); + Log.w(TAG, "showTextNotification start " + accountId + " " + conversation.getUri() + " " + texts.size()); + + //TODO handle groups if (texts.isEmpty() || conversation.isVisible()) { - cancelTextNotification(contact.getPrimaryUri()); + cancelTextNotification(conversation.getUri()); return; } if (texts.lastEntry().getValue().isNotified()) { return; } - Log.w(TAG, "showTextNotification " + accountId + " " + contact.getPrimaryNumber()); - mContactService.getLoadedContact(accountId, contact) - .subscribe(c -> textNotification(accountId, texts, c), + Log.w(TAG, "showTextNotification " + accountId + " " + conversation.getUri()); + mContactService.getLoadedContact(accountId, conversation.getContacts(), false) + .subscribe(c -> textNotification(accountId, texts, conversation), e -> Log.w(TAG, "Can't load contact", e)); } - private void textNotification(String accountId, TreeMap<Long, TextMessage> texts, CallContact contact) { - Uri contactUri = contact.getPrimaryUri(); - String contactId = contactUri.getUri(); - android.net.Uri path = ConversationPath.toUri(accountId, contactId); + private void textNotification(String accountId, TreeMap<Long, TextMessage> texts, Conversation conversation) { + android.net.Uri path = ConversationPath.toUri(conversation.getAccountId(), conversation.getUri()); + Pair<Bitmap, String> conversationProfile = getProfile(conversation); + int notificationVisibility = mPreferencesService.getSettings().getNotificationVisibility(); switch (notificationVisibility){ case SettingsFragment.NOTIFICATION_PUBLIC: @@ -530,11 +533,6 @@ public class NotificationServiceImpl implements NotificationService { notificationVisibility = Notification.VISIBILITY_PRIVATE; } - String contactName = contact.getDisplayName(); - if (TextUtils.isEmpty(contactName) || texts.isEmpty()) - return; - Bitmap contactPicture = getContactPicture(contact); - TextMessage last = texts.lastEntry().getValue(); Intent intentConversation = new Intent(DRingService.ACTION_CONV_ACCEPT, path, mContext, DRingService.class); Intent intentDelete = new Intent(DRingService.ACTION_CONV_DISMISS, path, mContext, DRingService.class); @@ -545,7 +543,7 @@ public class NotificationServiceImpl implements NotificationService { .setDefaults(NotificationCompat.DEFAULT_ALL) .setVisibility(notificationVisibility) .setSmallIcon(R.drawable.ic_ring_logo_white) - .setContentTitle(contactName) + .setContentTitle(conversationProfile.second) .setContentText(last.getBody()) .setWhen(last.getTimestamp()) .setContentIntent(PendingIntent.getService(mContext, random.nextInt(), intentConversation, 0)) @@ -553,28 +551,28 @@ public class NotificationServiceImpl implements NotificationService { .setAutoCancel(true) .setColor(ResourcesCompat.getColor(mContext.getResources(), R.color.color_primary_dark, null)); - String key = ConversationPath.toKey(accountId, contactId); + String key = ConversationPath.toKey(accountId, conversation.getUri()); Person contactPerson = new Person.Builder() .setKey(key) - .setName(contactName) - .setIcon(contactPicture == null ? null : IconCompat.createWithBitmap(contactPicture)) + .setName(conversationProfile.second) + .setIcon(conversationProfile.first == null ? null : IconCompat.createWithBitmap(conversationProfile.first)) .build(); - if (contactPicture != null) { - messageNotificationBuilder.setLargeIcon(contactPicture); + if (conversationProfile.first != null) { + messageNotificationBuilder.setLargeIcon(conversationProfile.first); Intent intentBubble = new Intent(Intent.ACTION_VIEW, path, mContext, ConversationActivity.class); intentBubble.putExtra(EXTRA_BUBBLE, true); messageNotificationBuilder.setBubbleMetadata(new NotificationCompat.BubbleMetadata.Builder() .setDesiredHeight(600) - .setIcon(IconCompat.createWithAdaptiveBitmap(contactPicture)) + .setIcon(IconCompat.createWithAdaptiveBitmap(conversationProfile.first)) .setIntent(PendingIntent.getActivity(mContext, 0, intentBubble, PendingIntent.FLAG_UPDATE_CURRENT)) .build()) .setShortcutId(key); } - UnreadConversation.Builder unreadConvBuilder = new UnreadConversation.Builder(contactName) + UnreadConversation.Builder unreadConvBuilder = new UnreadConversation.Builder(conversationProfile.second) .setLatestTimestamp(last.getTimestamp()); if (texts.size() == 1) { @@ -593,6 +591,13 @@ public class NotificationServiceImpl implements NotificationService { NotificationCompat.MessagingStyle history = new NotificationCompat.MessagingStyle(userPerson); for (TextMessage textMessage : texts.values()) { + CallContact contact = textMessage.getContact(); + Bitmap contactPicture = getContactPicture(contact); + Person contactPerson = new Person.Builder() + .setKey(textMessage.getAuthor()) + .setName(contact.getDisplayName()) + .setIcon(contactPicture == null ? null : IconCompat.createWithBitmap(contactPicture)) + .build(); history.addMessage(new NotificationCompat.MessagingStyle.Message( textMessage.getBody(), textMessage.getTimestamp(), @@ -602,7 +607,7 @@ public class NotificationServiceImpl implements NotificationService { messageNotificationBuilder.setStyle(history); } - int notificationId = getTextNotificationId(contactUri); + int notificationId = getTextNotificationId(conversation.getUri()); int replyId = notificationId + 1; int markAsReadId = notificationId + 2; @@ -670,38 +675,31 @@ public class NotificationServiceImpl implements NotificationService { return; if (requests.size() == 1) { Conversation request = requests.iterator().next(); - CallContact contact = request.getContact(); - String contactKey = contact.getPrimaryUri().getRawRingId(); + String contactKey = request.getUri().getRawUriString(); if (notifiedRequests.contains(contactKey)) { return; } - mContactService.getLoadedContact(account.getAccountID(), contact).subscribe(c -> { + mContactService.getLoadedContact(account.getAccountID(), request.getContacts(), false).subscribe(c -> { NotificationCompat.Builder builder = getRequestNotificationBuilder(account.getAccountID()); mPreferencesService.saveRequestPreferences(account.getAccountID(), contactKey); - Bundle info = new Bundle(); - info.putString(TRUST_REQUEST_NOTIFICATION_ACCOUNT_ID, account.getAccountID()); - info.putString(TRUST_REQUEST_NOTIFICATION_FROM, c.getPrimaryNumber()); - builder.setContentText(c.getRingUsername()) + android.net.Uri info = ConversationPath.toUri(account.getAccountID(), request.getUri()); + builder.setContentText(request.getUriTitle()) .addAction(R.drawable.baseline_person_add_24, mContext.getText(R.string.accept), PendingIntent.getService(mContext, random.nextInt(), - new Intent(DRingService.ACTION_TRUST_REQUEST_ACCEPT) - .setClass(mContext, DRingService.class) - .putExtras(info), + new Intent(DRingService.ACTION_TRUST_REQUEST_ACCEPT, info, mContext, DRingService.class), PendingIntent.FLAG_ONE_SHOT)) .addAction(R.drawable.baseline_delete_24, mContext.getText(R.string.refuse), PendingIntent.getService(mContext, random.nextInt(), - new Intent(DRingService.ACTION_TRUST_REQUEST_REFUSE) - .setClass(mContext, DRingService.class) - .putExtras(info), + new Intent(DRingService.ACTION_TRUST_REQUEST_REFUSE, info, mContext, DRingService.class), PendingIntent.FLAG_ONE_SHOT)) .addAction(R.drawable.baseline_block_24, mContext.getText(R.string.block), PendingIntent.getService(mContext, random.nextInt(), - new Intent(DRingService.ACTION_TRUST_REQUEST_BLOCK) - .setClass(mContext, DRingService.class) - .putExtras(info), + new Intent(DRingService.ACTION_TRUST_REQUEST_BLOCK, info, mContext, DRingService.class), PendingIntent.FLAG_ONE_SHOT)); - setContactPicture(c, builder); + Bitmap pic = getContactPicture(request); + if (pic != null) + builder.setLargeIcon(pic); notificationManager.notify(notificationId, builder.build()); }, e -> Log.w(TAG, "error showing notification", e)); } else { @@ -709,10 +707,12 @@ public class NotificationServiceImpl implements NotificationService { boolean newRequest = false; for (Conversation request : requests) { CallContact contact = request.getContact(); - String contactKey = contact.getPrimaryUri().getRawRingId(); - if (!notifiedRequests.contains(contactKey)) { - newRequest = true; - mPreferencesService.saveRequestPreferences(account.getAccountID(), contactKey); + if (contact != null) { + String contactKey = contact.getUri().getRawRingId(); + if (!notifiedRequests.contains(contactKey)) { + newRequest = true; + mPreferencesService.saveRequestPreferences(account.getAccountID(), contactKey); + } } } if (!newRequest) @@ -724,7 +724,7 @@ public class NotificationServiceImpl implements NotificationService { } @Override - public void showFileTransferNotification(DataTransfer info, CallContact contact) { + public void showFileTransferNotification(Conversation conversation, DataTransfer info) { if (info == null) { return; } @@ -735,7 +735,7 @@ public class NotificationServiceImpl implements NotificationService { long dataTransferId = info.getDaemonId(); int notificationId = getFileTransferNotificationId(dataTransferId); - android.net.Uri path = ConversationPath.toUri(info.getAccount(), new Uri(info.getConversation().getParticipant())); + android.net.Uri path = ConversationPath.toUri(info.getAccount(), conversation.getUri()); Intent intentConversation = new Intent(DRingService.ACTION_CONV_ACCEPT, path, mContext, DRingService.class); @@ -752,7 +752,7 @@ public class NotificationServiceImpl implements NotificationService { .setAutoCancel(true); if (info.showPicture()) { - File filePath = mDeviceRuntimeService.getConversationPath(info.getPeerId(), info.getStoragePath()); + File filePath = mDeviceRuntimeService.getConversationPath(conversation.getUri().getRawRingId(), info.getStoragePath()); Bitmap img; try { BitmapDrawable d = (BitmapDrawable) Glide.with(mContext) @@ -760,7 +760,7 @@ public class NotificationServiceImpl implements NotificationService { .submit() .get(); img = d.getBitmap(); - notif.setContentTitle(mContext.getString(R.string.notif_incoming_picture, contact.getDisplayName())); + notif.setContentTitle(mContext.getString(R.string.notif_incoming_picture, conversation.getTitle())); notif.setStyle(new NotificationCompat.BigPictureStyle() .bigPicture(img)); } catch (Exception e) { @@ -768,11 +768,12 @@ public class NotificationServiceImpl implements NotificationService { return; } } else { - notif.setContentTitle(mContext.getString(R.string.notif_incoming_file_transfer_title, contact.getDisplayName())); + notif.setContentTitle(mContext.getString(R.string.notif_incoming_file_transfer_title, conversation.getTitle())); notif.setStyle(null); } - - setContactPicture(contact, notif); + Bitmap picture = getContactPicture(conversation); + if (picture != null) + notif.setLargeIcon(picture); notificationManager.notify(random.nextInt(), notif.build()); return; } @@ -782,7 +783,7 @@ public class NotificationServiceImpl implements NotificationService { } boolean ongoing = event == InteractionStatus.TRANSFER_ONGOING || event == InteractionStatus.TRANSFER_ACCEPTED; - String titleMessage = mContext.getString(info.isOutgoing() ? R.string.notif_outgoing_file_transfer_title : R.string.notif_incoming_file_transfer_title, contact.getDisplayName()); + String titleMessage = mContext.getString(info.isOutgoing() ? R.string.notif_outgoing_file_transfer_title : R.string.notif_incoming_file_transfer_title, conversation.getTitle()); messageNotificationBuilder.setContentTitle(titleMessage) .setPriority(NotificationCompat.PRIORITY_DEFAULT) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) @@ -796,7 +797,9 @@ public class NotificationServiceImpl implements NotificationService { info.getDisplayName() + ": " + ResourceMapper.getReadableFileTransferStatus(mContext, event)) .setContentIntent(PendingIntent.getService(mContext, random.nextInt(), intentConversation, 0)) .setColor(ResourcesCompat.getColor(mContext.getResources(), R.color.color_primary_dark, null)); - setContactPicture(contact, messageNotificationBuilder); + Bitmap picture = getContactPicture(conversation); + if (picture != null) + messageNotificationBuilder.setLargeIcon(picture); if (event.isOver()) { messageNotificationBuilder.setProgress(0, 0, false); } else if (ongoing) { @@ -817,14 +820,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) - .setClass(mContext, DRingService.class) + new Intent(DRingService.ACTION_FILE_ACCEPT, ConversationPath.toUri(conversation), 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) - .setClass(mContext, DRingService.class) + new Intent(DRingService.ACTION_FILE_CANCEL, ConversationPath.toUri(conversation), mContext, DRingService.class) .putExtra(DRingService.KEY_TRANSFER_ID, dataTransferId), PendingIntent.FLAG_ONE_SHOT)); mNotificationBuilders.put(notificationId, messageNotificationBuilder); @@ -840,7 +841,6 @@ public class NotificationServiceImpl implements NotificationService { PendingIntent.FLAG_ONE_SHOT)); } mNotificationBuilders.put(notificationId, messageNotificationBuilder); - dataTransferNotifications.remove(notificationId); dataTransferNotifications.put(notificationId, messageNotificationBuilder.build()); startForegroundService(notificationId, DataTransferService.class); } @@ -901,8 +901,8 @@ public class NotificationServiceImpl implements NotificationService { mNotificationBuilders.remove(notificationId); } - public void cancelTextNotification(String ringId) { - int notificationId = (NOTIF_MSG + ringId).hashCode(); + public void cancelTextNotification(String accountId, Uri contact) { + int notificationId = getTextNotificationId(contact); notificationManager.cancel(notificationId); mNotificationBuilders.remove(notificationId); } @@ -954,7 +954,14 @@ public class NotificationServiceImpl implements NotificationService { private Bitmap getContactPicture(CallContact contact) { try { - return AvatarFactory.getBitmapAvatar(mContext, contact, avatarSize).blockingGet(); + return AvatarFactory.getBitmapAvatar(mContext, contact, avatarSize, false).blockingGet(); + } catch (Exception e) { + return null; + } + } + private Bitmap getContactPicture(Conversation conversation) { + try { + return AvatarFactory.getBitmapAvatar(mContext, conversation, avatarSize, false).blockingGet(); } catch (Exception e) { return null; } @@ -964,6 +971,10 @@ public class NotificationServiceImpl implements NotificationService { return AvatarFactory.getBitmapAvatar(mContext, account, avatarSize).blockingGet(); } + private Pair<Bitmap, String> getProfile(Conversation conversation) { + return Pair.create(getContactPicture(conversation), conversation.getTitle()); + } + private void setContactPicture(CallContact contact, NotificationCompat.Builder messageNotificationBuilder) { Bitmap pic = getContactPicture(contact); if (pic != null) 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 e0a875323cf9d19c7cef4d36d45a1841c6a761da..afc22c712d54eb249d5d6a1a47bd077734fa2c0f 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 @@ -27,13 +27,9 @@ import androidx.annotation.NonNull; import java.io.ByteArrayOutputStream; import java.io.File; -import java.util.List; -import java.util.Map; import cx.ring.model.Account; -import cx.ring.model.CallContact; import cx.ring.utils.BitmapUtils; -import cx.ring.utils.FileUtils; import cx.ring.utils.Tuple; import cx.ring.utils.VCardUtils; import ezvcard.VCard; @@ -110,94 +106,6 @@ public class VCardServiceImpl extends VCardService { return new Tuple<>(profile.first, BitmapUtils.bytesToBitmap(profile.second)); } - /** - * Migrates the user's contacts to their individual account folders under the subfolder profiles - * @param contacts a hash map of the user's contacts - * @param accountId the directory where the profile is stored - */ - @Override - public void migrateContact(Map<String, CallContact> contacts, String accountId) { - File fileDir = mContext.getFilesDir(); - File legacyProfileFolder = new File(fileDir, "peer_profiles"); - if (!legacyProfileFolder.exists()) - return; - - File[] profiles = legacyProfileFolder.listFiles(); - - if (profiles == null) - return; - - File accountDir = new File(fileDir, accountId); - File profilesDir = new File(accountDir, "profiles"); - profilesDir.mkdirs(); - - - for (File profile : profiles) { - String filename = profile.getName(); - String contactUri = filename.lastIndexOf(".") > 0 ? filename.substring(0, filename.lastIndexOf(".")) : filename; - if (contacts.containsKey(contactUri)) { - File destination = new File(profilesDir, filename); - FileUtils.copyFile(profile, destination); - } - } - } - - /** - * Migrates the user's vcards and renames them to profile.vcf - * - * @param accountIds the list of accounts to migrate - */ - @Override - public void migrateProfiles(List<String> accountIds) { - File fileDir = mContext.getFilesDir(); - File profileDir = new File(fileDir, "profiles"); - - if (!profileDir.exists()) - return; - - File[] profiles = profileDir.listFiles(); - - if (profiles == null) - return; - - for (File profile : profiles) { - for (String account : accountIds) { - if (profile.getName().equals(account + ".vcf")) { - File accountDir = new File(fileDir, account); - File newProfile = new File(accountDir, VCardUtils.LOCAL_USER_VCARD_NAME); - FileUtils.moveFile(profile, newProfile); - break; - } - } - } - - // necessary to delete profiles leftover by deleted accounts - for (File profile : profiles) { - profile.delete(); - } - - - profileDir.delete(); - } - - /** - * Deletes the legacy peer_profiles folder - */ - @Override - public void deleteLegacyProfiles() { - File fileDir = mContext.getFilesDir(); - File legacyProfileFolder = new File(fileDir, "peer_profiles"); - File[] profiles = legacyProfileFolder.listFiles(); - if (profiles == null) - return; - - for (File file : profiles) { - file.delete(); - } - - legacyProfileFolder.delete(); - } - @Override public Object base64ToBitmap(String base64) { return BitmapUtils.base64ToBitmap(base64); diff --git a/ring-android/app/src/main/java/cx/ring/share/ScanFragment.java b/ring-android/app/src/main/java/cx/ring/share/ScanFragment.java index 9b3325dd9d05b5154791086e1e48457d5ee9df08..519c21f9b78425cf94769ab8a19cde13c5fa35ff 100644 --- a/ring-android/app/src/main/java/cx/ring/share/ScanFragment.java +++ b/ring-android/app/src/main/java/cx/ring/share/ScanFragment.java @@ -52,15 +52,12 @@ import cx.ring.R; import cx.ring.application.JamiApplication; import cx.ring.client.HomeActivity; import cx.ring.fragments.ConversationFragment; +import cx.ring.fragments.QRCodeFragment; import cx.ring.mvp.BaseSupportFragment; public class ScanFragment extends BaseSupportFragment { - public static final String TAG = ScanFragment.class.getSimpleName(); - private boolean mIsVisible; - private boolean mIsStarted; - private DecoratedBarcodeView barcodeView; private TextView mErrorMessageTextView; @@ -99,20 +96,6 @@ public class ScanFragment extends BaseSupportFragment { } } - @Override - public void onStart() { - super.onStart(); - mIsStarted = true; - if (mIsVisible) - checkPermission(); - } - - @Override - public void onStop() { - super.onStop(); - mIsStarted = false; - } - private void showErrorPanel(final int textResId) { if (mErrorMessageTextView != null) { mErrorMessageTextView.setText(textResId); @@ -157,9 +140,8 @@ public class ScanFragment extends BaseSupportFragment { } private void initializeBarcode() { if (barcodeView != null) { - Collection<BarcodeFormat> formats = Collections.singletonList(BarcodeFormat.QR_CODE); - barcodeView.getBarcodeView().setDecoderFactory(new DefaultDecoderFactory(formats)); - barcodeView.initializeFromIntent(getActivity().getIntent()); + barcodeView.getBarcodeView().setDecoderFactory(new DefaultDecoderFactory(Collections.singletonList(BarcodeFormat.QR_CODE))); + //barcodeView.initializeFromIntent(getActivity().getIntent()); barcodeView.decodeContinuous(callback); } } @@ -168,9 +150,13 @@ public class ScanFragment extends BaseSupportFragment { @Override public void barcodeResult(@NonNull BarcodeResult result) { if (result.getText() != null) { - String contact_uri = result.getText(); - if (contact_uri != null ) { - goToConversation(contact_uri); + String contactUri = result.getText(); + if (contactUri != null) { + QRCodeFragment parent = (QRCodeFragment) getParentFragment(); + if (parent != null) { + parent.dismiss(); + } + goToConversation(contactUri); } } } @@ -180,8 +166,8 @@ public class ScanFragment extends BaseSupportFragment { } }; - private void goToConversation(String contactId) { - ((HomeActivity) requireActivity()).startConversation(contactId); + private void goToConversation(String conversationUri) { + ((HomeActivity) requireActivity()).startConversation(conversationUri); } private boolean checkPermission() { 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 17a76af6ce3ea82bbe9b977970367ec53b133548..90c20b9f4b0be1bd5e8f5c6c1971d2819dfa6317 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 @@ -323,7 +323,7 @@ public class TVCallFragment extends BaseSupportFragment<CallPresenter> implement if (resultCode == Activity.RESULT_OK && data != null) { ConversationPath path = ConversationPath.fromUri(data.getData()); if (path != null) { - presenter.addConferenceParticipant(path.getAccountId(), path.getContactId()); + presenter.addConferenceParticipant(path.getAccountId(), path.getConversationUri()); } } } @@ -651,9 +651,11 @@ public class TVCallFragment extends BaseSupportFragment<CallPresenter> implement Bundle args; args = getArguments(); if (args != null) { - presenter.initOutGoing(getArguments().getString(KEY_ACCOUNT_ID), - getArguments().getString(KEY_CONTACT_RING_ID), - getArguments().getBoolean(KEY_AUDIO_ONLY)); + ConversationPath conversation = ConversationPath.fromBundle(args); + presenter.initOutGoing(conversation.getAccountId(), + conversation.getConversationUri(), + args.getString(Intent.EXTRA_PHONE_NUMBER), + args.getBoolean(KEY_AUDIO_ONLY)); } } } 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 5e95306e5549daaa066e81b96724ba2ba45e5f4e..f85cfdaae2c2591ab8bec4b5c556281d5574b7a7 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 @@ -40,10 +40,10 @@ public class ContactCard extends Card { public void setModel(SmartListViewModel model) { mModel = model; - setTitle(mModel.getContact().getDisplayName()); - String username = mModel.getContact().getRingUsername(); + setTitle(mModel.getContactName()); + String username = mModel.getContact().get(0).getRingUsername(); setDescription(username); - boolean isOnline = mModel.getContact().isOnline(); + boolean isOnline = mModel.getContact().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/cards/contacts/ContactCardPresenter.java b/ring-android/app/src/main/java/cx/ring/tv/cards/contacts/ContactCardPresenter.java index b9995a01373e6904b9f5ab3639660718023de30c..f0798ae0f5bfa139240b0488fec12fbcc53d65c5 100644 --- a/ring-android/app/src/main/java/cx/ring/tv/cards/contacts/ContactCardPresenter.java +++ b/ring-android/app/src/main/java/cx/ring/tv/cards/contacts/ContactCardPresenter.java @@ -74,7 +74,7 @@ public class ContactCardPresenter extends AbstractCardPresenter<ImageCardView> { cardView.setBackgroundColor(cardView.getResources().getColor(R.color.color_primary_dark)); cardView.setMainImage( new AvatarDrawable.Builder() - .withContact(model.getContact()) + .withViewModel(model) .withPresence(false) .withCircleCrop(false) .build(getContext()) diff --git a/ring-android/app/src/main/java/cx/ring/tv/contact/TVContactFragment.java b/ring-android/app/src/main/java/cx/ring/tv/contact/TVContactFragment.java index c42bf43e45ac1c6c78248f3fa14feda33dd1a25a..5b75bac06d3b9bade0e3af2cc8413c9c13e0ffd6 100644 --- a/ring-android/app/src/main/java/cx/ring/tv/contact/TVContactFragment.java +++ b/ring-android/app/src/main/java/cx/ring/tv/contact/TVContactFragment.java @@ -168,8 +168,8 @@ public class TVContactFragment extends BaseDetailFragment<TVContactPresenter> im final DetailsOverviewRow row = new DetailsOverviewRow(model); AvatarDrawable avatar = new AvatarDrawable.Builder() - .withContact(model.getContact()) - .withPresence(false) + .withViewModel(model) + //.withPresence(false) .withCircleCrop(false) .build(getActivity()); avatar.setInSize(iconSize); 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 55493821f4b52f344852855faac13f12d818270c..a3734c74bbaf636108e63d5d02a7f21b83cf11c2 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 @@ -26,6 +26,7 @@ import cx.ring.daemon.Blob; import cx.ring.facades.ConversationFacade; import cx.ring.model.Account; import cx.ring.model.Conference; +import cx.ring.model.Conversation; import cx.ring.model.SipCall; import cx.ring.model.Uri; import cx.ring.mvp.RootPresenter; @@ -98,9 +99,10 @@ 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) - .subscribe(vCard -> mAccountService.sendTrustRequest(accountId, conversationUri.getRawRingId(), Blob.fromString(VCardUtils.vcardToString(vCard))), - e -> mAccountService.sendTrustRequest(accountId, conversationUri.getRawRingId(), null)); + .subscribe(vCard -> mAccountService.sendTrustRequest(conversation, conversationUri, Blob.fromString(VCardUtils.vcardToString(vCard))), + e -> mAccountService.sendTrustRequest(conversation, conversationUri, null)); } public void acceptTrustRequest() { 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 213757da530586b896bdd2db6c0755c633303366..f797751982263ec50925c09ec07e3d494cdb6262 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().getRingUsername(); + String id = viewModel.getContact().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 4b1f3206f7529e298b279acf9987f304bfcfab4e..4e968ad08a721097d50f1dd42aac1e939bedd471 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 @@ -365,7 +365,7 @@ public class TvConversationAdapter extends RecyclerView.Adapter<TvConversationVi private void configureForFileInfoTextMessage(@NonNull final TvConversationViewHolder viewHolder, @NonNull final Interaction interaction, int position) { DataTransfer file = (DataTransfer) interaction; - File path = presenter.getDeviceRuntimeService().getConversationPath(file.getPeerId(), file.getStoragePath()); + File path = presenter.getDeviceRuntimeService().getConversationPath(file.getConversationId(), file.getStoragePath()); file.setSize(path.length()); String timeString = timestampToDetailString(viewHolder.itemView.getContext(), file.getTimestamp()); diff --git a/ring-android/app/src/main/java/cx/ring/tv/conversation/TvConversationFragment.java b/ring-android/app/src/main/java/cx/ring/tv/conversation/TvConversationFragment.java index 8552988ee26d483d5de7769227aef878ce095982..ee7b2b6e8d1ca155bfe7bc56ef871d825fe0f002 100644 --- a/ring-android/app/src/main/java/cx/ring/tv/conversation/TvConversationFragment.java +++ b/ring-android/app/src/main/java/cx/ring/tv/conversation/TvConversationFragment.java @@ -74,6 +74,7 @@ import cx.ring.model.DataTransfer; import cx.ring.model.Error; import cx.ring.model.Interaction; import cx.ring.mvp.BaseSupportFragment; +import cx.ring.service.DRingService; import cx.ring.tv.camera.CustomCameraActivity; import cx.ring.utils.AndroidFileUtils; import cx.ring.utils.ContentUriHandler; @@ -605,15 +606,16 @@ public class TvConversationFragment extends BaseSupportFragment<ConversationPres } @Override - public void displayContact(CallContact contact) { + public void displayContact(Conversation conversation) { + List<CallContact> contacts = conversation.getContacts(); mCompositeDisposable.clear(); - mCompositeDisposable.add(AvatarFactory.getAvatar(requireContext(), contact) + mCompositeDisposable.add(AvatarFactory.getAvatar(requireContext(), conversation, true) .doOnSuccess(d -> { mConversationAvatar = (AvatarDrawable) d; - mParticipantAvatars.put(contact.getPrimaryNumber(), + mParticipantAvatars.put(contacts.get(0).getPrimaryNumber(), new AvatarDrawable((AvatarDrawable) d)); }) - .flatMapObservable(d -> contact.getUpdatesSubject()) + .flatMapObservable(d -> contacts.get(0).getUpdatesSubject()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(c -> { String id = c.getRingUsername(); @@ -624,7 +626,7 @@ public class TvConversationFragment extends BaseSupportFragment<ConversationPres else binding.subtitle.setVisibility(View.GONE); mConversationAvatar.update(c); - String uri = contact.getPrimaryNumber(); + String uri = contacts.get(0).getPrimaryNumber(); AvatarDrawable a = mParticipantAvatars.get(uri); if (a != null) a.update(c); @@ -655,6 +657,16 @@ public class TvConversationFragment extends BaseSupportFragment<ConversationPres } + @Override + public void updateContact(CallContact contact) { + mCompositeDisposable.add(AvatarFactory.getAvatar(requireContext(), contact, true) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(avatar -> { + mParticipantAvatars.put(contact.getPrimaryNumber(), (AvatarDrawable) avatar); + mAdapter.setPhoto(); + })); + } + @Override public void setComposingStatus(Account.ComposingStatus composingStatus) { @@ -709,6 +721,11 @@ public class TvConversationFragment extends BaseSupportFragment<ConversationPres } + @Override + public void updateLastRead(String last) { + + } + @Override public void displayOnGoingCallPane(boolean display) { @@ -745,12 +762,12 @@ public class TvConversationFragment extends BaseSupportFragment<ConversationPres } @Override - public void goToCallActivityWithResult(String accountId, String contactRingId, boolean audioOnly) { + public void goToCallActivityWithResult(String accountId, cx.ring.model.Uri conversationUri, cx.ring.model.Uri contactRingId, boolean audioOnly) { } @Override - public void goToContactActivity(String accountId, String contactRingId) { + public void goToContactActivity(String accountId, cx.ring.model.Uri contactRingId) { } @@ -774,4 +791,26 @@ public class TvConversationFragment extends BaseSupportFragment<ConversationPres } + @Override + public void acceptFile(String accountId, net.jami.model.Uri conversationUri, DataTransfer transfer) { + File cacheDir = requireContext().getCacheDir(); + long spaceLeft = AndroidFileUtils.getSpaceLeft(cacheDir.toString()); + if (spaceLeft == -1L || transfer.getTotalSize() > spaceLeft) { + presenter.noSpaceLeft(); + return; + } + requireActivity().startService(new Intent(DRingService.ACTION_FILE_ACCEPT) + .setClass(requireContext(), DRingService.class) + .setData(ConversationPath.toUri(accountId, conversationUri)) + .putExtra(DRingService.KEY_TRANSFER_ID, transfer.getDaemonId())); + } + + @Override + public void refuseFile(String accountId, net.jami.model.Uri conversationUri, DataTransfer transfer) { + requireActivity().startService(new Intent(DRingService.ACTION_FILE_CANCEL) + .setClass(requireContext(), DRingService.class) + .setData(ConversationPath.toUri(accountId, conversationUri)) + .putExtra(DRingService.KEY_TRANSFER_ID, transfer.getDaemonId())); + } + } 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 edcb09ff98480fe1aa4973275dd2f990e3f6934e..63cd7f69cd38e4b15ac5504ac5eb8ce5e5a54375 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 @@ -282,10 +282,8 @@ public class MainFragment extends BaseBrowseFragment<MainPresenter> implements M private static Single<PreviewProgram> buildProgram(Context context, SmartListViewModel vm, String launcherName, long channelId) { return new AvatarDrawable.Builder() - .withContact(vm.getContact()) + .withViewModel(vm) .withPresence(false) - //.withPresence(vm.getContact().size() == 1) - //.withOnlineState(vm.getContact().size() == 1 && vm.getContact().get(0).isOnline()) .buildAsync(context) .map(avatar -> { File file = AndroidFileUtils.createImageFile(context); @@ -304,7 +302,7 @@ public class MainFragment extends BaseBrowseFragment<MainPresenter> implements M .setChannelId(channelId) .setType(TvContractCompat.PreviewPrograms.TYPE_CLIP) .setTitle(vm.getContactName()) - .setAuthor(vm.getContact().getRingUsername()) + .setAuthor(vm.getContact().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/tv/search/ContactSearchFragment.java b/ring-android/app/src/main/java/cx/ring/tv/search/ContactSearchFragment.java index 6faae15edb9bec675198e99bcccf98379b16c3f0..90a4440eb643e9a9e300288f7cdfaf49817c680d 100644 --- a/ring-android/app/src/main/java/cx/ring/tv/search/ContactSearchFragment.java +++ b/ring-android/app/src/main/java/cx/ring/tv/search/ContactSearchFragment.java @@ -139,8 +139,8 @@ public class ContactSearchFragment extends BaseSearchFragment<ContactSearchPrese public void startCall(String accountID, String number) { Intent intent = new Intent(CallActivity.ACTION_CALL, ConversationPath.toUri(accountID, number), getActivity(), TVCallActivity.class); intent.putExtra(ConversationFragment.KEY_ACCOUNT_ID, accountID); - intent.putExtra(ConversationFragment.KEY_CONTACT_RING_ID, number); - getActivity().startActivity(intent, null); + intent.putExtra(Intent.EXTRA_PHONE_NUMBER, number); + startActivity(intent); getActivity().finish(); } @@ -149,7 +149,7 @@ public class ContactSearchFragment extends BaseSearchFragment<ContactSearchPrese Intent intent = new Intent(getActivity(), TVContactActivity.class); //intent.putExtra(TVContactActivity.CONTACT_REQUEST_URI, model.getContact().getPrimaryUri()); intent.setDataAndType(ConversationPath.toUri(model.getAccountId(), model.getUri()), TVContactActivity.TYPE_CONTACT_REQUEST_OUTGOING); - getActivity().startActivity(intent); + startActivity(intent); getActivity().finish(); } } diff --git a/ring-android/app/src/main/java/cx/ring/tv/search/ContactSearchPresenter.java b/ring-android/app/src/main/java/cx/ring/tv/search/ContactSearchPresenter.java index b2b5a53378de49802d48c83bfc30ed829997d9c3..76d8723501a865aa437abebcba2a98ce3c003a88 100644 --- a/ring-android/app/src/main/java/cx/ring/tv/search/ContactSearchPresenter.java +++ b/ring-android/app/src/main/java/cx/ring/tv/search/ContactSearchPresenter.java @@ -82,8 +82,8 @@ public class ContactSearchPresenter extends RootPresenter<ContactSearchView> { return; } - Uri uri = new Uri(query); - if (uri.isRingId()) { + Uri uri = Uri.fromString(query); + if (uri.isHexId()) { mCallContact = currentAccount.getContactFromCache(uri); getView().displayContact(currentAccount.getAccountID(), mCallContact); } else { @@ -103,8 +103,8 @@ public class ContactSearchPresenter extends RootPresenter<ContactSearchView> { break; case 1: // invalid name - Uri uriName = new Uri(name); - if (uriName.isRingId()) { + Uri uriName = Uri.fromString(name); + if (uriName.isHexId()) { mCallContact = account.getContactFromCache(uriName); getView().displayContact(account.getAccountID(), mCallContact); } else { @@ -113,8 +113,8 @@ public class ContactSearchPresenter extends RootPresenter<ContactSearchView> { break; default: // on error - Uri uriAddress = new Uri(address); - if (uriAddress.isRingId()) { + Uri uriAddress = Uri.fromString(address); + if (uriAddress.isHexId()) { mCallContact = account.getContactFromCache(uriAddress); getView().displayContact(account.getAccountID(), mCallContact); } else { diff --git a/ring-android/app/src/main/java/cx/ring/utils/ActionHelper.java b/ring-android/app/src/main/java/cx/ring/utils/ActionHelper.java index 9475b92da17ef7097d4bb7e2383522d2f377b0ed..9b02f1102c214c036f5484392e71460cc3d2f551 100644 --- a/ring-android/app/src/main/java/cx/ring/utils/ActionHelper.java +++ b/ring-android/app/src/main/java/cx/ring/utils/ActionHelper.java @@ -25,17 +25,13 @@ import android.content.Context; import android.content.Intent; import android.provider.ContactsContract; -import androidx.appcompat.app.AlertDialog; - import com.google.android.material.dialog.MaterialAlertDialogBuilder; import java.util.ArrayList; import cx.ring.R; -import cx.ring.adapters.NumberAdapter; import cx.ring.model.CallContact; import cx.ring.model.Conversation; -import cx.ring.model.Phone; import cx.ring.model.Uri; public class ActionHelper { @@ -50,14 +46,14 @@ public class ActionHelper { } public static void launchClearAction(final Context context, - final CallContact callContact, + final Uri uri, final Conversation.ConversationActionCallback callback) { if (context == null) { Log.d(TAG, "launchClearAction: activity is null"); return; } - if (callContact == null) { + if (uri == null) { Log.d(TAG, "launchClearAction: conversation is null"); return; } @@ -67,7 +63,7 @@ public class ActionHelper { .setMessage(R.string.conversation_action_history_clear_message) .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> { if (callback != null) { - callback.clearConversation(callContact); + callback.clearConversation(uri); } }) .setNegativeButton(android.R.string.cancel, (dialog, whichButton) -> { @@ -77,14 +73,14 @@ public class ActionHelper { } public static void launchDeleteAction(final Context context, - final CallContact callContact, + final Uri uri, final Conversation.ConversationActionCallback callback) { if (context == null) { Log.d(TAG, "launchDeleteAction: activity is null"); return; } - if (callContact == null) { + if (uri == null) { Log.d(TAG, "launchDeleteAction: conversation is null"); return; } @@ -94,7 +90,7 @@ public class ActionHelper { .setMessage(R.string.conversation_action_remove_this_message) .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> { if (callback != null) { - callback.removeConversation(callContact); + callback.removeConversation(uri); } }) .setNegativeButton(android.R.string.cancel, (dialog, whichButton) -> { @@ -104,9 +100,9 @@ public class ActionHelper { } public static void launchCopyNumberToClipboardFromContact(final Context context, - final CallContact callContact, + final Uri callContact, final Conversation.ConversationActionCallback callback) { - if (callContact == null) { + /*if (callContact == null) { Log.d(TAG, "launchCopyNumberToClipboardFromContact: callContact is null"); return; } @@ -140,7 +136,7 @@ public class ActionHelper { .getDimension(R.dimen.alert_dialog_side_padding_list_view); alertDialog.getListView().setPadding(listViewSidePadding, 0, listViewSidePadding, 0); alertDialog.show(); - } + }*/ } public static Intent getAddNumberIntentForContact(CallContact contact) { @@ -150,8 +146,8 @@ public class ActionHelper { ArrayList<ContentValues> data = new ArrayList<>(); ContentValues values = new ContentValues(); - Uri number = contact.getPhones().get(0).getNumber(); - if (number.isRingId()) { + Uri number = contact.getUri(); + if (number.isHexId()) { values.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE); values.put(ContactsContract.CommonDataKinds.Im.DATA, number.getRawUriString()); values.put(ContactsContract.CommonDataKinds.Im.PROTOCOL, ContactsContract.CommonDataKinds.Im.PROTOCOL_CUSTOM); 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 190871c4d34eb8c688bf211b75009edfdcd13034..5eb454eece8b8970046dc881fe59fc601e7ba18c 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,13 +8,11 @@ import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import org.jetbrains.annotations.Contract; - -import java.util.Arrays; import java.util.List; import java.util.Objects; import cx.ring.fragments.ConversationFragment; +import cx.ring.model.Conversation; import cx.ring.model.Interaction; public class ConversationPath { @@ -24,6 +22,10 @@ public class ConversationPath { accountId = account; conversationId = contact; } + public ConversationPath(String account, net.jami.model.Uri conversationUri) { + accountId = account; + conversationId = conversationUri.getUri(); + } public ConversationPath(@NonNull Tuple<String, String> path) { accountId = path.first; @@ -57,8 +59,11 @@ public class ConversationPath { .appendEncodedPath(conversationUri.getUri()) .build(); } + public static Uri toUri(@NonNull Conversation conversation) { + return toUri(conversation.getAccountId(), conversation.getUri()); + } public static Uri toUri(@NonNull Interaction interaction) { - return toUri(interaction.getAccount(), new cx.ring.model.Uri(interaction.getConversation().getParticipant())); + return toUri(interaction.getAccount(), cx.ring.model.Uri.fromString(interaction.getConversation().getParticipant())); } public Bundle toBundle() { @@ -108,7 +113,6 @@ public class ConversationPath { return null; } - @Contract("null -> null") public static ConversationPath fromBundle(@Nullable Bundle bundle) { if (bundle != null) { String accountId = bundle.getString(ConversationFragment.KEY_ACCOUNT_ID); @@ -120,7 +124,6 @@ public class ConversationPath { return null; } - @Contract("null -> null") public static ConversationPath fromIntent(@Nullable Intent intent) { if (intent != null) { Uri uri = intent.getData(); @@ -132,11 +135,19 @@ public class ConversationPath { return null; } + @Override + public @NonNull String toString() { + return "ConversationPath{" + + "accountId='" + accountId + '\'' + + ", conversationId='" + conversationId + '\'' + + '}'; + } + @Override public boolean equals(@Nullable Object obj) { if (obj == this) return true; - if (obj == null || obj.getClass() != getClass()) + if (!(obj instanceof ConversationPath)) return false; ConversationPath o = (ConversationPath) obj; return Objects.equals(o.accountId, accountId) @@ -149,6 +160,6 @@ public class ConversationPath { } public cx.ring.model.Uri getConversationUri() { - return new cx.ring.model.Uri(conversationId); + return cx.ring.model.Uri.fromString(conversationId); } } diff --git a/ring-android/app/src/main/java/cx/ring/viewholders/SmartListViewHolder.java b/ring-android/app/src/main/java/cx/ring/viewholders/SmartListViewHolder.java index 5548921610a63f7aa061fa5aade04169d388b7a4..875325ca56be58e69e85c04711ed748da9880c32 100644 --- a/ring-android/app/src/main/java/cx/ring/viewholders/SmartListViewHolder.java +++ b/ring-android/app/src/main/java/cx/ring/viewholders/SmartListViewHolder.java @@ -22,14 +22,14 @@ package cx.ring.viewholders; import cx.ring.R; import cx.ring.databinding.ItemSmartlistBinding; import cx.ring.databinding.ItemSmartlistHeaderBinding; -import cx.ring.model.CallContact; import cx.ring.model.ContactEvent; import cx.ring.model.Interaction; import cx.ring.model.SipCall; import cx.ring.smartlist.SmartListViewModel; -import cx.ring.utils.BitmapUtils; +import cx.ring.utils.Log; import cx.ring.utils.ResourceMapper; import cx.ring.views.AvatarDrawable; +import io.reactivex.Observable; import io.reactivex.disposables.CompositeDisposable; import android.content.Context; @@ -40,35 +40,38 @@ import android.view.View; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; -import com.jakewharton.rxbinding3.view.RxView; - import java.util.concurrent.TimeUnit; public class SmartListViewHolder extends RecyclerView.ViewHolder { - public ItemSmartlistBinding binding; - public ItemSmartlistHeaderBinding headerBinding; + public final ItemSmartlistBinding binding; + public final ItemSmartlistHeaderBinding headerBinding; - private CompositeDisposable compositeDisposable = new CompositeDisposable(); + private final CompositeDisposable compositeDisposable = new CompositeDisposable(); public SmartListViewHolder(@NonNull ItemSmartlistBinding b) { super(b.getRoot()); binding = b; + headerBinding = null; } public SmartListViewHolder(@NonNull ItemSmartlistHeaderBinding b) { super(b.getRoot()); + binding = null; headerBinding = b; } public void bind(final SmartListListeners clickListener, final SmartListViewModel smartListViewModel) { + //Log.w("SmartListViewHolder", "bind " + smartListViewModel.getContact() + " " +smartListViewModel.showPresence()); compositeDisposable.clear(); if (binding != null) { - compositeDisposable.add(RxView.clicks(itemView) + compositeDisposable.add(Observable.create(e -> itemView.setOnClickListener(e::onNext)) .throttleFirst(1000, TimeUnit.MILLISECONDS) .subscribe(v -> clickListener.onItemClick(smartListViewModel))); - compositeDisposable.add(RxView.longClicks(itemView) - .subscribe(u -> clickListener.onItemLongClick(smartListViewModel))); + itemView.setOnLongClickListener(v -> { + clickListener.onItemLongClick(smartListViewModel); + return true; + }); binding.convParticipant.setText(smartListViewModel.getContactName()); @@ -95,11 +98,10 @@ public class SmartListViewHolder extends RecyclerView.ViewHolder { binding.convLastItem.setTypeface(null, Typeface.NORMAL); } - binding.photo.setImageDrawable( - new AvatarDrawable.Builder() - .withContact(smartListViewModel.getContact()) - .withCircleCrop(true) - .build(binding.photo.getContext())); + binding.photo.setImageDrawable(new AvatarDrawable.Builder() + .withViewModel(smartListViewModel) + .withCircleCrop(true) + .build(binding.photo.getContext())); } else if (headerBinding != null) { headerBinding.headerTitle.setText(smartListViewModel.getHeaderTitle() == SmartListViewModel.Title.Conversations ? R.string.navigation_item_conversation : R.string.search_results_public_directory); 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 f88328b3f4439ae2fb79ebc2f676d30fd28b0376..550e0ad3e4f569dab70785b8407adec7a98d236e 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 @@ -30,6 +30,7 @@ import android.graphics.PixelFormat; import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.RectF; +import android.graphics.Shader; import android.graphics.Typeface; import android.graphics.drawable.Drawable; @@ -40,20 +41,28 @@ import cx.ring.model.Account; import cx.ring.model.CallContact; import cx.ring.model.Conversation; import cx.ring.services.VCardServiceImpl; +import cx.ring.smartlist.SmartListViewModel; import cx.ring.utils.DeviceUtils; import cx.ring.utils.HashUtils; import io.reactivex.Single; import android.graphics.drawable.VectorDrawable; import android.text.TextUtils; +import android.util.Log; import android.util.TypedValue; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.List; public class AvatarDrawable extends Drawable { private static final int SIZE_AB = 36; + private static final float SIZE_BORDER = 2f; + private static final float DEFAULT_TEXT_SIZE_PERCENTAGE = 0.5f; private static final int PLACEHOLDER_ICON = R.drawable.baseline_account_crop_24; + private static final int PLACEHOLDER_ICON_GROUP = R.drawable.baseline_group_24; private static final int CHECKED_ICON = R.drawable.baseline_check_circle_24; private static final int PRESENCE_COLOR = R.color.green_A700; @@ -72,25 +81,27 @@ public class AvatarDrawable extends Drawable { private final PresenceIndicatorInfo presence = new PresenceIndicatorInfo(); private boolean update = true; + private final boolean isGroup; private int inSize = -1; private final int minSize; - private Bitmap workspace; - private Bitmap bitmap; + private final List<Bitmap> workspace; + private final List<Bitmap> bitmaps; private VectorDrawable placeholder; private VectorDrawable checkedIcon; - private final RectF backgroundBounds = new RectF(); + private final List<RectF> backgroundBounds; + private final List<Rect> inBounds; private String avatarText; private float textStartXPoint; private float textStartYPoint; private int color; - private final Paint clipPaint = new Paint(); + private final List<Paint> clipPaint; private final Paint textPaint = new Paint(); - private static final Paint drawPaint = new Paint(); private final Paint presenceFillPaint; private final Paint presenceStrokePaint; private final Paint checkedPaint; + private static final Paint drawPaint = new Paint(); static { drawPaint.setAntiAlias(true); drawPaint.setFilterBitmap(true); @@ -103,13 +114,14 @@ public class AvatarDrawable extends Drawable { public static class Builder { - private Bitmap photo = null; + private List<Bitmap> photos = null; private String name = null; private String id = null; private boolean circleCrop = false; private boolean isOnline = false; private boolean showPresence = true; private boolean isChecked = false; + private boolean isGroup = false; public Builder() {} @@ -118,7 +130,11 @@ public class AvatarDrawable extends Drawable { return this; } public Builder withPhoto(Bitmap photo) { - this.photo = 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) { @@ -146,6 +162,8 @@ public class AvatarDrawable extends Drawable { return withName(TextUtils.isEmpty(profileName) ? username : profileName); } public Builder withContact(CallContact contact){ + if (contact == null) + return this; return withPhoto((Bitmap)contact.getPhoto()) .withId(contact.getPrimaryNumber()) .withOnlineState(contact.isOnline()) @@ -153,19 +171,58 @@ public class AvatarDrawable extends Drawable { } public Builder withContacts(List<CallContact> contacts) { - if (contacts.size() == 1) { - return withContact(contacts.get(0)); + List<Bitmap> bitmaps = new ArrayList<>(contacts.size()); + int notTheUser = 0; + for (CallContact contact : contacts) { + if (contact.isUser()) + continue; + notTheUser++; + Bitmap bitmap = (Bitmap) contact.getPhoto(); + if (bitmap != null) { + bitmaps.add(bitmap); + } + if (bitmaps.size() == 4) + break;; + } + if (bitmaps.isEmpty()) { + if (notTheUser == 1) { + for (CallContact contact : contacts) { + if (!contact.isUser()) + return withContact(contact); + } + } else if (notTheUser == 0) { + // Fallback to the user avatar + for (CallContact contact : contacts) + return withContact(contact); + } + return this; } else { - return withName("G"); + return withPhotos(bitmaps); } } public Builder withConversation(Conversation conversation) { - return withContacts(conversation.getContacts()); + return conversation.isSwarm() + ? withContacts(conversation.getContacts()).setGroup() + : withContact(conversation.getContact()); + } + + private Builder setGroup() { + isGroup = true; + return this; + } + + 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))) + .withPresence(vm.showPresence()) + .withCheck(vm.isChecked()); } public AvatarDrawable build(Context context) { AvatarDrawable avatarDrawable = new AvatarDrawable( - context, photo, name, id, circleCrop); + context, photos, name, id, circleCrop, isGroup); avatarDrawable.setOnline(isOnline); avatarDrawable.setChecked(isChecked); avatarDrawable.showPresence = this.showPresence; @@ -195,7 +252,9 @@ public class AvatarDrawable extends Drawable { String username = contact.getUsername(); avatarText = convertNameToAvatarText( TextUtils.isEmpty(profileName) ? username : profileName); - bitmap = (Bitmap)contact.getPhoto(); + if (bitmaps != null) { + bitmaps.set(0, (Bitmap)contact.getPhoto()); + } isOnline = contact.isOnline(); update = true; } @@ -204,7 +263,7 @@ public class AvatarDrawable extends Drawable { update = true; } public void setPhoto(Bitmap photo) { - bitmap = photo; + bitmaps.set(0, photo); update = true; } public void setOnline(boolean online) { @@ -215,19 +274,50 @@ public class AvatarDrawable extends Drawable { isChecked = checked; } - private AvatarDrawable(Context context, Bitmap photo, String name, String id, boolean crop) { + private AvatarDrawable(Context context, List<Bitmap> photos, String name, String id, boolean crop, boolean group) { + //Log.w("AvatarDrawable", "AvatarDrawable " + (photos == null ? null : photos.size()) + " " + name); cropCircle = crop; + isGroup = group; minSize = (int) TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, SIZE_AB, context.getResources().getDisplayMetrics()); - if (photo != null) { + float borderSize = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, SIZE_BORDER, context.getResources().getDisplayMetrics()); + if (photos != null && photos.size() > 0) { avatarText = null; - bitmap = photo; + bitmaps = photos; + if (photos.size() == 1) { + backgroundBounds = Collections.singletonList(new RectF()); + inBounds = Collections.singletonList(null); + clipPaint = cropCircle ? Collections.singletonList(new Paint()) : 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); + for (Bitmap ignored : bitmaps) { + backgroundBounds.add(new RectF()); + inBounds.add(cropCircle ? null : new Rect()); + if (cropCircle) { + Paint p = new Paint(); + p.setStrokeWidth(borderSize); + p.setColor(Color.WHITE); + p.setStyle(Paint.Style.FILL); + clipPaint.add(p); + workspace.add(null); + } + } + } } else { - bitmap = null; + workspace = Arrays.asList((Bitmap)null); + bitmaps = null; + backgroundBounds = null; + inBounds = null; avatarText = convertNameToAvatarText(name); color = ContextCompat.getColor(context, getAvatarColor(id)); + clipPaint = cropCircle ? Collections.singletonList(new Paint()) : null; if (avatarText == null) { - placeholder = (VectorDrawable) context.getDrawable(PLACEHOLDER_ICON); + placeholder = (VectorDrawable) context.getDrawable(isGroup ? PLACEHOLDER_ICON_GROUP : PLACEHOLDER_ICON); } else { textPaint.setColor(Color.WHITE); textPaint.setTypeface(Typeface.SANS_SERIF); @@ -254,19 +344,42 @@ public class AvatarDrawable extends Drawable { checkedPaint.setStyle(Paint.Style.FILL_AND_STROKE); checkedPaint.setAntiAlias(true); - clipPaint.setAntiAlias(true); + if (clipPaint != null) + for (Paint p : clipPaint) + p.setAntiAlias(true); textPaint.setAntiAlias(true); textPaint.setColor(Color.WHITE); textPaint.setTypeface(Typeface.SANS_SERIF); } public AvatarDrawable(AvatarDrawable other) { + //Log.w("AvatarDrawable", "AvatarDrawable copy"); cropCircle = other.cropCircle; + isGroup = other.isGroup; minSize = other.minSize; - bitmap = other.bitmap; + 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++) { + backgroundBounds.add(new RectF()); + } + } + + inBounds = other.inBounds; color = other.color; placeholder = other.placeholder; avatarText = other.avatarText; + workspace = new ArrayList<>(other.workspace.size()); + 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++) { + clipPaint.add(new Paint(other.clipPaint.get(i))); + clipPaint.get(i).setShader(null); + } + } isOnline = other.isOnline; isChecked = other.isChecked; @@ -275,7 +388,6 @@ public class AvatarDrawable extends Drawable { presenceStrokePaint = other.presenceStrokePaint; checkedPaint = other.checkedPaint; - clipPaint.setAntiAlias(true); textPaint.setAntiAlias(true); textPaint.setColor(Color.WHITE); textPaint.setTypeface(Typeface.SANS_SERIF); @@ -283,18 +395,35 @@ public class AvatarDrawable extends Drawable { @Override public void draw(@NonNull Canvas finalCanvas) { - if (workspace == null) + if (workspace.get(0) == null) return; if (update) { - drawActual(new Canvas(workspace)); + for (int i = 0, s = workspace.size(); i < s; i++) + drawActual(i, new Canvas(workspace.get(i))); update = false; } if (cropCircle) { - int d = Math.min(getBounds().width(), getBounds().height()); - int r = d / 2; - finalCanvas.drawCircle(getBounds().centerX(), getBounds().centerY(), r, clipPaint); + float r = Math.min(getBounds().width(), getBounds().height()) / 2; + int cx = getBounds().centerX(); + float cy = getBounds().height() / 2; + int i = 0; + final float ratio = 1.333333f; + for (Paint paint : clipPaint) { + finalCanvas.drawCircle(cx, getBounds().bottom - 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); + paint.setShader(s); + paint.setStyle(Paint.Style.FILL); + } + i++; + r /= ratio; + cy /= ratio; + } } else { - finalCanvas.drawBitmap(workspace, null, getBounds(), drawPaint); + finalCanvas.drawBitmap(workspace.get(0), null, getBounds(), drawPaint); } if (showPresence && isOnline) { drawPresence(finalCanvas); @@ -304,9 +433,16 @@ public class AvatarDrawable extends Drawable { } } - private void drawActual(@NonNull Canvas canvas) { - if (bitmap != null) { - canvas.drawBitmap(bitmap, null, backgroundBounds, drawPaint); + private void drawActual(int i, @NonNull Canvas canvas) { + if (bitmaps != null) { + if (cropCircle) { + canvas.drawBitmap(bitmaps.get(i), inBounds.get(i), backgroundBounds.get(i), drawPaint); + } else { + if (backgroundBounds.size() == bitmaps.size()) + for (int n = 0, s = bitmaps.size(); n < s; n++) { + canvas.drawBitmap(bitmaps.get(n), inBounds.get(n), backgroundBounds.get(n), drawPaint); + } + } } else { canvas.drawColor(color); if (avatarText != null) { @@ -342,6 +478,54 @@ public class AvatarDrawable extends Drawable { } } + private static Rect getSubBounds(@NonNull Rect bounds, int total, int i) { + if (total == 1) + return bounds; + + if (total == 2 || (total == 3 && i == 0)) { + //Rect zone = getSubZone(bounds, 2, 1); + int w = bounds.width() / 2; + return (i == 0) + ? new Rect(bounds.left, bounds.top, bounds.left + w, bounds.bottom) + : new Rect(bounds.left + w, bounds.top, bounds.right, bounds.bottom); + } + if (total == 3 || (total == 4 && (i == 1 || i == 2))) { + int w = bounds.width() / 2; + int h = bounds.height() / 2; + return (i == 1) + ? new Rect(bounds.left + w, bounds.top, bounds.right, bounds.top + h) + : new Rect(bounds.left + w, bounds.top + h, bounds.right, bounds.bottom); + } + if (total == 4) { + int w = bounds.width() / 2; + int h = bounds.height() / 2; + return (i == 0) + ? new Rect(bounds.left, bounds.top, bounds.left + w, bounds.top + h) + : new Rect(bounds.left, bounds.top + h, bounds.left + w, bounds.bottom); + } + return null; + } + + 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; + int h; + if (outfit == (a < b)) { + w = iw; + h = (iw * bh) / bw; + } else { + w = (ih * bw) / bh; + h = ih; + } + int x = (iw - w) / 2; + int y = (ih - h) / 2; + if (ret instanceof Rect) + ((Rect)ret).set(x, y, x + w, y + h); + else if (ret instanceof RectF) + ((RectF)ret).set(x, y, x + w, y + h); + } + @Override protected void onBoundsChange(Rect bounds) { //if (showPresence) @@ -352,41 +536,48 @@ public class AvatarDrawable extends Drawable { int cy = (bounds.height()-d)/2; placeholder.setBounds(cx, cy, cx + d, cy + d); } - if (bitmap != null) { - int iw = cropCircle ? d : bounds.width(); - int ih = cropCircle ? d : bounds.height(); - int a = bitmap.getWidth() * ih; - int b = bitmap.getHeight() * iw; - int w; - int h; - if (a < b) { - w = iw; - h = (iw * bitmap.getHeight())/bitmap.getWidth(); - } else { - w = (ih * bitmap.getWidth())/bitmap.getHeight(); - h = ih; + int iw = cropCircle ? d : bounds.width(); + int ih = cropCircle ? d : bounds.height(); + for (int i=0, n=workspace.size(); i<n; i++) { + if (workspace.get(i) != null) { + workspace.get(i).recycle(); + workspace.set(i, null); } - int cx = (iw - w)/2; - int cy = (ih - h)/2; - backgroundBounds.set(cx, cy, cx + w, h + cy); - } else { - setAvatarTextValues(bounds); + } + if (iw <= 0 || ih <= 0) { + for (Paint p : clipPaint) + p.setShader(null); + return; } if (cropCircle) { - if (d > 0) { - workspace = Bitmap.createBitmap(d, d, Bitmap.Config.ARGB_8888); - clipPaint.setShader(new BitmapShader(workspace, BitmapShader.TileMode.CLAMP, - BitmapShader.TileMode.CLAMP)); + for (int i = 0, s = workspace.size(); i < s; i++) { + Bitmap workspacei = Bitmap.createBitmap(iw, ih, Bitmap.Config.ARGB_8888); + workspace.set(i, workspacei); + clipPaint.get(i).setShader(new BitmapShader(workspacei, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP)); + } + } else { + workspace.set(0, Bitmap.createBitmap(bounds.width(), bounds.height(), Bitmap.Config.ARGB_8888)); + } + + if (bitmaps != null) { + if (bitmaps.size() == 1 || cropCircle) { + 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 { - clipPaint.setShader(null); - if (workspace != null) { - workspace.recycle(); - workspace = null; + Rect realBounds = cropCircle ? new Rect(0, 0, iw, ih) : bounds; + for (int i=0; i<bitmaps.size(); i++) { + Bitmap bitmap = bitmaps.get(i); + Rect subBounds = getSubBounds(realBounds, bitmaps.size(), i); + if (subBounds != null) { + fit(bitmap.getWidth(), bitmap.getHeight(), subBounds.width(), subBounds.height(), false, inBounds.get(i)); + backgroundBounds.get(i).set(subBounds); + } } } } else { - workspace = Bitmap.createBitmap(bounds.width(), bounds.height(), - Bitmap.Config.ARGB_8888); + setAvatarTextValues(bounds); } update = true; } diff --git a/ring-android/app/src/main/java/cx/ring/views/ConversationViewHolder.java b/ring-android/app/src/main/java/cx/ring/views/ConversationViewHolder.java index 99e4c6c189f3e48bfdaff94db81795baec737e58..0795e32863caa5c659e4b51b7347cd856f6e14df 100644 --- a/ring-android/app/src/main/java/cx/ring/views/ConversationViewHolder.java +++ b/ring-android/app/src/main/java/cx/ring/views/ConversationViewHolder.java @@ -58,7 +58,6 @@ public class ConversationViewHolder extends RecyclerView.ViewHolder { public MediaPlayer player; public TextureView video; public Surface surface = null; - public String mCid; public UiUpdater updater; public LinearLayout mCallInfoLayout, mFileInfoLayout, mAudioInfoLayout; public ValueAnimator animator; @@ -76,7 +75,7 @@ public class ConversationViewHolder extends RecyclerView.ViewHolder { mHistDetailTxt = v.findViewById(R.id.call_details_txt); mIcon = v.findViewById(R.id.call_icon); mCallInfoLayout = v.findViewById(R.id.callInfoLayout); - } else { + } else if (type != ConversationAdapter.MessageType.INVALID) { switch (type) { // common layout elements case INCOMING_TEXT_MESSAGE: diff --git a/ring-android/app/src/main/java/cx/ring/views/SwitchButton.java b/ring-android/app/src/main/java/cx/ring/views/SwitchButton.java index cb585d3a61b53b6cf7246d1ff18adcbe223389c3..0d62b8d410bbf26ea308c2d5f3964e07fb5ab0d2 100644 --- a/ring-android/app/src/main/java/cx/ring/views/SwitchButton.java +++ b/ring-android/app/src/main/java/cx/ring/views/SwitchButton.java @@ -120,7 +120,6 @@ public class SwitchButton extends CompoundButton { setClickable(true); mStatus = status; - mBackColor = backColor; float margin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULT_THUMB_MARGIN_DP, getResources().getDisplayMetrics()); diff --git a/ring-android/app/src/main/res/layout-w720dp-land/activity_home.xml b/ring-android/app/src/main/res/layout-w720dp-land/activity_home.xml index a996a6fc4c09f49c9a8835bd63fe81a60cfcf43e..7d7194710309a567ac4ba86a52fa8e4fcb5d5cbd 100644 --- a/ring-android/app/src/main/res/layout-w720dp-land/activity_home.xml +++ b/ring-android/app/src/main/res/layout-w720dp-land/activity_home.xml @@ -1,8 +1,8 @@ <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - xmlns:tools="http://schemas.android.com/tools" android:orientation="vertical"> <com.google.android.material.appbar.AppBarLayout @@ -17,9 +17,9 @@ android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:layout_alignParentTop="true" - app:contentInsetStart="20dp" android:clipChildren="false" - android:clipToPadding="false"> + android:clipToPadding="false" + app:contentInsetStart="20dp"> <cx.ring.views.SwitchButton android:id="@+id/account_switch" @@ -60,7 +60,8 @@ android:layout_width="36dp" android:layout_height="36dp" android:layout_alignParentStart="true" - android:layout_centerVertical="true" /> + android:layout_centerVertical="true" + tools:src="@drawable/baseline_person_24" /> <TextView android:id="@+id/contact_title" @@ -69,8 +70,11 @@ android:layout_alignTop="@+id/contact_image" android:layout_marginStart="16dp" android:layout_toEndOf="@+id/contact_image" + android:ellipsize="middle" + android:singleLine="true" android:textColor="@color/textColorPrimary" - android:textSize="16sp" /> + android:textSize="16sp" + tools:text="@tools:sample/first_names" /> <TextView android:id="@+id/contact_subtitle" @@ -79,9 +83,10 @@ android:layout_below="@+id/contact_title" android:layout_alignStart="@id/contact_title" android:layout_toEndOf="@+id/contact_image" - android:ellipsize="end" - android:maxLines="1" - android:textSize="14sp" /> + android:ellipsize="middle" + android:singleLine="true" + android:textSize="14sp" + tools:text="@tools:sample/full_names" /> </RelativeLayout> @@ -95,16 +100,14 @@ android:id="@+id/main_frame" android:layout_width="match_parent" android:layout_height="match_parent" - android:layout_below="@id/main_toolbar" - /> + android:layout_below="@id/main_toolbar" /> <com.google.android.material.bottomnavigation.BottomNavigationView android:id="@+id/navigation_view" android:layout_width="320dp" android:layout_height="@dimen/navigation_bottom_height" - app:menu="@menu/navigation_bar" - app:labelVisibilityMode="labeled" android:layout_gravity="bottom" - /> + app:labelVisibilityMode="labeled" + app:menu="@menu/navigation_bar" /> </androidx.coordinatorlayout.widget.CoordinatorLayout> \ No newline at end of file diff --git a/ring-android/app/src/main/res/layout-w720dp-land/frag_smartlist.xml b/ring-android/app/src/main/res/layout-w720dp-land/frag_smartlist.xml index 2c7fbce48505aca5954946a0367b54a4caf72647..684907ed753360c29d7a4258a370b8bee286c908 100644 --- a/ring-android/app/src/main/res/layout-w720dp-land/frag_smartlist.xml +++ b/ring-android/app/src/main/res/layout-w720dp-land/frag_smartlist.xml @@ -9,10 +9,9 @@ <com.google.android.material.card.MaterialCardView android:id="@+id/qr_code" - android:layout_width="300dp" + android:layout_width="128dp" android:layout_height="wrap_content" style="@style/ButtonOutLined" - android:layout_toLeftOf="@+id/separator" android:layout_alignParentLeft="true" android:layout_marginLeft="20dp" android:layout_marginRight="20dp" @@ -33,6 +32,31 @@ </com.google.android.material.card.MaterialCardView> + <com.google.android.material.card.MaterialCardView + android:id="@+id/new_group" + style="@style/Widget.MaterialComponents.Button.OutlinedButton" + android:layout_toLeftOf="@+id/separator" + android:layout_marginTop="?actionBarSize" + android:layout_width="128dp" + android:layout_height="wrap_content" + android:layout_toEndOf="@id/qr_code" + android:visibility="gone" + app:cardCornerRadius="@dimen/button_corner_radius" + tools:visibility="visible"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:drawableLeft="@drawable/baseline_group_add_24" + android:drawablePadding="8dp" + android:drawableTint="@color/colorPrimary" + android:text="Add group" + android:textColor="@color/colorPrimary" + android:textSize="16sp" /> + + </com.google.android.material.card.MaterialCardView> + <androidx.coordinatorlayout.widget.CoordinatorLayout android:id="@+id/list_coordinator" android:layout_width="320dp" @@ -57,7 +81,7 @@ <ImageView android:layout_width="128dp" android:layout_height="128dp" - android:tint="@color/darker_gray" + app:tint="@color/darker_gray" android:src="@drawable/baseline_forum_24" /> <TextView diff --git a/ring-android/app/src/main/res/layout/frag_acc_summary.xml b/ring-android/app/src/main/res/layout/frag_acc_summary.xml index 214056396460deaac4989058dd1fd01c45a17d66..4d80b1e6600799380e0d34e24c953bc09761bc01 100644 --- a/ring-android/app/src/main/res/layout/frag_acc_summary.xml +++ b/ring-android/app/src/main/res/layout/frag_acc_summary.xml @@ -73,16 +73,18 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. app:endIconDrawable="@drawable/baseline_edit_twoton_24dp" app:endIconMode="custom" app:endIconTint="@color/colorPrimary" + app:hintEnabled="true" app:hintTextColor="@color/text_hint"> <com.google.android.material.textfield.TextInputEditText android:id="@+id/username" android:layout_width="match_parent" android:layout_height="wrap_content" - android:textColor="@color/text_color" - android:textStyle="bold" + android:inputType="textPersonName" + android:maxLines="1" android:singleLine="true" - android:maxLines="1"/> + android:textColor="@color/text_color" + android:textStyle="bold" /> </com.google.android.material.textfield.TextInputLayout> @@ -139,12 +141,27 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. android:layout_width="match_parent" android:layout_height="wrap_content"> - <cx.ring.views.TwoButtonEditText - android:id="@+id/registered_name" + <com.google.android.material.textfield.TextInputLayout android:layout_width="match_parent" android:layout_height="wrap_content" - android:enabled="false" - android:hint="@string/registered_username" /> + android:hint="@string/registered_username" + android:textColorHint="@color/text_hint" + app:hintEnabled="true" + app:hintTextColor="@color/text_hint" + android:enabled="false"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/registered_name" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:inputType="textPersonName" + android:maxLines="1" + android:singleLine="true" + android:textColor="@color/text_color" + android:textStyle="bold" + android:textIsSelectable="true"/> + + </com.google.android.material.textfield.TextInputLayout> <com.google.android.material.chip.Chip android:id="@+id/register_name" @@ -182,13 +199,29 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. </LinearLayout> - <cx.ring.views.TwoButtonEditText - android:id="@+id/identity" + <com.google.android.material.textfield.TextInputLayout android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginTop="12dp" - android:enabled="false" - android:hint="@string/ring_account_identity" /> + android:hint="@string/ring_account_identity" + android:textColorHint="@color/text_hint" + app:hintEnabled="true" + app:hintTextColor="@color/text_hint" + android:layout_marginTop="12dp"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/identity" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:inputType="none" + android:editable="false" + android:singleLine="true" + android:textColor="@color/text_color" + android:textStyle="bold" + android:textIsSelectable="true" + android:ellipsize="end" + /> + + </com.google.android.material.textfield.TextInputLayout> <cx.ring.views.TwoButtonEditText android:id="@+id/linked_devices" diff --git a/ring-android/app/src/main/res/layout/frag_contact_picker.xml b/ring-android/app/src/main/res/layout/frag_contact_picker.xml new file mode 100644 index 0000000000000000000000000000000000000000..09b65f0ea97a88564b2a8bef05610c89b7974726 --- /dev/null +++ b/ring-android/app/src/main/res/layout/frag_contact_picker.xml @@ -0,0 +1,69 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:context=".fragments.ContactPickerFragment" + android:animateLayoutChanges="true"> + + <com.google.android.material.appbar.AppBarLayout + style="@style/Widget.MaterialComponents.AppBarLayout.Surface" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:id="@+id/selected_contacts_tooolbar" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + android:animateLayoutChanges="true"> + + <com.google.android.material.appbar.MaterialToolbar + style="@style/Widget.MaterialComponents.Toolbar.Surface" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:title="Create a swarm" + app:navigationIcon="@drawable/baseline_group_add_24" /> + + <com.google.android.material.chip.ChipGroup + style="@style/Widget.MaterialComponents.ChipGroup" + android:id="@+id/selected_contacts" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="100dp" + android:animateLayoutChanges="true" + android:paddingHorizontal="8dp"> + + </com.google.android.material.chip.ChipGroup> + + </com.google.android.material.appbar.AppBarLayout> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/contact_list" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="200dp" + android:paddingTop="16dp" + android:clipToPadding="false" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/selected_contacts_tooolbar" + tools:listitem="@layout/item_contact" + app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"/> + + <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton + android:id="@+id/create_group_btn" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="24dp" + android:layout_marginStart="381dp" + android:layout_marginEnd="16dp" + android:text="Create swarm" + app:icon="@drawable/baseline_group_add_24" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + android:enabled="false" + android:animateLayoutChanges="true" /> + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/ring-android/app/src/main/res/layout/frag_smartlist.xml b/ring-android/app/src/main/res/layout/frag_smartlist.xml index b3719022e0e02466f5fdb313b7b5bfa691f5aa2a..146aebe82339e28ef11ab6964e62a40c7636ee3b 100644 --- a/ring-android/app/src/main/res/layout/frag_smartlist.xml +++ b/ring-android/app/src/main/res/layout/frag_smartlist.xml @@ -28,11 +28,10 @@ along with this program; if not, write to the Free Software <com.google.android.material.card.MaterialCardView android:id="@+id/qr_code" - android:layout_width="@dimen/wizard_button_width" + android:layout_width="@dimen/wizard_button_width_half" android:layout_height="wrap_content" style="@style/ButtonOutLined" - android:layout_centerHorizontal="true" - android:layout_marginTop="8dp" + android:layout_margin="8dp" app:cardCornerRadius="@dimen/button_corner_radius" android:visibility="gone" tools:visibility="visible" > @@ -49,6 +48,30 @@ along with this program; if not, write to the Free Software </com.google.android.material.card.MaterialCardView> + <com.google.android.material.card.MaterialCardView + android:id="@+id/new_group" + style="@style/Widget.MaterialComponents.Button.OutlinedButton" + android:layout_width="@dimen/wizard_button_width_half" + android:layout_height="wrap_content" + android:layout_toEndOf="@id/qr_code" + app:cardCornerRadius="@dimen/button_corner_radius" + android:layout_margin="8dp" + android:visibility="gone" + tools:visibility="visible"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:drawableLeft="@drawable/baseline_group_add_24" + android:drawablePadding="8dp" + android:drawableTint="@color/colorPrimary" + android:text="Add group" + android:textColor="@color/colorPrimary" + android:textSize="16sp" /> + + </com.google.android.material.card.MaterialCardView> + <androidx.coordinatorlayout.widget.CoordinatorLayout android:id="@+id/list_coordinator" android:layout_below="@id/qr_code" @@ -85,12 +108,14 @@ along with this program; if not, write to the Free Software android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" - android:indeterminate="true" /> + android:indeterminate="true" + tools:visibility="gone"/> <androidx.recyclerview.widget.RecyclerView android:id="@+id/confs_list" android:layout_width="match_parent" android:layout_height="match_parent" + app:layout_constraintTop_toBottomOf="@id/qr_code" android:layout_marginStart="0dp" android:clipToPadding="false" android:divider="@null" @@ -98,7 +123,7 @@ along with this program; if not, write to the Free Software android:paddingTop="8dp" android:paddingBottom="8dp" tools:listitem="@layout/item_smartlist" - tools:visibility="gone"/> + tools:visibility="visible"/> <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton style="@style/Widget.AppTheme.MainActionButton" diff --git a/ring-android/app/src/main/res/layout/item_two_button_edittext.xml b/ring-android/app/src/main/res/layout/item_two_button_edittext.xml index 98c3029afe6a6dca0effd7e2f013757703fc2c6d..263f29f7253bbb068a723b0a7adc9ae9c27a6235 100644 --- a/ring-android/app/src/main/res/layout/item_two_button_edittext.xml +++ b/ring-android/app/src/main/res/layout/item_two_button_edittext.xml @@ -1,6 +1,7 @@ <?xml version="1.0" encoding="utf-8"?> <merge xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content"> @@ -10,7 +11,8 @@ android:layout_height="wrap_content" android:layout_weight="1" android:textColorHint="@color/text_hint" - app:hintTextColor="@color/text_hint"> + app:hintTextColor="@color/text_hint" + tools:hint="Name"> <com.google.android.material.textfield.TextInputEditText android:id="@+id/edit_text" @@ -22,7 +24,8 @@ android:singleLine="true" android:textColor="@color/text_color" android:textIsSelectable="true" - android:textStyle="bold" /> + android:textStyle="bold" + tools:text="@tools:sample/full_names"/> </com.google.android.material.textfield.TextInputLayout> @@ -34,7 +37,7 @@ android:layout_marginEnd="2dp" android:background="?selectableItemBackgroundBorderless" android:padding="2dp" - android:visibility="gone" /> + tools:src="@drawable/baseline_add_24"/> <androidx.appcompat.widget.AppCompatImageButton android:id="@+id/btn_right" diff --git a/ring-android/app/src/main/res/values/dimens.xml b/ring-android/app/src/main/res/values/dimens.xml index 3067f2376fde189dafd2494684485e753c2f3464..36a7caad61b7362a0fe4a49179289ea88a70bc54 100644 --- a/ring-android/app/src/main/res/values/dimens.xml +++ b/ring-android/app/src/main/res/values/dimens.xml @@ -84,6 +84,8 @@ along with this program; if not, write to the Free Software <!-- Wizard --> <dimen name="wizard_button_width">340dp</dimen> + <dimen name="wizard_button_width_half">200dp</dimen> + <dimen name="wizard_button_corner_radius">@dimen/button_corner_radius</dimen> <dimen name="wizard_button_text_size">12sp</dimen> <dimen name="wizard_button_padding">@dimen/button_padding</dimen> diff --git a/ring-android/app/src/main/res/values/strings.xml b/ring-android/app/src/main/res/values/strings.xml index 5ed4a68eb165462891b891a1bc9b10c9d04e3e76..66a1fe34d28fe0560e6bef6fb68d83a4b3145f3f 100644 --- a/ring-android/app/src/main/res/values/strings.xml +++ b/ring-android/app/src/main/res/values/strings.xml @@ -239,6 +239,12 @@ along with this program; if not, write to the Free Software <string name="block_contact_completed">%1$s was blocked.</string> <string name="conversation_contact_is_typing">Contact is typing…</string> + <string name="conversation_preference_color">Change conversation color</string> + <string name="conversation_preference_emoji">Change conversation emoji</string> + <string name="conversation_type_contact">Jami contact</string> + <string name="conversation_type_private">Private swarm</string> + <string name="conversation_type_group">Group swarm</string> + <!-- Contacts --> <string name="add_call_contact_number_to_contacts">Add %1$s?</string> <string name="prompt_new_password">New password</string> diff --git a/ring-android/libringclient/src/main/java/cx/ring/call/CallPresenter.java b/ring-android/libringclient/src/main/java/cx/ring/call/CallPresenter.java index c687b98f6ce25247fe22ce2ef039e6a323ee3b79..ae402b826b7d8905c2006c7ddd611d3b782f4585 100644 --- a/ring-android/libringclient/src/main/java/cx/ring/call/CallPresenter.java +++ b/ring-android/libringclient/src/main/java/cx/ring/call/CallPresenter.java @@ -46,6 +46,7 @@ import io.reactivex.Maybe; import io.reactivex.Observable; import io.reactivex.Observer; import io.reactivex.Scheduler; +import io.reactivex.annotations.NonNull; import io.reactivex.disposables.Disposable; import io.reactivex.subjects.BehaviorSubject; import io.reactivex.subjects.Subject; @@ -150,8 +151,8 @@ public class CallPresenter extends RootPresenter<CallView> { }));*/ } - public void initOutGoing(String accountId, String contactRingId, boolean audioOnly) { - if (accountId == null || contactRingId == null) { + public void initOutGoing(String accountId, Uri conversationUri, String contactId, boolean audioOnly) { + if (accountId == null || contactId == null) { Log.e(TAG, "initOutGoing: null account or contact"); hangupCall(); return; @@ -162,7 +163,7 @@ public class CallPresenter extends RootPresenter<CallView> { //getView().blockScreenRotation(); Observable<Conference> callObservable = mCallService - .placeCall(accountId, StringUtils.toNumber(contactRingId), audioOnly) + .placeCall(accountId, conversationUri, Uri.fromString(StringUtils.toNumber(contactId)), audioOnly) //.map(mCallService::getConference) .flatMapObservable(call -> mCallService.getConfUpdates(call)) .share(); @@ -635,18 +636,18 @@ public class CallPresenter extends RootPresenter<CallView> { mCallService.playDtmf(s.toString()); } - public void addConferenceParticipant(String accountId, String contactId) { - mCompositeDisposable.add(mConversationFacade.startConversation(accountId, new Uri(contactId)) + public void addConferenceParticipant(String accountId, Uri contactUri) { + mCompositeDisposable.add(mConversationFacade.startConversation(accountId, contactUri) .map(Conversation::getCurrentCalls) .subscribe(confs -> { if (confs.isEmpty()) { final Observer<SipCall> pendingObserver = new Observer<SipCall>() { private SipCall call = null; @Override - public void onSubscribe(Disposable d) {} + public void onSubscribe(@NonNull Disposable d) {} @Override - public void onNext(SipCall sipCall) { + public void onNext(@NonNull SipCall sipCall) { if (call == null) { call = sipCall; mPendingCalls.add(sipCall); @@ -668,7 +669,7 @@ public class CallPresenter extends RootPresenter<CallView> { }; // Place new call, join to conference when answered - Maybe<SipCall> newCall = mCallService.placeCallObservable(accountId, contactId, mAudioOnly) + Maybe<SipCall> newCall = mCallService.placeCallObservable(accountId, null, contactUri, mAudioOnly) .doOnEach(pendingObserver) .filter(SipCall::isOnGoing) .firstElement() diff --git a/ring-android/libringclient/src/main/java/cx/ring/contactrequests/BlackListPresenter.java b/ring-android/libringclient/src/main/java/cx/ring/contactrequests/BlackListPresenter.java index 7c18f419baf59fe4c96aacdda20b5d217252ca98..8a92725cd445a2f63faab01593e0cffe28b6e098 100644 --- a/ring-android/libringclient/src/main/java/cx/ring/contactrequests/BlackListPresenter.java +++ b/ring-android/libringclient/src/main/java/cx/ring/contactrequests/BlackListPresenter.java @@ -87,7 +87,7 @@ public class BlackListPresenter extends RootPresenter<BlackListView> { } public void unblockClicked(CallContact contact) { - String contactId = contact.getPhones().get(0).getNumber().getRawRingId(); + String contactId = contact.getUri().getRawRingId(); mAccountService.addContact(mAccountID, contactId); } } diff --git a/ring-android/libringclient/src/main/java/cx/ring/contactrequests/ContactRequestsPresenter.java b/ring-android/libringclient/src/main/java/cx/ring/contactrequests/ContactRequestsPresenter.java index d777074778d311074492afeccc042c448e3e183a..c5678e6a31f8b151f9ef790c1ba059115d01ba45 100644 --- a/ring-android/libringclient/src/main/java/cx/ring/contactrequests/ContactRequestsPresenter.java +++ b/ring-android/libringclient/src/main/java/cx/ring/contactrequests/ContactRequestsPresenter.java @@ -26,16 +26,14 @@ import javax.inject.Inject; import javax.inject.Named; import cx.ring.facades.ConversationFacade; -import cx.ring.model.CallContact; -import cx.ring.model.Conversation; +import cx.ring.model.Account; +import cx.ring.model.Uri; import cx.ring.mvp.RootPresenter; import cx.ring.services.AccountService; -import cx.ring.services.ContactService; import cx.ring.smartlist.SmartListViewModel; import cx.ring.utils.Log; import io.reactivex.Observable; import io.reactivex.Scheduler; -import io.reactivex.disposables.CompositeDisposable; import io.reactivex.subjects.BehaviorSubject; public class ContactRequestsPresenter extends RootPresenter<ContactRequestsView> { @@ -45,7 +43,7 @@ public class ContactRequestsPresenter extends RootPresenter<ContactRequestsView> private final Scheduler mUiScheduler; private final AccountService mAccountService; private final ConversationFacade mConversationFacade; - private final BehaviorSubject<String> mAccount = BehaviorSubject.create(); + private final BehaviorSubject<Account> mAccount = BehaviorSubject.create(); @Inject ContactRequestsPresenter(ConversationFacade conversationFacade, AccountService accountService, @Named("UiScheduler") Scheduler scheduler) { @@ -57,7 +55,7 @@ public class ContactRequestsPresenter extends RootPresenter<ContactRequestsView> @Override public void bindView(ContactRequestsView view) { super.bindView(view); - mCompositeDisposable.add(mConversationFacade.getPendingList(mAccount.map(mAccountService::getAccount)) + mCompositeDisposable.add(mConversationFacade.getPendingList(mAccount) .switchMap(viewModels -> viewModels.isEmpty() ? SmartListViewModel.EMPTY_RESULTS : Observable.combineLatest(viewModels, obs -> { List<SmartListViewModel> vms = new ArrayList<>(obs.length); @@ -66,16 +64,25 @@ public class ContactRequestsPresenter extends RootPresenter<ContactRequestsView> return vms; })) .observeOn(mUiScheduler) - .subscribe(viewModels -> { - getView().updateView(viewModels); - }, e -> Log.d(TAG, "updateList subscribe onError", e))); + .subscribe(viewModels -> getView().updateView(viewModels), + e -> Log.d(TAG, "updateList subscribe onError", e))); + } + + @Override + public void unbindView() { + mAccount.onComplete(); + super.unbindView(); } public void updateAccount(String accountId) { - mAccount.onNext(accountId); + if (accountId == null) { + mAccountService.getCurrentAccountSubject().subscribe(mAccount); + } else { + mAccount.onNext(mAccountService.getAccount(accountId)); + } } - public void contactRequestClicked(String accountId, CallContact contactId) { - getView().goToConversation(accountId, contactId.getPrimaryNumber()); + public void contactRequestClicked(String accountId, Uri uri) { + getView().goToConversation(accountId, uri); } } diff --git a/ring-android/libringclient/src/main/java/cx/ring/contactrequests/ContactRequestsView.java b/ring-android/libringclient/src/main/java/cx/ring/contactrequests/ContactRequestsView.java index 7b6de07e721eca3b6de61b30c98a2462ce52cb10..4e9e0013e0932d4c61a267586df5b00e9d5ecd83 100644 --- a/ring-android/libringclient/src/main/java/cx/ring/contactrequests/ContactRequestsView.java +++ b/ring-android/libringclient/src/main/java/cx/ring/contactrequests/ContactRequestsView.java @@ -21,6 +21,7 @@ package cx.ring.contactrequests; import java.util.List; +import cx.ring.model.Uri; import cx.ring.smartlist.SmartListViewModel; public interface ContactRequestsView { @@ -28,5 +29,5 @@ public interface ContactRequestsView { void updateView(List<SmartListViewModel> list); void updateItem(SmartListViewModel item); - void goToConversation(String accountId, String contactId); + void goToConversation(String accountId, Uri contactId); } diff --git a/ring-android/libringclient/src/main/java/cx/ring/conversation/ConversationPresenter.java b/ring-android/libringclient/src/main/java/cx/ring/conversation/ConversationPresenter.java index 21e5dea4000eec64563d3f2b212e6bce4725376a..23c14a6acf45d27d19cd5fd93462a256a192bf94 100644 --- a/ring-android/libringclient/src/main/java/cx/ring/conversation/ConversationPresenter.java +++ b/ring-android/libringclient/src/main/java/cx/ring/conversation/ConversationPresenter.java @@ -67,8 +67,7 @@ public class ConversationPresenter extends RootPresenter<ConversationView> { private final PreferencesService mPreferencesService; private Conversation mConversation; - private Uri mContactUri; - private String mAccountId; + private Uri mConversationUri; private CompositeDisposable mConversationDisposable; private final CompositeDisposable mVisibilityDisposable = new CompositeDisposable(); @@ -94,61 +93,50 @@ public class ConversationPresenter extends RootPresenter<ConversationView> { mVCardService = vCardService; mDeviceRuntimeService = deviceRuntimeService; mPreferencesService = preferencesService; + mCompositeDisposable.add(mVisibilityDisposable); } - @Override + /*@Override public void bindView(ConversationView view) { super.bindView(view); mCompositeDisposable.add(mVisibilityDisposable); if (mConversationDisposable == null && mConversation != null) - initView(mConversation, view); - } + initView(mAccountService.getAccount(mConversation.getAccountId()), mConversation, view); + }*/ - public void init(Uri contactRingId, String accountId) { - Log.w(TAG, "init " + contactRingId + " " + accountId); - mContactUri = contactRingId; - mAccountId = accountId; + 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, contactRingId, getView()); - mCompositeDisposable.add(mConversationFacade.loadConversationHistory(account, contactRingId) + initContact(account, account.getByUri(mConversationUri), getView()); + mCompositeDisposable.add(mConversationFacade.loadConversationHistory(account, conversationUri) .observeOn(mUiScheduler) - .subscribe(this::setConversation, e -> getView().goToHome())); + .subscribe(c -> setConversation(account, c), e -> { + Log.e(TAG, "Error loading conversation", e); + getView().goToHome(); + })); } else { getView().goToHome(); return; } - mCompositeDisposable.add(Observable.combineLatest( - mHardwareService.getConnectivityState(), - mAccountService.getObservableAccount(account), - (isConnected, a) -> isConnected || a.isRegistered()) - .observeOn(mUiScheduler) - .subscribe(isOk -> { - ConversationView view = getView(); - if (view != null) { - if (!isOk) - view.displayNetworkErrorPanel(); - else if(!account.isEnabled()) { - view.displayAccountOfflineErrorPanel(); - } - else { - view.hideErrorPanel(); - } - } - })); - getView().setReadIndicatorStatus(showReadIndicator()); } - private void setConversation(final Conversation conversation) { - if (conversation == null || mConversation == conversation) + private void setConversation(Account account, final Conversation conversation) { + Log.w(TAG, "setConversation " + conversation.getAggregateHistory().size()); + if (mConversation == conversation) return; + if (mConversation != null) + mConversation.setVisible(false); mConversation = conversation; mConversationSubject.onNext(conversation); ConversationView view = getView(); if (view != null) - initView(conversation, view); + initView(account, conversation, view); } public void pause() { @@ -159,48 +147,38 @@ public class ConversationPresenter extends RootPresenter<ConversationView> { } public void resume(boolean isBubble) { - Log.w(TAG, "resume " + mConversation + " " + mAccountId + " " + mContactUri); + Log.w(TAG, "resume " + mConversationUri); mVisibilityDisposable.clear(); mVisibilityDisposable.add(mConversationSubject - .firstOrError() .subscribe(conversation -> { conversation.setVisible(true); updateOngoingCallView(conversation); - mConversationFacade.readMessages(mAccountService.getAccount(mAccountId), conversation, !isBubble); + mConversationFacade.readMessages(mAccountService.getAccount(conversation.getAccountId()), conversation, !isBubble); }, e -> Log.e(TAG, "Error loading conversation", e))); } - private CallContact initContact(final Account account, final Uri uri, - final ConversationView view) { - CallContact contact; + private void initContact(final Account account, final Conversation conversation, final ConversationView view) { if (account.isJami()) { - String rawId = uri.getRawRingId(); - contact = account.getContact(rawId); - if (contact == null) { - contact = account.getContactFromCache(uri); + Log.w(TAG, "initContact " + conversation.getUri()); + if (conversation.isSwarm() || account.isContact(conversation)) { + view.switchToConversationView(); + } else { + Uri uri = conversation.getUri(); TrustRequest req = account.getRequest(uri); if (req == null) { - view.switchToUnknownView(contact.getRingUsername()); + view.switchToUnknownView(uri.getRawUriString()); } else { view.switchToIncomingTrustRequestView(req.getDisplayname()); } - } else { - view.switchToConversationView(); - } - Log.w(TAG, "initContact " + contact.getUsername()); - if (contact.getUsername() == null) { - mAccountService.lookupAddress(mAccountId, "", rawId); } } else { - contact = mContactService.findContact(account, uri); view.switchToConversationView(); } - view.displayContact(contact); - return contact; + view.displayContact(conversation); } - private void initView(final Conversation c, final ConversationView view) { - Log.w(TAG, "initView"); + private void initView(Account account, final Conversation c, final ConversationView view) { + Log.w(TAG, "initView " + c.getUri()); if (mConversationDisposable == null) { mConversationDisposable = new CompositeDisposable(); mCompositeDisposable.add(mConversationDisposable); @@ -208,17 +186,52 @@ public class ConversationPresenter extends RootPresenter<ConversationView> { mConversationDisposable.clear(); view.hideNumberSpinner(); - Account account = mAccountService.getAccount(mAccountId); + if (account.isJami() && !c.isSwarm()) { + String accountId = account.getAccountID(); + mConversationDisposable.add(c.getContact().getConversationUri() + .observeOn(mUiScheduler) + .subscribe(uri -> init(uri, accountId))); + } + mConversationDisposable.add(Observable.combineLatest( + mHardwareService.getConnectivityState(), + mAccountService.getObservableAccount(account), + (isConnected, a) -> isConnected || a.isRegistered()) + .observeOn(mUiScheduler) + .subscribe(isOk -> { + ConversationView v = getView(); + if (v != null) { + if (!isOk) + v.displayNetworkErrorPanel(); + else if(!account.isEnabled()) { + v.displayAccountOfflineErrorPanel(); + } + else { + v.hideErrorPanel(); + } + } + })); mConversationDisposable.add(c.getSortedHistory() + .observeOn(mUiScheduler) .subscribe(view::refreshView, e -> Log.e(TAG, "Can't update element", e))); mConversationDisposable.add(c.getCleared() .observeOn(mUiScheduler) .subscribe(view::refreshView, e -> Log.e(TAG, "Can't update elements", e))); - mConversationDisposable.add(mContactService.getLoadedContact(c.getAccountId(), c.getContact()) + + mConversationDisposable.add(c.getContactUpdates() + .switchMap(contacts -> Observable.merge(mContactService.observeLoadedContact(c.getAccountId(), contacts, true))) + .observeOn(mUiScheduler) + .subscribe(contact -> { + ConversationView v = getView(); + if (v != null) + v.updateContact(contact); + })); + + mConversationDisposable.add(mContactService.getLoadedContact(c.getAccountId(), c.getContacts(), true) .observeOn(mUiScheduler) - .subscribe(contact -> initContact(account, mContactUri, view), e -> Log.e(TAG, "Can't get contact", e))); + .subscribe(contact -> initContact(account, c, view), e -> Log.e(TAG, "Can't get contact", e))); + mConversationDisposable.add(c.getUpdatedElements() .observeOn(mUiScheduler) .subscribe(elementTuple -> { @@ -234,6 +247,7 @@ public class ConversationPresenter extends RootPresenter<ConversationView> { break; } }, e -> Log.e(TAG, "Can't update element", e))); + if (showTypingIndicator()) { mConversationDisposable.add(c.getComposingStatus() .observeOn(mUiScheduler) @@ -251,17 +265,21 @@ public class ConversationPresenter extends RootPresenter<ConversationView> { Log.e(TAG, "getLocationUpdates subscribe"); mConversationDisposable.add(account - .getLocationUpdates(c.getContact().getPrimaryUri()) + .getLocationUpdates(c.getUri()) .observeOn(mUiScheduler) .subscribe(u -> { Log.e(TAG, "getLocationUpdates: update"); - getView().showMap(c.getAccountId(), c.getContact().getPrimaryUri().getUri(), false); + getView().showMap(c.getAccountId(), c.getUri().getUri(), false); })); } + public void loadMore() { + mConversationFacade.loadMore(mConversation); + } + public void openContact() { if (mConversation != null) - getView().goToContactActivity(mAccountId, mConversation.getContact().getPrimaryNumber()); + getView().goToContactActivity(mConversation.getAccountId(), mConversation.getUri()); } public void sendTextMessage(String message) { @@ -269,8 +287,8 @@ public class ConversationPresenter extends RootPresenter<ConversationView> { return; } Conference conference = mConversation.getCurrentCall(); - if (conference == null || !conference.isOnGoing()) { - mConversationFacade.sendTextMessage(mAccountId, mConversation, mContactUri, message).subscribe(); + if (mConversation.isSwarm() || conference == null || !conference.isOnGoing()) { + mConversationFacade.sendTextMessage(mConversation, mConversationUri, message).subscribe(); } else { mConversationFacade.sendTextMessage(mConversation, conference, message); } @@ -281,7 +299,9 @@ public class ConversationPresenter extends RootPresenter<ConversationView> { } public void sendFile(File file) { - mConversationFacade.sendFile(mAccountId, mContactUri, file).subscribe(); + if (mConversation == null) + return; + mConversationFacade.sendFile(mConversation, mConversationUri, file).subscribe(); } /** @@ -293,25 +313,37 @@ public class ConversationPresenter extends RootPresenter<ConversationView> { public void saveFile(Interaction interaction) { DataTransfer transfer = (DataTransfer) interaction; String fileAbsolutePath = getDeviceRuntimeService(). - getConversationPath(transfer.getPeerId(), transfer.getStoragePath()) + getConversationPath(mConversation.getUri().getRawRingId(), transfer.getStoragePath()) .getAbsolutePath(); getView().startSaveFile(transfer, fileAbsolutePath); } public void shareFile(Interaction interaction) { DataTransfer file = (DataTransfer) interaction; - File path = getDeviceRuntimeService().getConversationPath(file.getPeerId(), file.getStoragePath()); + File path = getDeviceRuntimeService().getConversationPath(mConversation.getUri().getRawRingId(), file.getStoragePath()); getView().shareFile(path); } public void openFile(Interaction interaction) { DataTransfer file = (DataTransfer) interaction; - File path = getDeviceRuntimeService().getConversationPath(file.getPeerId(), file.getStoragePath()); + File path = getDeviceRuntimeService().getConversationPath(mConversation.getUri().getRawRingId(), file.getStoragePath()); getView().openFile(path); } + public void acceptFile(DataTransfer transfer) { + if (!getDeviceRuntimeService().hasWriteExternalStoragePermission()) { + getView().askWriteExternalStoragePermission(); + return; + } + getView().acceptFile(mConversation.getAccountId(), mConversationUri, transfer); + } + + public void refuseFile(DataTransfer transfer) { + getView().refuseFile(mConversation.getAccountId(), mConversationUri, transfer); + } + public void deleteConversationItem(Interaction element) { - mConversationFacade.deleteConversationItem(element); + mConversationFacade.deleteConversationItem(mConversation, element); } public void cancelMessage(Interaction message) { @@ -319,16 +351,15 @@ public class ConversationPresenter extends RootPresenter<ConversationView> { } private void sendTrustRequest() { - final String accountId = mAccountId; - final Uri contactId = mContactUri; - CallContact contact = mContactService.findContact(mAccountService.getAccount(accountId), contactId); + //final Uri contactId = mConversationUri; + CallContact contact = mConversation.getContact();//mAccountService.getAccount(accountId).getContactFromCache(contactId); if (contact != null) { contact.setStatus(CallContact.Status.REQUEST_SENT); } - mVCardService.loadSmallVCard(accountId, VCardService.MAX_SIZE_REQUEST) + mVCardService.loadSmallVCard(mConversation.getAccountId(), VCardService.MAX_SIZE_REQUEST) .subscribeOn(Schedulers.computation()) - .subscribe(vCard -> mAccountService.sendTrustRequest(accountId, contactId.getRawRingId(), Blob.fromString(VCardUtils.vcardToString(vCard))), - e -> mAccountService.sendTrustRequest(accountId, contactId.getRawRingId(), null)); + .subscribe(vCard -> mAccountService.sendTrustRequest(mConversation, contact.getUri(), Blob.fromString(VCardUtils.vcardToString(vCard))), + e -> mAccountService.sendTrustRequest(mConversation, contact.getUri(), null)); } public void clickOnGoingPane() { @@ -358,7 +389,7 @@ public class ConversationPresenter extends RootPresenter<ConversationView> { && conf.getParticipants().get(0).getCallStatus() != SipCall.CallStatus.FAILURE) { view.goToCallActivity(conf.getId()); } else { - view.goToCallActivityWithResult(mAccountId, mContactUri.getRawUriString(), audioOnly); + view.goToCallActivityWithResult(mConversation.getAccountId(), mConversation.getUri(), mConversation.getContact().getUri(), audioOnly); } } })); @@ -374,22 +405,19 @@ public class ConversationPresenter extends RootPresenter<ConversationView> { } public void onBlockIncomingContactRequest() { - String accountId = mAccountId == null ? mAccountService.getCurrentAccount().getAccountID() : mAccountId; - mConversationFacade.discardRequest(accountId, mContactUri); - mAccountService.removeContact(accountId, mContactUri.getHost(), true); + mConversationFacade.discardRequest(mConversation.getAccountId(), mConversationUri); + mAccountService.removeContact(mConversation.getAccountId(), mConversationUri.getHost(), true); getView().goToHome(); } public void onRefuseIncomingContactRequest() { - String accountId = mAccountId == null ? mAccountService.getCurrentAccount().getAccountID() : mAccountId; - - mConversationFacade.discardRequest(accountId, mContactUri); + mConversationFacade.discardRequest(mConversation.getAccountId(), mConversationUri); getView().goToHome(); } public void onAcceptIncomingContactRequest() { - mConversationFacade.acceptRequest(mAccountId, mContactUri); + mConversationFacade.acceptRequest(mConversation.getAccountId(), mConversationUri); getView().switchToConversationView(); } @@ -422,7 +450,7 @@ public class ConversationPresenter extends RootPresenter<ConversationView> { } public void shareLocation() { - getView().startShareLocation(mAccountId, mContactUri.getUri()); + getView().startShareLocation(mConversation.getAccountId(), mConversationUri.getUri()); } public void showPluginListHandlers() { @@ -430,14 +458,14 @@ public class ConversationPresenter extends RootPresenter<ConversationView> { } public Tuple<String, String> getPath() { - return new Tuple<>(mAccountId, mContactUri.getUri()); + return new Tuple<>(mConversation.getAccountId(), mConversationUri.getUri()); } public void onComposingChanged(boolean hasMessage) { if (mConversation == null || !showTypingIndicator()) { return; } - mConversationFacade.setIsComposing(mAccountId, mContactUri, hasMessage); + mConversationFacade.setIsComposing(mConversation.getAccountId(), mConversationUri, hasMessage); } public boolean showTypingIndicator() { diff --git a/ring-android/libringclient/src/main/java/cx/ring/conversation/ConversationView.java b/ring-android/libringclient/src/main/java/cx/ring/conversation/ConversationView.java index ee08f4645185a8b01c8f65174b09f0f39ee5409c..47e8ccc50c833dcdb1b0f2b2ee29292b264f66ca 100644 --- a/ring-android/libringclient/src/main/java/cx/ring/conversation/ConversationView.java +++ b/ring-android/libringclient/src/main/java/cx/ring/conversation/ConversationView.java @@ -37,7 +37,9 @@ public interface ConversationView extends BaseView { void scrollToEnd(); - void displayContact(CallContact contact); + void updateContact(CallContact contact); + + void displayContact(Conversation conversation); void displayOnGoingCallPane(boolean display); @@ -55,9 +57,9 @@ public interface ConversationView extends BaseView { void goToCallActivity(String conferenceId); - void goToCallActivityWithResult(String accountId, String contactRingId, boolean audioOnly); + void goToCallActivityWithResult(String accountId, Uri conversationUri, Uri contactUri, boolean audioOnly); - void goToContactActivity(String accountId, String contactRingId); + void goToContactActivity(String accountId, Uri uri); void switchToUnknownView(String name); @@ -69,8 +71,9 @@ public interface ConversationView extends BaseView { void openFilePicker(); + void acceptFile(String accountId, Uri conversationUri, DataTransfer transfer); + void refuseFile(String accountId, Uri conversationUri, DataTransfer transfer); void shareFile(File path); - void openFile(File path); void addElement(Interaction e); @@ -98,4 +101,6 @@ public interface ConversationView extends BaseView { void setReadIndicatorStatus(boolean show); + void updateLastRead(String last); + } diff --git a/ring-android/libringclient/src/main/java/cx/ring/facades/ConversationFacade.java b/ring-android/libringclient/src/main/java/cx/ring/facades/ConversationFacade.java index 908a987118988a217c9a00601802a83898b79e15..30e416ac20ec840ff7adc4bbe934340cce1380aa 100644 --- a/ring-android/libringclient/src/main/java/cx/ring/facades/ConversationFacade.java +++ b/ring-android/libringclient/src/main/java/cx/ring/facades/ConversationFacade.java @@ -22,6 +22,7 @@ package cx.ring.facades; import java.io.File; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.NavigableMap; @@ -117,7 +118,7 @@ public class ConversationFacade { mDisposableBag.add(mAccountService.getIncomingRequests() .concatMapSingle(r -> getAccountSubject(r.getAccountId())) - .subscribe(a -> mNotificationService.showIncomingTrustRequestNotification(a), + .subscribe(mNotificationService::showIncomingTrustRequestNotification, e -> Log.e(TAG, "Error showing contact request"))); mDisposableBag.add(mAccountService @@ -129,6 +130,10 @@ public class ConversationFacade { })) .subscribe(this::parseNewMessage, e -> Log.e(TAG, "Error adding text message", e))); + mDisposableBag.add(mAccountService + .getIncomingSwarmMessages() + .subscribe(this::parseNewMessage, + e -> Log.e(TAG, "Error adding text message", e))); mDisposableBag.add(mAccountService.getLocationUpdates() .concatMapSingle(location -> getAccountSubject(location.getAccount()) @@ -158,15 +163,15 @@ public class ConversationFacade { mDisposableBag.add(mAccountService .getMessageStateChanges() .concatMapSingle(txt -> getAccountSubject(txt.getAccount()) - .map(a -> a.getByUri(txt.getConversation().getParticipant())) - .doOnSuccess(conv -> conv.updateTextMessage(txt))) + .map(a -> txt.getConversation() == null ? a.getSwarm(txt.getConversationId()) : a.getByUri(txt.getConversation().getParticipant())) + .doOnSuccess(conversation -> conversation.updateTextMessage(txt))) .subscribe(c -> { }, e -> Log.e(TAG, "Error updating text message", e))); mDisposableBag.add(mAccountService .getDataTransfers() .subscribe(this::handleDataTransferEvent, - e -> Log.e(TAG, "Error adding data transfer"))); + e -> Log.e(TAG, "Error adding data transfer", e))); } public Observable<Conversation> getUpdatedConversation() { @@ -193,56 +198,68 @@ public class ConversationFacade { .switchMap(Account::getConversationsSubject); } - public void readMessages(String accountId, Uri contact) { + public String readMessages(String accountId, Uri contact) { Account account = mAccountService.getAccount(accountId); - if (account != null) - readMessages(account, account.getByUri(contact), true); + return account != null ? + readMessages(account, account.getByUri(contact), true) : null; } - public void readMessages(Account account, Conversation conversation, boolean cancelNotification) { + public String readMessages(Account account, Conversation conversation, boolean cancelNotification) { if (conversation != null) { String lastMessage = readMessages(conversation); if (lastMessage != null) { account.refreshed(conversation); if (mPreferencesService.getSettings().isAllowReadIndicator()) { - mAccountService.setMessageDisplayed(account.getAccountID(), conversation.getContact().getPrimaryNumber(), lastMessage); + mAccountService.setMessageDisplayed(account.getAccountID(), conversation.getUri().getRawRingId(), lastMessage); } if (cancelNotification) { - mNotificationService.cancelTextNotification(conversation.getContact().getPrimaryUri()); + mNotificationService.cancelTextNotification(account.getAccountID(), conversation.getUri()); } } + return lastMessage; } + return null; } private String readMessages(Conversation conversation) { String lastRead = null; - NavigableMap<Long, Interaction> messages = conversation.getRawHistory(); - for (Interaction e : messages.descendingMap().values()) { - if (!(e.getType().equals(InteractionType.TEXT))) - continue; - if (e.isRead()) { - break; + if (conversation.isSwarm()) { + lastRead = conversation.readMessages(); + if (lastRead != null) + mHistoryService.setMessageRead(conversation.getAccountId(), conversation.getUri(), lastRead); + } else { + NavigableMap<Long, Interaction> messages = conversation.getRawHistory(); + for (Interaction e : messages.descendingMap().values()) { + if (!(e.getType().equals(InteractionType.TEXT))) + continue; + if (e.isRead()) { + break; + } + e.read(); + Long did = e.getDaemonId(); + if (lastRead == null && did != null && did != 0L) + lastRead = Long.toString(did, 16); + mHistoryService.updateInteraction(e, conversation.getAccountId()).subscribe(); } - e.read(); - Long did = e.getDaemonId(); - if (lastRead == null && did != null && did != 0L) - lastRead = Long.toString(did, 16); - mHistoryService.updateInteraction(e, conversation.getAccountId()).subscribe(); } return lastRead; } - public Single<TextMessage> sendTextMessage(String account, Conversation c, Uri to, String txt) { - return mCallService.sendAccountTextMessage(account, to.getRawUriString(), txt) + public Completable sendTextMessage(Conversation c, Uri to, String txt) { + if (c.isSwarm()) { + mAccountService.sendConversationMessage(c.getAccountId(), c.getUri(), txt); + return Completable.complete(); + } + return mCallService.sendAccountTextMessage(c.getAccountId(), to.getRawUriString(), txt) .map(id -> { - TextMessage message = new TextMessage(null, account, Long.toHexString(id), c, txt); + TextMessage message = new TextMessage(null, c.getAccountId(), Long.toHexString(id), c, txt); if (c.isVisible()) message.read(); - mHistoryService.insertInteraction(account, c, message).subscribe(); + mHistoryService.insertInteraction(c.getAccountId(), c, message).subscribe(); c.addTextMessage(message); - mAccountService.getAccount(account).conversationUpdated(c); + mAccountService.getAccount(c.getAccountId()).conversationUpdated(c); return message; - }); + }).ignoreElement(); } public void sendTextMessage(Conversation c, Conference conf, String txt) { @@ -253,60 +270,56 @@ public class ConversationFacade { c.addTextMessage(message); } - public void setIsComposing(String accountId, Uri contactUri, boolean isComposing) { - mCallService.setIsComposing(accountId, contactUri.getRawUriString(), isComposing); + public void setIsComposing(String accountId, Uri conversationUri, boolean isComposing) { + mCallService.setIsComposing(accountId, conversationUri.getRawRingId(), isComposing); } - public Completable sendFile(String account, Uri to, File file) { - return Completable.fromAction(() -> { - if (file == null) { - return; + public Completable sendFile(Conversation conversation, Uri to, File file) { + return Single.fromCallable(() -> { + if (file == null || !file.exists() || !file.canRead()) { + Log.w(TAG, "sendFile: file not found or not readable: " + file); + return null; } - // check file - if (!file.exists()) { - Log.w(TAG, "sendFile: file not found"); - return; + DataTransfer transfer = new DataTransfer(conversation, to.getRawRingId(), conversation.getAccountId(), file.getName(), true, file.length(), 0, 0L); + if (conversation.isSwarm()) { + transfer.setSwarmInfo(conversation.getUri().getRawRingId(), null, null); + } else { + mHistoryService.insertInteraction(conversation.getAccountId(), conversation, transfer).blockingAwait(); } - if (!file.canRead()) { - Log.w(TAG, "sendFile: file not readable"); - return; - } - String peerId = to.getUri(); - Conversation conversation = mAccountService.getAccount(account).getByUri(to); - DataTransfer transfer = new DataTransfer(conversation, account, file.getName(), true, file.length(), 0, 0L); - mHistoryService.insertInteraction(account, conversation, transfer).blockingAwait(); - File dest = mDeviceRuntimeService.getConversationPath(peerId, transfer.getStoragePath()); + File dest = mDeviceRuntimeService.getConversationPath(conversation.getUri().getRawRingId(), transfer.getStoragePath()); if (!FileUtils.moveFile(file, dest)) { Log.e(TAG, "sendFile: can't move file to " + dest); - return; + return null; } - // send file - mAccountService.sendFile(transfer, dest); - }).subscribeOn(Schedulers.io()); + transfer.destination = dest; + return transfer; + }) + .subscribeOn(Schedulers.io()) + .flatMapCompletable(mAccountService::sendFile); } - public void deleteConversationItem(Interaction element) { + public void deleteConversationItem(Conversation conversation, Interaction element) { if (element.getType() == InteractionType.DATA_TRANSFER) { DataTransfer transfer = (DataTransfer) element; if (transfer.getStatus() == InteractionStatus.TRANSFER_ONGOING) { - mAccountService.cancelDataTransfer(transfer.getDaemonId()); + mAccountService.cancelDataTransfer(conversation.getAccountId(), conversation.getUri().getRawRingId(), transfer.getDaemonId()); } else { - File file = mDeviceRuntimeService.getConversationPath(transfer.getPeerId(), transfer.getStoragePath()); + File file = mDeviceRuntimeService.getConversationPath(conversation.getUri().getRawRingId(), transfer.getStoragePath()); mDisposableBag.add(Completable.mergeArrayDelayError( mHistoryService.deleteInteraction(element.getId(), element.getAccount()), - Completable.fromAction(file::delete).subscribeOn(Schedulers.io())) - .andThen(startConversation(transfer.getAccount(), new Uri(transfer.getConversation().getParticipant()))) - .subscribe(c -> c.removeInteraction(transfer), + Completable.fromAction(file::delete) + .subscribeOn(Schedulers.io())) + .subscribe(() -> conversation.removeInteraction(transfer), 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(), new Uri(element.getConversation().getParticipant()))) + .andThen(startConversation(element.getAccount(), Uri.fromString(element.getConversation().getParticipant()))) .subscribe(c -> c.removeInteraction(element), e -> Log.e(TAG, "Can't delete message", e))); } @@ -315,7 +328,7 @@ public class ConversationFacade { public void cancelMessage(Interaction message) { mDisposableBag.add(Completable.mergeArrayDelayError( mCallService.cancelMessage(message.getAccount(), message.getId()).subscribeOn(Schedulers.io())) - .andThen(startConversation(message.getAccount(), new Uri(message.getConversation().getParticipant()))) + .andThen(startConversation(message.getAccount(), Uri.fromString(message.getConversation().getParticipant()))) .subscribe(c -> c.removeInteraction(message), e -> Log.e(TAG, "Can't cancel message sending", e))); } @@ -340,20 +353,26 @@ public class ConversationFacade { * Loads history for a specific conversation from cache or database * * @param account the user account - * @param contactUri the conversation participant + * @param conversationUri the conversation * @return a conversation single */ - public Single<Conversation> loadConversationHistory(final Account account, final Uri contactUri) { - Conversation conversation = account.getByUri(contactUri); + public Single<Conversation> loadConversationHistory(final Account account, final Uri conversationUri) { + Conversation conversation = account.getByUri(conversationUri); 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) { return Single.just(conversation); } Single<Conversation> ret = conversation.getLoaded(); if (ret == null) { - ret = getConversationHistory(account, conversation); + ret = getConversationHistory(conversation); conversation.setLoaded(ret); } return ret; @@ -361,23 +380,36 @@ public class ConversationFacade { } private Observable<SmartListViewModel> observeConversation(Account account, Conversation conversation, boolean hasPresence) { - return account.getConversationSubject() + return Observable.merge(account.getConversationSubject() + .filter(c -> c == conversation) + .startWith(conversation), + mContactService + .observeContact(conversation.getAccountId(), conversation.getContacts(), hasPresence)) + .map(e -> new SmartListViewModel(conversation, hasPresence)); + /*return account.getConversationSubject() .filter(c -> c == conversation) .startWith(conversation) .switchMap(c -> mContactService - .observeContact(c.getAccountId(), c.getContact(), hasPresence) - .map(contact -> new SmartListViewModel(c, hasPresence))); + .observeContact(c.getAccountId(), c.getContacts(), hasPresence) + .map(contact -> new SmartListViewModel(c, hasPresence)));*/ } public Observable<List<Observable<SmartListViewModel>>> getSmartList(Observable<Account> currentAccount, boolean hasPresence) { return currentAccount.switchMap(account -> account.getConversationsSubject() .switchMapSingle(conversations -> Observable.fromIterable(conversations) - .map(conv -> observeConversation(account, conv, hasPresence)) + .map(conversation -> observeConversation(account, conversation, hasPresence)) + .toList())); + } + 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 SmartListViewModel(conversation,false)) .toList())); } public Observable<List<Observable<SmartListViewModel>>> getPendingList(Observable<Account> currentAccount) { return currentAccount.switchMap(account -> account.getPendingSubject() .switchMapSingle(conversations -> Observable.fromIterable(conversations) - .map(conv -> observeConversation(account, conv, false)) + .map(conversation -> observeConversation(account, conversation, false)) .toList())); } @@ -387,16 +419,19 @@ public class ConversationFacade { public Observable<List<Observable<SmartListViewModel>>> getPendingList() { return getPendingList(mAccountService.getCurrentAccountSubject()); } + public Observable<List<SmartListViewModel>> getContactList() { + return getContactList(mAccountService.getCurrentAccountSubject()); + } private Single<List<Observable<SmartListViewModel>>> getSearchResults(Account account, String query) { - Uri uri = new Uri(query); + Uri uri = Uri.fromString(query); if (account.isSip()) { CallContact contact = account.getContactFromCache(uri); return mContactService.loadContactData(contact, account.getAccountID()) - .andThen(Single.just(Collections.singletonList(Observable.just(new SmartListViewModel(account.getAccountID(), contact, null))))); - } else if (uri.isRingId()) { + .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 SmartListViewModel(account.getAccountID(), contact, 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); @@ -431,7 +466,7 @@ public class ConversationFacade { newList.add(SmartListViewModel.TITLE_CONVERSATIONS); int nRes = 0; for (Conversation conversation : conversations) { - if (conversation.getContact().matches(lq)) { + if (conversation.matches(lq)) { newList.add(observeConversation(account, conversation, hasPresence)); nRes++; } @@ -470,17 +505,16 @@ public class ConversationFacade { /** * Loads a conversation's history from the database * - * @param account the user account * @param conversation a conversation object with a valid conversation ID * @return a conversation single */ - private Single<Conversation> getConversationHistory(final Account account, final Conversation conversation) { - Log.d(TAG, "getConversationHistory()"); + private Single<Conversation> getConversationHistory(final Conversation conversation) { + Log.d(TAG, "getConversationHistory() " + conversation.getAccountId() + " " + conversation.getUri()); - return mHistoryService.getConversationHistory(account.getAccountID(), conversation.getId()) + return mHistoryService.getConversationHistory(conversation.getAccountId(), conversation.getId()) .map(loadedConversation -> { - if (loadedConversation.isEmpty()) - return null; + /*if (loadedConversation.isEmpty()) + return conversation;*/ conversation.clearHistory(true); conversation.setHistory(loadedConversation); return conversation; @@ -513,7 +547,7 @@ public class ConversationFacade { } public void updateTextNotifications(String accountId, List<Conversation> conversations) { - Log.d(TAG, "updateTextNotifications()"); + Log.d(TAG, "updateTextNotifications() " + accountId + " " + conversations.size()); for (Conversation conversation : conversations) { mNotificationService.showTextNotification(accountId, conversation); @@ -522,9 +556,15 @@ public class ConversationFacade { private void parseNewMessage(final TextMessage txt) { if (txt.isRead()) { - mHistoryService.updateInteraction(txt, txt.getAccount()).subscribe(); + if (txt.getMessageId() == null) { + mHistoryService.updateInteraction(txt, txt.getAccount()).subscribe(); + } if (mPreferencesService.getSettings().isAllowReadIndicator()) { - mAccountService.setMessageDisplayed(txt.getAccount(), txt.getAuthor(), Long.toString(txt.getDaemonId(), 16)); + if (txt.getMessageId() != null) { + mAccountService.setMessageDisplayed(txt.getAccount(), txt.getConversationId(), txt.getMessageId()); + } else { + mAccountService.setMessageDisplayed(txt.getAccount(), txt.getAuthor(), Long.toString(txt.getDaemonId(), 16)); + } } } getAccountSubject(txt.getAccount()) @@ -551,10 +591,10 @@ public class ConversationFacade { Conversation conversation = mAccountService.getAccount(transfer.getAccount()).onDataTransferEvent(transfer); if (transfer.getStatus() == InteractionStatus.TRANSFER_CREATED && !transfer.isOutgoing()) { if (transfer.canAutoAccept(mPreferencesService.getMaxFileAutoAccept(transfer.getAccount()))) { - mAccountService.acceptFileTransfer(transfer); + mAccountService.acceptFileTransfer(conversation, transfer); } } - mNotificationService.handleDataTransferNotification(transfer, conversation.getContact(), conversation.isVisible()); + mNotificationService.handleDataTransferNotification(transfer, conversation, conversation.isVisible()); } private void onConfStateChange(Conference conference) { @@ -568,8 +608,19 @@ public class ConversationFacade { mHardwareService.updateAudioState(newState, incomingCall, !call.isAudioOnly()); Account account = mAccountService.getAccount(call.getAccount()); + if (account == null) + return; CallContact contact = call.getContact(); - Conversation conversation = (contact == null || account == null) ? null : account.getByUri(contact.getPrimaryUri()); + 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)); Conference conference = null; if (conversation != null) { conference = conversation.getConference(call.getDaemonIdString()); @@ -607,7 +658,8 @@ public class ConversationFacade { if (newState == SipCall.CallStatus.HUNGUP || call.getTimestampEnd() == 0) { call.setTimestampEnd(now); } - if (conversation != null && conference.removeParticipant(call)) { + if (conference != null && conference.removeParticipant(call) && !conversation.isSwarm()) { + Log.w(TAG, "Adding call history for conversation " + conversation.getUri()); mHistoryService.insertInteraction(account.getAccountID(), conversation, call).subscribe(); conversation.addCall(call); if (call.isIncoming() && call.isMissed()) { @@ -622,22 +674,22 @@ public class ConversationFacade { } } - public Single<SipCall> placeCall(String accountId, String number, boolean video) { - String rawId = new Uri(number).getRawRingId(); + public Single<SipCall> placeCall(String accountId, Uri contactUri, boolean video) { + //String rawId = contactUri.getRawRingId(); return getAccountSubject(accountId).flatMap(account -> { - CallContact contact = account.getContact(rawId); - if (contact == null) - mAccountService.addContact(accountId, rawId); - return mCallService.placeCall(rawId, number, video); + //CallContact contact = account.getContact(rawId); + //if (contact == null) + // mAccountService.addContact(accountId, rawId); + return mCallService.placeCall(accountId, null, contactUri, video); }); } - public void cancelFileTransfer(long id) { - mAccountService.cancelDataTransfer(id); + public void cancelFileTransfer(String accountId, Uri conversationId, long id) { + mAccountService.cancelDataTransfer(accountId, conversationId.isSwarm() ? conversationId.getRawRingId() : "", id); mNotificationService.removeTransferNotification(id); - DataTransfer transfer = mAccountService.getDataTransfer(id); + DataTransfer transfer = mAccountService.getAccount(accountId).getDataTransfer(id); if (transfer != null) - deleteConversationItem(transfer); + deleteConversationItem((Conversation) transfer.getConversation(), transfer); } public Completable removeConversation(String accountId, Uri contact) { @@ -649,4 +701,21 @@ public class ConversationFacade { mAccountService.removeContact(accountId, contact.getRawRingId(), false); }); } + + public Single<Conversation> createConversation(String accountId, Collection<CallContact> currentSelection) { + List<String> contactIds = new ArrayList<>(currentSelection.size()); + for (CallContact 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/cx/ring/model/Account.java b/ring-android/libringclient/src/main/java/cx/ring/model/Account.java index 5f6102b11a9a6e498dc611a5030c87b176974250..bcdbd9b4964ec1a7970347747edd8d0127080c1d 100644 --- a/ring-android/libringclient/src/main/java/cx/ring/model/Account.java +++ b/ring-android/libringclient/src/main/java/cx/ring/model/Account.java @@ -65,6 +65,8 @@ public class Account { private final Map<String, CallContact> mContacts = new HashMap<>(); private final Map<String, TrustRequest> mRequests = new HashMap<>(); private final Map<String, CallContact> mContactCache = new HashMap<>(); + private final Map<String, Conversation> swarmConversations = new HashMap<>(); + private final HashMap<Long, DataTransfer> mDataTransfers = new HashMap<>(); private final Map<String, Conversation> conversations = new HashMap<>(); private final Map<String, Conversation> pending = new HashMap<>(); @@ -83,16 +85,63 @@ public class Account { private final Subject<List<Conversation>> pendingSubject = BehaviorSubject.create(); private final Subject<Integer> unreadConversationsSubject = BehaviorSubject.create(); private final Subject<Integer> unreadPendingSubject = BehaviorSubject.create(); - private final Observable<Integer> unreadConversationsCount = unreadConversationsSubject.distinct(); - private final Observable<Integer> unreadPendingCount = unreadPendingSubject.distinct(); + private final Observable<Integer> unreadConversationsCount = unreadConversationsSubject.distinctUntilChanged(); + private final Observable<Integer> unreadPendingCount = unreadPendingSubject.distinctUntilChanged(); private final BehaviorSubject<Collection<CallContact>> contactListSubject = BehaviorSubject.create(); - private final BehaviorSubject<Collection<TrustRequest>> trustRequestsSubject = BehaviorSubject.create(); public boolean canSearch() { return !StringUtils.isEmpty(getDetail(ConfigKey.MANAGER_URI)); } + public boolean isContact(Conversation conversation) { + CallContact contact = conversation.getContact(); + return contact != null && getContact(contact.getUri().getRawRingId()) != null; + } + + public void conversationStarted(Conversation conversation) { + Log.w(TAG, "conversationStarted " + conversation.getAccountId() + " " + conversation.getUri() + " " + conversation.isSwarm() + " " + conversation.getContacts().size()); + synchronized (conversations) { + if (conversation.isSwarm() && conversation.getMode() == Conversation.Mode.OneToOne) { + CallContact contact = conversation.getContact(); + String key = contact.getUri().getUri(); + Conversation removed = cache.remove(key); + conversations.remove(key); + //Conversation contactConversation = getByUri(contact.getPrimaryUri()); + //Log.w(TAG, "conversationStarted " + conversation.getAccountId() + " contact " + key + " " + removed); + /*if (contactConversation != null) { + conversations.remove(contactConversation.getUri().getUri()); + }*/ + contact.setConversationUri(conversation.getUri()); + } + conversations.put(conversation.getUri().getUri(), conversation); + } + conversationChanged(); + } + public Conversation getSwarm(String conversationId) { + return swarmConversations.get(conversationId); + } + + public Conversation newSwarm(String conversationId, Conversation.Mode mode) { + Conversation c = swarmConversations.get(conversationId); + if (c == null) { + c = new Conversation(accountID, new Uri(Uri.SWARM_SCHEME, conversationId), mode); + swarmConversations.put(conversationId, c); + } + return c; + } + + public void removeSwarm(String conversationId) { + Conversation conversation = swarmConversations.remove(conversationId); + if (conversation != null) { + synchronized (conversations) { + conversations.remove(conversation.getUri().getUri()); + } + conversationChanged(); + } + } + + public static class ContactLocation { public double latitude; public double longitude; @@ -140,7 +189,7 @@ public class Account { conversationsSubject.onComplete(); pendingSubject.onComplete(); contactListSubject.onComplete(); - trustRequestsSubject.onComplete(); + //trustRequestsSubject.onComplete(); } public Observable<List<Conversation>> getConversationsSubject() { @@ -161,10 +210,6 @@ public class Account { return conversationSubject; } - public Observable<SmartListViewModel> getConversationViewModel() { - return conversationSubject.map(c -> new SmartListViewModel(accountID, c.getContact(), c.getLastEvent())); - } - public Observable<List<Conversation>> getPendingSubject() { return pendingSubject; } @@ -217,7 +262,7 @@ public class Account { } } - private void conversationChanged() { + public void conversationChanged() { conversationsChanged = true; if (historyLoaded) { conversationsSubject.onNext(new ArrayList<>(getSortedConversations())); @@ -243,13 +288,15 @@ public class Account { private void updateUnreadConversations() { int unread = 0; - for (Conversation model : sortedConversations) { - Interaction last = model.getLastEvent(); - if (last != null && !last.isRead()) - unread++; + synchronized (sortedConversations) { + for (Conversation model : sortedConversations) { + Interaction last = model.getLastEvent(); + if (last != null && !last.isRead()) + unread++; + } + // Log.w(TAG, "updateUnreadConversations " + unread); + unreadConversationsSubject.onNext(unread); } - // Log.w(TAG, "updateUnreadConversations " + unread); - unreadConversationsSubject.onNext(unread); } private void updateUnreadPending() { @@ -282,13 +329,17 @@ public class Account { } public void updated(Conversation conversation) { - String key = conversation.getContact().getPrimaryUri().getUri(); + String key = conversation.getUri().getUri(); if (conversation == conversations.get(key)) conversationUpdated(conversation); else if (conversation == pending.get(key)) pendingUpdated(conversation); else if (conversation == cache.get(key)) { + if (isJami() && !conversation.isSwarm() && conversation.getContacts().size() == 1 && !conversation.getContact().getConversationUri().blockingFirst().equals(conversation.getUri())) { + return; + } if (mContacts.containsKey(key) || !isJami()) { + Log.w(TAG, "updated " + conversation.getAccountId() + " contact " + key); conversations.put(key, conversation); conversationChanged(); } else { @@ -320,7 +371,7 @@ public class Account { } public Conversation onDataTransferEvent(DataTransfer transfer) { - Conversation conversation = getByUri(new Uri(transfer.getPeerId())); + Conversation conversation = (Conversation) transfer.getConversation(); InteractionStatus transferEventCode = transfer.getStatus(); if (transferEventCode == InteractionStatus.TRANSFER_CREATED) { conversation.addFileTransfer(transfer); @@ -342,22 +393,27 @@ public class Account { CallContact contact = mContactCache.get(key); if (contact == null) { if (isSip()) - contact = CallContact.buildSIP(new Uri(key)); + contact = CallContact.buildSIP(Uri.fromString(key)); else - contact = CallContact.build(key); + contact = CallContact.build(key, isMe(key)); mContactCache.put(key, contact); } return contact; } } + boolean isMe(String uri) { + //Log.w(TAG, "isMe " + uri + " " + getUsername()); + return getUsername().equals(uri); + } + public CallContact getContactFromCache(Uri uri) { return getContactFromCache(uri.getUri()); } public void dispose() { contactListSubject.onComplete(); - trustRequestsSubject.onComplete(); + //trustRequestsSubject.onComplete(); } public Map<String, String> getDevices() { @@ -610,7 +666,7 @@ public class Account { public void addContact(String id, boolean confirmed) { CallContact callContact = mContacts.get(id); if (callContact == null) { - callContact = getContactFromCache(new Uri(id)); + callContact = getContactFromCache(Uri.fromId(id)); mContacts.put(id, callContact); } callContact.setAddedDate(new Date()); @@ -622,7 +678,7 @@ public class Account { TrustRequest req = mRequests.get(id); if (req != null) { mRequests.remove(id); - trustRequestsSubject.onNext(mRequests.values()); + //trustRequestsSubject.onNext(mRequests.values()); } contactAdded(callContact); //contactSubject.onNext(new ContactEvent(callContact, true)); @@ -633,7 +689,7 @@ public class Account { CallContact callContact = mContacts.get(id); if (banned) { if (callContact == null) { - callContact = getContactFromCache(new Uri(id)); + callContact = getContactFromCache(Uri.fromId(id)); mContacts.put(id, callContact); } callContact.setStatus(CallContact.Status.BANNED); @@ -643,10 +699,10 @@ public class Account { TrustRequest req = mRequests.get(id); if (req != null) { mRequests.remove(id); - trustRequestsSubject.onNext(mRequests.values()); + //trustRequestsSubject.onNext(mRequests.values()); } if (callContact != null) { - contactRemoved(callContact.getPrimaryUri()); + contactRemoved(callContact.getUri()); //contactSubject.onNext(new ContactEvent(callContact, false)); } contactListSubject.onNext(mContacts.values()); @@ -656,7 +712,7 @@ public class Account { String contactId = contact.get(CONTACT_ID); CallContact callContact = mContacts.get(contactId); if (callContact == null) { - callContact = getContactFromCache(new Uri(contactId)); + callContact = getContactFromCache(Uri.fromId(contactId)); } String addedStr = contact.get(CONTACT_ADDED); if (!StringUtils.isEmpty(addedStr)) { @@ -696,21 +752,31 @@ public class Account { } public TrustRequest getRequest(Uri uri) { - return mRequests.get(uri.getRawRingId()); + return mRequests.get(uri.getUri()); } public void addRequest(TrustRequest request) { + //Log.w(TAG, "addRequest start"); synchronized (pending) { - mRequests.put(request.getContactId(), request); - //trustRequestSubject.onNext(new RequestEvent(request, true)); - trustRequestsSubject.onNext(mRequests.values()); + //boolean isSwarm = request.getConversationId() != null; + String key = request.getUri().getUri(); + //if (!isSwarm) { + mRequests.put(key, request); + //trustRequestSubject.onNext(new RequestEvent(request, true)); + //trustRequestsSubject.onNext(mRequests.values()); + //} - String key = new Uri(request.getContactId()).getUri(); Conversation conversation = pending.get(key); if (conversation == null) { - conversation = getByKey(key); + /*if (isSwarm) { + Log.w(TAG, "new public swarm request"); + }*/ + conversation = /*isSwarm ? newSwarm(key, Conversation.Mode.Public) : */getByKey(key); pending.put(key, conversation); - conversation.addRequestEvent(request); + if (!conversation.isSwarm()) { + CallContact contact = getContactFromCache(request.getUri()); + conversation.addRequestEvent(request, contact); + } pendingChanged(); } } @@ -720,17 +786,18 @@ public class Account { Log.w(TAG, "setRequests " + requests.size()); synchronized (pending) { for (TrustRequest request : requests) { - mRequests.put(request.getContactId(), request); - String key = new Uri(request.getContactId()).getUri(); + String key = request.getUri().getUri(); + mRequests.put(key, request); Conversation conversation = pending.get(key); if (conversation == null) { conversation = getByKey(key); pending.put(key, conversation); - conversation.addRequestEvent(request); + CallContact contact = getContactFromCache(request.getUri()); + conversation.addRequestEvent(request, contact); } //trustRequestSubject.onNext(new RequestEvent(request, true)); } - trustRequestsSubject.onNext(mRequests.values()); + //trustRequestsSubject.onNext(mRequests.values()); pendingChanged(); } } @@ -741,7 +808,7 @@ public class Account { TrustRequest request = mRequests.remove(contactUri); if (request != null) { //trustRequestSubject.onNext(new RequestEvent(request, true)); - trustRequestsSubject.onNext(mRequests.values()); + //trustRequestsSubject.onNext(mRequests.values()); } if (pending.remove(contactUri) != null) { pendingChanged(); @@ -751,11 +818,11 @@ public class Account { return false; } - public void registeredNameFound(int state, String address, String name) { - Uri uri = new Uri(address); - CallContact contact = getContactFromCache(uri); + public boolean registeredNameFound(int state, String address, String name) { + Uri uri = Uri.fromString(address); + String key = uri.getUri(); + CallContact contact = getContactFromCache(key); if (contact.setUsername(state == 0 ? name : null)) { - String key = uri.getUri(); synchronized (conversations) { Conversation conversation = conversations.get(key); if (conversation != null) @@ -765,18 +832,22 @@ public class Account { if (pending.containsKey(key)) pendingRefreshed(); } + return true; } + return false; } public Conversation getByUri(Uri uri) { - if (uri != null && !uri.isEmpty()) { - return getByKey(uri.getUri()); - } - return null; + //Log.w(TAG, "getByUri " + getAccountID() + " " + uri); + if (uri == null || uri.isEmpty()) + return null; + return uri.isSwarm() + ? getSwarm(uri.getRawRingId()) + : getByKey(uri.getUri()); } public Conversation getByUri(String uri) { - return getByUri(new Uri(uri)); + return getByUri(Uri.fromString(uri)); } private Conversation getByKey(String key) { @@ -786,6 +857,7 @@ public class Account { } CallContact contact = getContactFromCache(key); conversation = new Conversation(getAccountID(), contact); + //Log.w(TAG, "getByKey " + getAccountID() + " contact " + key); cache.put(key, conversation); return conversation; } @@ -793,16 +865,19 @@ public class Account { public void setHistoryLoaded(List<Conversation> conversations) { if (historyLoaded) return; - Log.w(TAG, "setHistoryLoaded() " + conversations.size()); - for (Conversation c : conversations) - updated(c); + //Log.w(TAG, "setHistoryLoaded " + getAccountID() + " " + conversations.size()); + for (Conversation c : conversations) { + CallContact contact = c.getContact(); + if (!c.isSwarm() && contact != null && contact.getConversationUri().blockingFirst().equals(c.getUri())) + updated(c); + } historyLoaded = true; conversationChanged(); pendingChanged(); } private List<Conversation> getSortedConversations() { - Log.w(TAG, "getSortedConversations() " + Thread.currentThread().getId()); + //Log.w(TAG, "getSortedConversations() " + Thread.currentThread().getId()); synchronized (sortedConversations) { if (conversationsChanged) { sortedConversations.clear(); @@ -836,10 +911,15 @@ public class Account { private void contactAdded(CallContact contact) { - Uri uri = contact.getPrimaryUri(); + Uri uri = contact.getUri(); String key = uri.getUri(); + //Log.w(TAG, "contactAdded " + getAccountID() + " " + key + " " + contact.getConversationUri()); + if (!contact.getConversationUri().blockingFirst().equals(contact.getUri())) { + // Don't add conversation if we have a swarm conversation + return; + } synchronized (conversations) { - if (conversations.get(key) != null) + if (conversations.containsKey(key)) return; synchronized (pending) { Conversation pendingConversation = pending.get(key); @@ -851,7 +931,7 @@ public class Account { conversations.put(key, pendingConversation); pendingChanged(); } - pendingConversation.addContactEvent(); + pendingConversation.addContactEvent(contact); } conversationChanged(); } @@ -880,6 +960,7 @@ public class Account { } public void presenceUpdate(String contactUri, boolean isOnline) { + Log.w(TAG, "presenceUpdate " + contactUri + " " + isOnline); CallContact contact = getContactFromCache(contactUri); if (contact.isOnline() == isOnline) return; @@ -896,10 +977,15 @@ public class Account { } } - public void composingStatusChanged(Uri contactUri, ComposingStatus status) { - Conversation conversation = getByUri(contactUri); - if (conversation != null) - conversation.composingStatusChanged(getContactFromCache(contactUri), status); + public void composingStatusChanged(String conversationId, Uri contactUri, ComposingStatus status) { + boolean isSwarm = !StringUtils.isEmpty(conversationId); + Conversation conversation = isSwarm ? getSwarm(conversationId) : getByUri(contactUri); + if (conversation != null) { + CallContact contact = isSwarm ? conversation.findContact(contactUri) : getContactFromCache(contactUri); + if (contact != null) { + conversation.composingStatusChanged(contact, status); + } + } } synchronized public long onLocationUpdate(AccountService.Location location) { @@ -980,6 +1066,8 @@ public class Account { public Observable<Observable<ContactLocation>> getLocationUpdates(Uri contactId) { CallContact contact = getContactFromCache(contactId); Log.w(TAG, "getLocationUpdates " + contactId + " " + contact); + if (contact == null || contact.isUser()) + return Observable.empty(); return mLocationSubject .flatMapMaybe(locations -> { Observable<ContactLocation> r = locations.get(contact); @@ -1029,6 +1117,14 @@ public class Account { mLoadedProfile = profile; } + public DataTransfer getDataTransfer(long id) { + return mDataTransfers.get(id); + } + + public void putDataTransfer(long transferId, DataTransfer transfer) { + mDataTransfers.put(transferId, transfer); + } + private static class ConversationComparator implements Comparator<Conversation> { @Override public int compare(Conversation a, Conversation b) { diff --git a/ring-android/libringclient/src/main/java/cx/ring/model/CallContact.java b/ring-android/libringclient/src/main/java/cx/ring/model/CallContact.java index c415044a02e9231d37456753f5412b3f2a207b38..03cfb81a8edbc2eb895b21d1d902e946b1e077ed 100644 --- a/ring-android/libringclient/src/main/java/cx/ring/model/CallContact.java +++ b/ring-android/libringclient/src/main/java/cx/ring/model/CallContact.java @@ -38,11 +38,11 @@ public class CallContact { public enum Status {BANNED, REQUEST_SENT, CONFIRMED, NO_REQUEST} - private long mId; - private String mKey; + private final Uri mUri; + private String mUsername = null; private long mPhotoId; - private final ArrayList<Phone> mPhones; + private final ArrayList<Phone> mPhones = new ArrayList<>(); private final boolean isUser; private boolean stared = false; private boolean isFromSystem = false; @@ -50,8 +50,13 @@ public class CallContact { private Date mAddedDate = null; private boolean mOnline = false; + private long mId; + private String mLookupKey; + private boolean usernameLoaded = false; public boolean detailsLoaded = false; + //private Uri mConversationUri = null; + private final BehaviorSubject<Uri> mConversationUri; // Profile private String mDisplayName; @@ -63,38 +68,44 @@ public class CallContact { private Observable<Boolean> mContactPresenceObservable; private Emitter<Boolean> mContactPresenceEmitter; - public CallContact(long cID) { - this(cID, null, null, UNKNOWN_ID); + public CallContact(Uri uri) { + this(uri, false); } - public CallContact(long cID, String k, String displayName, long photoID) { - this(cID, k, displayName, photoID, new ArrayList<>(), false); + public CallContact(Uri uri, boolean user) { + this(uri, null, user); } - private CallContact(long cID, String k, String displayName, long photoID, ArrayList<Phone> p, boolean user) { - mId = cID; - mKey = k; + private CallContact(Uri uri, String displayName, boolean user) { + mUri = uri; mDisplayName = displayName; - mPhones = p; - mPhotoId = photoID; isUser = user; - if (cID != UNKNOWN_ID && (displayName == null || !displayName.contains(PREFIX_RING))) { + mConversationUri = BehaviorSubject.createDefault(mUri); + /*if (cID != UNKNOWN_ID && (displayName == null || !displayName.contains(PREFIX_RING))) { mStatus = Status.CONFIRMED; - } + }*/ + } + + public void setConversationUri(Uri conversationUri) { + mConversationUri.onNext(conversationUri); + //mConversationUri = conversationUri; + } + + public Observable<Uri> getConversationUri() { + return mConversationUri; } public static CallContact buildSIP(Uri to) { - ArrayList<Phone> phones = new ArrayList<>(); - phones.add(new Phone(to, 0)); - CallContact contact = new CallContact(UNKNOWN_ID, null, null, 0, phones, false); + CallContact contact = new CallContact(to); contact.usernameLoaded = true; return contact; } - public static CallContact build(String to) { - ArrayList<Phone> phones = new ArrayList<>(); - phones.add(new Phone(to, 0)); - return new CallContact(UNKNOWN_ID, null, null, 0, phones, false); + public static CallContact build(String uri, boolean isUser) { + return new CallContact(Uri.fromString(uri), isUser); + } + public static CallContact build(String uri) { + return build(uri, false); } public Observable<CallContact> getUpdatesSubject() { @@ -137,8 +148,13 @@ public class CallContact { mContactUpdates.onNext(this); } - public void setContactInfos(String k, String displayName, long photo_id) { - mKey = k; + public void setSystemId(long id) { + mId = id; + } + + public void setSystemContactInfo(long id, String k, String displayName, long photo_id) { + mId = id; + mLookupKey = k; mDisplayName = displayName; this.mPhotoId = photo_id; if (mUsername == null && displayName.contains(PREFIX_RING)) { @@ -149,7 +165,7 @@ public class CallContact { public static String canonicalNumber(String number) { if (number == null || number.isEmpty()) return null; - return new Uri(number).getRawUriString(); + return Uri.fromString(number).getRawUriString(); } public ArrayList<String> getIds() { @@ -192,7 +208,7 @@ public class CallContact { } public boolean hasNumber(String number) { - return hasNumber(new Uri(number)); + return hasNumber(Uri.fromString(number)); } public boolean hasNumber(Uri number) { @@ -213,15 +229,15 @@ public class CallContact { this.mId = id; } - public String getKey() { + /*public String getKey() { return mKey; - } + }*/ public String getPrimaryNumber() { - return getPrimaryUri().getRawRingId(); + return getUri().getRawRingId(); } - public Uri getPrimaryUri() { - return getPhones().get(0).getNumber(); + public Uri getUri() { + return mUri; } public void setStared() { @@ -304,8 +320,8 @@ public class CallContact { public String getRingUsername() { if (!StringUtils.isEmpty(mUsername)) { return mUsername; - } else if (usernameLoaded && !mPhones.isEmpty()) { - return getPrimaryUri().getRawUriString(); + } else if (usernameLoaded) { + return getUri().getRawUriString(); } else { return ""; } diff --git a/ring-android/libringclient/src/main/java/cx/ring/model/Conference.java b/ring-android/libringclient/src/main/java/cx/ring/model/Conference.java index 8c8aa5b7c7f006c0419d63b7e83a45bfafa040de..2e6f3551042c47d25e34022d1dd412bb86bc6ea7 100644 --- a/ring-android/libringclient/src/main/java/cx/ring/model/Conference.java +++ b/ring-android/libringclient/src/main/java/cx/ring/model/Conference.java @@ -55,7 +55,7 @@ public class Conference { private final Set<CallContact> mParticipantRecordingSet = new HashSet<>(); private final Subject<Set<CallContact>> mParticipantRecording = BehaviorSubject.createDefault(Collections.emptySet()); - private String mId; + private final String mId; private SipCall.CallStatus mConfState; private final ArrayList<SipCall> mParticipants; private boolean mRecording; @@ -172,7 +172,7 @@ public class Conference { public SipCall findCallByContact(Uri uri) { for (SipCall call : mParticipants) { - if (call.getContact().getPrimaryUri().toString().equals(uri.toString())) + if (call.getContact().getUri().toString().equals(uri.toString())) return call; } return null; diff --git a/ring-android/libringclient/src/main/java/cx/ring/model/ContactEvent.java b/ring-android/libringclient/src/main/java/cx/ring/model/ContactEvent.java index 895951c306f4e91b8f11ea08ecac1d505a8cec4b..871ed26bf00917d5cc05b5374cb2a96fb9b98a4e 100644 --- a/ring-android/libringclient/src/main/java/cx/ring/model/ContactEvent.java +++ b/ring-android/libringclient/src/main/java/cx/ring/model/ContactEvent.java @@ -49,7 +49,7 @@ public class ContactEvent extends Interaction { public ContactEvent(CallContact contact) { mContact = contact; - mAuthor = contact.getPrimaryUri().getUri(); + mAuthor = contact.getUri().getUri(); mType = InteractionType.CONTACT.toString(); event = Event.ADDED; mStatus = InteractionStatus.SUCCESS.toString(); @@ -60,7 +60,7 @@ public class ContactEvent extends Interaction { public ContactEvent(CallContact contact, TrustRequest request) { this.request = request; mContact = contact; - mAuthor = contact.getPrimaryUri().getUri(); + mAuthor = contact.getUri().getUri(); mTimestamp = request.getTimestamp(); mType = InteractionType.CONTACT.toString(); event = Event.INCOMING_REQUEST; diff --git a/ring-android/libringclient/src/main/java/cx/ring/model/Conversation.java b/ring-android/libringclient/src/main/java/cx/ring/model/Conversation.java index 1763e563d228049ff310ab83b987fc784a3a40f4..1c37a4145f841363cfbdc4ca3d565563a2a33421 100644 --- a/ring-android/libringclient/src/main/java/cx/ring/model/Conversation.java +++ b/ring-android/libringclient/src/main/java/cx/ring/model/Conversation.java @@ -23,14 +23,18 @@ package cx.ring.model; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.NavigableMap; +import java.util.Set; import java.util.TreeMap; import cx.ring.model.Interaction.InteractionType; import cx.ring.utils.Log; +import cx.ring.utils.StringUtils; import cx.ring.utils.Tuple; import io.reactivex.Observable; import io.reactivex.Single; @@ -39,8 +43,6 @@ import io.reactivex.subjects.PublishSubject; import io.reactivex.subjects.Subject; public class Conversation extends ConversationHistory { - - private static final String TAG = Conversation.class.getSimpleName(); private final String mAccountId; @@ -58,9 +60,16 @@ public class Conversation extends ConversationHistory { private final Subject<List<Conference>> callsSubject = BehaviorSubject.create(); private final Subject<Account.ComposingStatus> composingStatusSubject = BehaviorSubject.createDefault(Account.ComposingStatus.Idle); private final Subject<Integer> color = BehaviorSubject.create(); + private final Subject<List<CallContact>> mContactSubject = BehaviorSubject.create(); private Single<Conversation> isLoaded = 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; + // runtime flag set to true if the user is currently viewing this conversation private boolean mVisible = false; @@ -70,8 +79,17 @@ public class Conversation extends ConversationHistory { public Conversation(String accountId, CallContact contact) { mAccountId = accountId; mContacts = Collections.singletonList(contact); - mKey = contact.getPrimaryUri(); - mParticipant = contact.getPrimaryUri().getUri(); + mKey = contact.getUri(); + mParticipant = contact.getUri().getUri(); + mContactSubject.onNext(mContacts); + mMode = null; + } + + public Conversation(String accountId, Uri uri, Mode mode) { + mAccountId = accountId; + mKey = uri; + mContacts = new ArrayList<>(3); + mMode = mode; } public Conference getConference(String id) { @@ -90,12 +108,32 @@ public class Conversation extends ConversationHistory { return mKey; } - public CharSequence getDisplayName() { + public Mode getMode() { return mMode; } + + public boolean isSwarm() { + return Uri.SWARM_SCHEME.equals(getUri().getScheme()); + } + + public boolean matches(String query) { + for (CallContact contact : getContacts()) { + if (contact.matches(query)) + return true; + } + return false; + } + + public String getDisplayName() { return mContacts.get(0).getDisplayName(); } public void addContact(CallContact contact) { mContacts.add(contact); + mContactSubject.onNext(mContacts); + } + + public void removeContact(CallContact contact) { + mContacts.remove(contact); + mContactSubject.onNext(mContacts); } public String getTitle() { @@ -105,33 +143,76 @@ public class Conversation extends ConversationHistory { return mContacts.get(0).getDisplayName(); } StringBuilder ret = new StringBuilder(); - Iterator<CallContact> it = mContacts.iterator(); - while (it.hasNext()) { - CallContact c = it.next(); - ret.append(c.getDisplayName()); - if (it.hasNext()) - ret.append(", "); + ArrayList<String> names = new ArrayList<>(mContacts.size()); + int target = mContacts.size(); + for (CallContact c : mContacts) { + if (c.isUser()) { + target--; + continue; + } + String displayName = c.getDisplayName(); + if (!StringUtils.isEmpty(displayName)) { + names.add(displayName); + if (names.size() == 3) + break; + } } - return ret.toString(); + ret.append(StringUtils.join(", ", names)); + if (!names.isEmpty() && names.size() < target) { + ret.append(" + ").append(mContacts.size() - names.size()); + } + String result = ret.toString(); + return result.isEmpty() ? mKey.getRawUriString() : result; } public String getUriTitle() { if (mContacts.isEmpty()) { return null; } else if (mContacts.size() == 1) { - return mContacts.get(0).getDisplayName(); + return mContacts.get(0).getRingUsername(); } StringBuilder ret = new StringBuilder(); Iterator<CallContact> it = mContacts.iterator(); while (it.hasNext()) { CallContact c = it.next(); - ret.append(c.getDisplayName()); + if (c.isUser()) + continue; + ret.append(c.getRingUsername()); if (it.hasNext()) ret.append(", "); } return ret.toString(); } + public Observable<List<CallContact>> getContactUpdates() { + return mContactSubject; + } + + public String readMessages() { + Interaction interaction = null; + for (String branch : mBranches) { + Interaction i = mMessages.get(branch); + if (i != null && !i.isRead()) { + i.read(); + interaction = i; + lastRead = i.getMessageId(); + } + } + return interaction == null ? null : interaction.getMessageId(); + } + + public Interaction getMessage(String messageId) { + return mMessages.get(messageId); + } + + public void setLastMessageRead(String lastMessageRead) { + lastRead = lastMessageRead; + } + + public String getLastRead() { + return lastRead; + } + public enum ElementStatus { UPDATE, REMOVE, ADD } @@ -199,13 +280,22 @@ public class Conversation extends ConversationHistory { return mContacts; } - @Deprecated public CallContact getContact() { - return mContacts.get(0); + if (mContacts.size() == 1) + return mContacts.get(0); + if (isSwarm()) { + if (mContacts.size() > 2) + throw new IllegalStateException("getContact() called for group conversation of size " + mContacts.size()); + } + for (CallContact contact : mContacts) { + if (!contact.isUser()) + return contact; + } + return null; } public void addCall(SipCall call) { - if (getCallHistory().contains(call)) { + if (!isSwarm() && getCallHistory().contains(call)) { return; } mDirty = true; @@ -213,6 +303,25 @@ public class Conversation extends ConversationHistory { updatedElementSubject.onNext(new Tuple<>(call, ElementStatus.ADD)); } + private void setInteractionProperties(Interaction interaction) { + interaction.setAccount(getAccountId()); + if (interaction.getContact() == null) { + if (mContacts.size() == 1) + interaction.setContact(mContacts.get(0)); + else + interaction.setContact(findContact(Uri.fromString(interaction.getAuthor()))); + } + } + + public CallContact findContact(Uri uri) { + for (CallContact contact : mContacts) { + if (contact.getUri().equals(uri)) { + return contact; + } + } + return null; + } + public void addTextMessage(TextMessage txt) { if (mVisible) { txt.read(); @@ -220,24 +329,24 @@ public class Conversation extends ConversationHistory { if (txt.getConversation() == null) { Log.e(TAG, "Error in conversation class... No conversation is attached to this interaction"); } - if (txt.getContact() == null) { - txt.setContact(getContact()); - } + setInteractionProperties(txt); mHistory.put(txt.getTimestamp(), txt); mDirty = true; mAggregateHistory.add(txt); updatedElementSubject.onNext(new Tuple<>(txt, ElementStatus.ADD)); } - public void addRequestEvent(TrustRequest request) { - ContactEvent event = new ContactEvent(getContact(), request); + public void addRequestEvent(TrustRequest request, CallContact contact) { + if (isSwarm()) + return; + ContactEvent event = new ContactEvent(contact, request); mDirty = true; mAggregateHistory.add(event); updatedElementSubject.onNext(new Tuple<>(event, ElementStatus.ADD)); } - public void addContactEvent() { - ContactEvent event = new ContactEvent(getContact()); + public void addContactEvent(CallContact contact) { + ContactEvent event = new ContactEvent(contact); mDirty = true; mAggregateHistory.add(event); updatedElementSubject.onNext(new Tuple<>(event, ElementStatus.ADD)); @@ -259,11 +368,9 @@ public class Conversation extends ConversationHistory { } public void updateTextMessage(TextMessage text) { - text.setContact(getContact()); - long time = text.getTimestamp(); - NavigableMap<Long, Interaction> msgs = mHistory.subMap(time, true, time, true); - for (Interaction txt : msgs.values()) { - if (txt.getId() == text.getId()) { + if (isSwarm()) { + TextMessage txt = (TextMessage) mMessages.get(text.getMessageId()); + if (txt != null) { txt.setStatus(text.getStatus()); updatedElementSubject.onNext(new Tuple<>(txt, ElementStatus.UPDATE)); if (text.getStatus() == Interaction.InteractionStatus.DISPLAYED) { @@ -272,10 +379,28 @@ public class Conversation extends ConversationHistory { lastDisplayedSubject.onNext(text); } } - return; + } else { + Log.e(TAG, "Can't find swarm message to update: " + text.getMessageId()); } + } else { + setInteractionProperties(text); + long time = text.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 Tuple<>(txt, ElementStatus.UPDATE)); + if (text.getStatus() == Interaction.InteractionStatus.DISPLAYED) { + if (lastDisplayed == null || lastDisplayed.getTimestamp() < text.getTimestamp()) { + lastDisplayed = text; + lastDisplayedSubject.onNext(text); + } + } + return; + } + } + Log.e(TAG, "Can't find message to update: " + text.getId()); } - Log.e(TAG, "Can't find message to update: " + text.getId()); } public ArrayList<Interaction> getAggregateHistory() { @@ -289,6 +414,7 @@ public class Conversation extends ConversationHistory { public void sortHistory() { if (mDirty) { + Log.w(TAG, "sortHistory()"); synchronized (mAggregateHistory) { Collections.sort(mAggregateHistory, (c1, c2) -> Long.compare(c1.getTimestamp(), c2.getTimestamp())); } @@ -330,13 +456,23 @@ public class Conversation extends ConversationHistory { public TreeMap<Long, TextMessage> getUnreadTextMessages() { TreeMap<Long, TextMessage> texts = new TreeMap<>(); - for (Map.Entry<Long, Interaction> entry : mHistory.descendingMap().entrySet()) { - Interaction value = entry.getValue(); - if (value.getType() == InteractionType.TEXT) { - TextMessage message = (TextMessage) value; - if (message.isRead()) + if (isSwarm()) { + for(int j = mAggregateHistory.size() - 1; j >= 0; j--) { + Interaction i = mAggregateHistory.get(j); + if (i.isRead()) break; - texts.put(entry.getKey(), message); + if (i instanceof TextMessage) + texts.put(i.getTimestamp(), (TextMessage) i); + } + } else { + for (Map.Entry<Long, Interaction> entry : mHistory.descendingMap().entrySet()) { + Interaction value = entry.getValue(); + if (value.getType() == InteractionType.TEXT) { + TextMessage message = (TextMessage) value; + if (message.isRead()) + break; + texts.put(entry.getKey(), message); + } } } return texts; @@ -380,8 +516,8 @@ public class Conversation extends ConversationHistory { mAggregateHistory.clear(); mHistory.clear(); mDirty = false; - if(!delete) - mAggregateHistory.add(new ContactEvent(getContact())); + if (!delete && mContacts.size() == 1) + mAggregateHistory.add(new ContactEvent(mContacts.get(0))); clearedSubject.onNext(mAggregateHistory); } @@ -404,8 +540,7 @@ public class Conversation extends ConversationHistory { Interaction last = null; for (Interaction i : loadedConversation) { Interaction interaction = getTypedInteraction(i); - interaction.setAccount(mAccountId); - interaction.setContact(getContact()); + setInteractionProperties(interaction); mAggregateHistory.add(interaction); mHistory.put(interaction.getTimestamp(), interaction); if (!i.isIncoming() && i.getStatus() == Interaction.InteractionStatus.DISPLAYED) @@ -419,8 +554,7 @@ public class Conversation extends ConversationHistory { } public void addElement(Interaction interaction) { - interaction.setAccount(mAccountId); - interaction.setContact(getContact()); + setInteractionProperties(interaction); if (interaction.getType() == InteractionType.TEXT) { TextMessage msg = new TextMessage(interaction); addTextMessage(msg); @@ -436,6 +570,59 @@ 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); + } + 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; + } + if (mAggregateHistory.isEmpty() || interaction.getParentIds().contains(mAggregateHistory.get(mAggregateHistory.size()-1).getMessageId())) { + // New leaf + mAggregateHistory.add(interaction); + 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())) { + mAggregateHistory.add(i, interaction); + updatedElementSubject.onNext(new Tuple<>(interaction, ElementStatus.ADD)); + break; + } + } + } + return newMessage; + } + + public Collection<String> getSwarmRoot() { + return mRoots; + } + public void updateFileTransfer(DataTransfer transfer, Interaction.InteractionStatus eventCode) { DataTransfer dataTransfer = (DataTransfer) findConversationElement(transfer.getId()); if (dataTransfer != null) { @@ -469,11 +656,18 @@ public class Conversation extends ConversationHistory { return mAccountId; } + public enum Mode { + OneToOne, + AdminInvitesOnly, + InvitesOnly, + Public + } + public interface ConversationActionCallback { - void removeConversation(CallContact callContact); + void removeConversation(Uri callContact); - void clearConversation(CallContact callContact); + void clearConversation(Uri callContact); void copyContactNumberToClipboard(String contactNumber); diff --git a/ring-android/libringclient/src/main/java/cx/ring/model/ConversationHistory.java b/ring-android/libringclient/src/main/java/cx/ring/model/ConversationHistory.java index 832bb6c3b37081b9efbbe1f10da01e9b6e1bd838..38cc05d1de3fb89beafd8fa8629808763805fb4a 100644 --- a/ring-android/libringclient/src/main/java/cx/ring/model/ConversationHistory.java +++ b/ring-android/libringclient/src/main/java/cx/ring/model/ConversationHistory.java @@ -37,9 +37,6 @@ public class ConversationHistory { @DatabaseField(columnName = COLUMN_EXTRA_DATA) String mExtraData; - String account; - - /* Needed by ORMLite */ public ConversationHistory() { } @@ -70,12 +67,4 @@ public class ConversationHistory { return mParticipant; } - public void setAccount(String account) { - this.account = account; - } - - public String getAccount() { - return account; - } - } diff --git a/ring-android/libringclient/src/main/java/cx/ring/model/DataTransfer.java b/ring-android/libringclient/src/main/java/cx/ring/model/DataTransfer.java index e809d3916e236f1b2e69f2cdaa70631824c8bb53..146f58549dc17a10d41d331bdbfe3f240bee06b5 100644 --- a/ring-android/libringclient/src/main/java/cx/ring/model/DataTransfer.java +++ b/ring-android/libringclient/src/main/java/cx/ring/model/DataTransfer.java @@ -20,6 +20,7 @@ */ package cx.ring.model; +import java.io.File; import java.util.Set; import cx.ring.utils.HashUtils; @@ -29,8 +30,10 @@ public class DataTransfer extends Interaction { private long mTotalSize; private long mBytesProgress; - private String mPeerId; + //private final String mPeerId; private String mExtension; + //private String mTransferId; + public File destination; private static final Set<String> IMAGE_EXTENSIONS = HashUtils.asSet("jpg", "jpeg", "png", "gif"); private static final Set<String> AUDIO_EXTENSIONS = HashUtils.asSet("ogg", "mp3", "aac", "flac", "m4a"); @@ -38,11 +41,11 @@ public class DataTransfer extends Interaction { private static final int MAX_SIZE = 32 * 1024 * 1024; private static final int UNLIMITED_SIZE = 256 * 1024 * 1024; - public DataTransfer(ConversationHistory conversation, String account, String displayName, boolean isOutgoing, long totalSize, long bytesProgress, long daemonId) { - mAuthor = isOutgoing ? null : conversation.getParticipant(); + public DataTransfer(ConversationHistory conversation, String peer, String account, String displayName, boolean isOutgoing, long totalSize, long bytesProgress, long daemonId) { + mAuthor = isOutgoing ? null : peer; mAccount = account; mConversation = conversation; - mPeerId = conversation.getParticipant(); + mAuthor = peer; mTotalSize = totalSize; mBytesProgress = bytesProgress; mBody = displayName; @@ -51,16 +54,15 @@ public class DataTransfer extends Interaction { mTimestamp = System.currentTimeMillis(); mIsRead = 1; mDaemonId = daemonId; - mIsIncoming = mAuthor != null; + mIsIncoming = !isOutgoing; } - public DataTransfer(Interaction interaction) { mId = interaction.getId(); mDaemonId = interaction.getDaemonId(); mAuthor = interaction.getAuthor(); mConversation = interaction.getConversation(); - mPeerId = interaction.getConversation().getParticipant(); + // mPeerId = interaction.getConversation().getParticipant(); mBody = interaction.getBody(); mStatus = interaction.getStatus().toString(); mType = interaction.getType().toString(); @@ -68,12 +70,29 @@ public class DataTransfer extends Interaction { mAccount = interaction.getAccount(); mContact = interaction.getContact(); mIsRead = 1; - mIsIncoming = mAuthor != null; + mIsIncoming = interaction.mIsIncoming;//mAuthor != null; + } + + public DataTransfer(long transferId, String accountId, String peerUri, String displayName, boolean isOutgoing, long timestamp, long totalSize, long bytesProgress) { + mDaemonId = transferId; + mAccount = accountId; + //mTransferId = transferId; + //mPeerId = peerUri; + mBody = displayName; + mAuthor = peerUri; + mIsIncoming = !isOutgoing; + mTotalSize = totalSize; + mBytesProgress = bytesProgress; + mTimestamp = timestamp; + //mDaemonId = Long.parseUnsignedLong(transferId); + mType = InteractionType.DATA_TRANSFER.toString(); } public String getExtension() { + if (mBody == null) + return null; if (mExtension == null) - mExtension = StringUtils.getFileExtension(getDisplayName()).toLowerCase(); + mExtension = StringUtils.getFileExtension(mBody).toLowerCase(); return mExtension; } @@ -96,6 +115,9 @@ public class DataTransfer extends Interaction { } public String getStoragePath() { + if (StringUtils.isEmpty(mBody)) { + return getMessageId(); + } String ext = StringUtils.getFileExtension(mBody); if (ext.length() > 8) ext = ext.substring(0, 8); @@ -125,11 +147,6 @@ public class DataTransfer extends Interaction { public void setBytesProgress(long bytesProgress) { mBytesProgress = bytesProgress; } - public String getPeerId() { - return mPeerId; - } - - public boolean isError() { return getStatus().isError(); } diff --git a/ring-android/libringclient/src/main/java/cx/ring/model/Interaction.java b/ring-android/libringclient/src/main/java/cx/ring/model/Interaction.java index eb13f90e0f97a44d6ca354e691a66ac9398afe17..8bbe3ddf4848f53a7a7d892f70751c1edfa27b17 100644 --- a/ring-android/libringclient/src/main/java/cx/ring/model/Interaction.java +++ b/ring-android/libringclient/src/main/java/cx/ring/model/Interaction.java @@ -24,6 +24,8 @@ import com.google.gson.JsonParser; import com.j256.ormlite.field.DatabaseField; import com.j256.ormlite.table.DatabaseTable; +import java.util.List; + @DatabaseTable(tableName = Interaction.TABLE_NAME) public class Interaction { @@ -63,6 +65,11 @@ public class Interaction { @DatabaseField(columnName = COLUMN_EXTRA_FLAG) String mExtraFlag = new JsonObject().toString(); + // Swarm + private String mConversationId; + private String mMessageId; + private List<String> mParentIds; + /* Needed by ORMLite */ public Interaction() { } @@ -168,6 +175,18 @@ public class Interaction { return mDaemonId == null ? null : Long.toString(mDaemonId); } + public String getMessageId() { + return mMessageId; + } + + public String getConversationId() { + return mConversationId; + } + + public List<String> getParentIds() { + return mParentIds; + } + public boolean isIncoming() { return mIsIncoming; } @@ -184,6 +203,17 @@ public class Interaction { mContact = contact; } + public void setSwarmInfo(String conversationId) { + mConversationId = conversationId; + mMessageId = null; + mParentIds = null; + } + public void setSwarmInfo(String conversationId, String messageId, List<String> parents) { + mConversationId = conversationId; + mMessageId = messageId; + mParentIds = parents; + } + public enum InteractionStatus { UNKNOWN, SENDING, SUCCESS, DISPLAYED, INVALID, FAILURE, @@ -222,6 +252,7 @@ public class Interaction { case 1: return TRANSFER_CREATED; case 2: + case 9: return TRANSFER_ERROR; case 3: return TRANSFER_AWAITING_PEER; @@ -232,11 +263,7 @@ public class Interaction { case 6: return TRANSFER_FINISHED; case 7: - return TRANSFER_UNJOINABLE_PEER; case 8: - return TRANSFER_UNJOINABLE_PEER; - case 9: - return TRANSFER_ERROR; case 10: return TRANSFER_UNJOINABLE_PEER; case 11: diff --git a/ring-android/libringclient/src/main/java/cx/ring/model/Phone.java b/ring-android/libringclient/src/main/java/cx/ring/model/Phone.java index d0d629dc4c2ff8201446e9faebc206f080d9ee4f..7c514658b1b5bdf8db15133838b639f655b4db12 100644 --- a/ring-android/libringclient/src/main/java/cx/ring/model/Phone.java +++ b/ring-android/libringclient/src/main/java/cx/ring/model/Phone.java @@ -43,7 +43,7 @@ public class Phone { public Phone(String number, int category, String label) { mNumberType = NumberType.UNKNOWN; mCategory = category; - mNumber = new Uri(number); + mNumber = Uri.fromString(number); mLabel = label; } public Phone(Uri number, int category, String label) { @@ -55,7 +55,7 @@ public class Phone { public Phone(String number, int category, String label, NumberType numberType) { mNumberType = numberType; - mNumber = new Uri(number); + mNumber = Uri.fromString(number); mLabel = label; mCategory = category; } @@ -79,7 +79,7 @@ public class Phone { } public void setNumber(String number) { - setNumber(new Uri(number)); + setNumber(Uri.fromString(number)); } public NumberType getNumbertype() { diff --git a/ring-android/libringclient/src/main/java/cx/ring/model/SipCall.java b/ring-android/libringclient/src/main/java/cx/ring/model/SipCall.java index cb1308d8c59c12e9b337b602ddaf113649f71900..00911714adbf00caab670afab6302fea571307df 100644 --- a/ring-android/libringclient/src/main/java/cx/ring/model/SipCall.java +++ b/ring-android/libringclient/src/main/java/cx/ring/model/SipCall.java @@ -20,11 +20,13 @@ package cx.ring.model; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import java.util.HashMap; import java.util.Locale; import java.util.Map; +import java.util.Objects; import cx.ring.utils.ProfileChunk; import cx.ring.utils.StringUtils; @@ -44,6 +46,7 @@ public class SipCall extends Interaction { public final static String KEY_VIDEO_MUTED = "VIDEO_MUTED"; public final static String KEY_AUDIO_CODEC = "AUDIO_CODEC"; public final static String KEY_VIDEO_CODEC = "VIDEO_CODEC"; + public final static String KEY_REGISTERED_NAME = "REGISTERED_NAME"; public final static String KEY_DURATION = "duration"; public final static String KEY_CONF_ID = "CONF_ID"; @@ -56,6 +59,7 @@ public class SipCall extends Interaction { private CallStatus mCallStatus = CallStatus.NONE; private long timestampEnd = 0; + private Long duration = null; private boolean missed = true; private String mAudioCodec; private String mVideoCodec; @@ -93,7 +97,7 @@ public class SipCall extends Interaction { mContact = interaction.getContact(); } - public SipCall(String daemonId, String account, String contactNumber, Direction direction) { + public SipCall(String daemonId, String account, String contactNumber, Direction direction, long timestamp) { mDaemonId = daemonId == null ? null : Long.parseLong(daemonId); mIsIncoming = direction == Direction.INCOMING; mAccount = account; @@ -105,7 +109,7 @@ public class SipCall extends Interaction { } public SipCall(String daemonId, Map<String, String> call_details) { - this(daemonId, call_details.get(KEY_ACCOUNT_ID), call_details.get(KEY_PEER_NUMBER), Direction.fromInt(Integer.parseInt(call_details.get(KEY_CALL_TYPE)))); + this(daemonId, call_details.get(KEY_ACCOUNT_ID), call_details.get(KEY_PEER_NUMBER), Direction.fromInt(Integer.parseInt(call_details.get(KEY_CALL_TYPE))), System.currentTimeMillis()); setCallState(CallStatus.fromString(call_details.get(KEY_CALL_STATE))); setDetails(call_details); } @@ -130,17 +134,29 @@ public class SipCall extends Interaction { } public Long getDuration() { - return toJson(mExtraFlag).get(KEY_DURATION) == null ? 0 : toJson(mExtraFlag).get(KEY_DURATION).getAsLong(); + if (duration == null) { + JsonElement element = toJson(mExtraFlag).get(KEY_DURATION); + if (element != null) { + duration = element.getAsLong(); + } + } + return duration == null ? 0 : duration; } public void setDuration(Long value) { - JsonObject jsonObject = getExtraFlag(); - jsonObject.addProperty(KEY_DURATION, value); - mExtraFlag = fromJson(jsonObject); + if (Objects.equals(value, duration)) + return; + duration = value; + if (duration != null && duration != 0) { + JsonObject jsonObject = getExtraFlag(); + jsonObject.addProperty(KEY_DURATION, value); + mExtraFlag = fromJson(jsonObject); + missed = false; + } } public String getDurationString() { - Long mDuration = getDuration() / 1000; + long mDuration = getDuration() / 1000; if (mDuration < 60) { return String.format(Locale.getDefault(), "%02d secs", mDuration); } diff --git a/ring-android/libringclient/src/main/java/cx/ring/model/TextMessage.java b/ring-android/libringclient/src/main/java/cx/ring/model/TextMessage.java index 36881cd70ce4d09f86c30ead86a2cc2a6d681251..b9ca64b398b3d512f9c14e0b73499d341e7954c8 100644 --- a/ring-android/libringclient/src/main/java/cx/ring/model/TextMessage.java +++ b/ring-android/libringclient/src/main/java/cx/ring/model/TextMessage.java @@ -45,6 +45,16 @@ public class TextMessage extends Interaction { mBody = message; } + public TextMessage(String author, String account, long timestamp, ConversationHistory conversation, String message, boolean isIncoming) { + mAuthor = author; + mAccount = account; + mTimestamp = timestamp; + mType = InteractionType.TEXT.toString(); + mConversation = conversation; + mIsIncoming = isIncoming; + mBody = message; + } + public TextMessage(Interaction interaction) { mId = interaction.getId(); mAuthor = interaction.getAuthor(); diff --git a/ring-android/libringclient/src/main/java/cx/ring/model/TrustRequest.java b/ring-android/libringclient/src/main/java/cx/ring/model/TrustRequest.java index ec87091b44033f2d5f83e9949445277b3997f540..bf85766f0272620c77a66a1e8ea0f47c5433a72f 100644 --- a/ring-android/libringclient/src/main/java/cx/ring/model/TrustRequest.java +++ b/ring-android/libringclient/src/main/java/cx/ring/model/TrustRequest.java @@ -21,46 +21,53 @@ package cx.ring.model; import java.util.Map; -import java.util.Random; +import cx.ring.utils.StringUtils; import ezvcard.Ezvcard; import ezvcard.VCard; public class TrustRequest { private static final String TAG = TrustRequest.class.getSimpleName(); - private String mAccountId; + private final String mAccountId; private String mContactUsername = null; - private String mContactId; + private final Uri mRequestUri; + private String mConversationId; private VCard mVcard; private String mMessage; - private long mTimestamp; - private int mUuid; + private final long mTimestamp; private boolean mUsernameResolved = false; - public TrustRequest(String accountId, String contact, long received, String payload) { + public TrustRequest(String accountId, Uri uri, long received, String payload, String conversationId) { mAccountId = accountId; - mContactId = contact; + mRequestUri = uri; + mConversationId = StringUtils.isEmpty(conversationId) ? null : conversationId; mTimestamp = received; mVcard = Ezvcard.parse(payload).first(); mMessage = null; - mUuid = new Random().nextInt(); } public TrustRequest(String accountId, Map<String, String> info) { - this(accountId, info.get("from"), Long.decode(info.get("received")) * 1000L, info.get("payload")); + this(accountId, Uri.fromId(info.get("from")), Long.decode(info.get("received")) * 1000L, info.get("payload"), info.get("conversationId")); } - public int getUuid() { - return mUuid; + public TrustRequest(String accountId, Uri contactUri, String conversationId) { + mAccountId = accountId; + mRequestUri = contactUri; + mConversationId = conversationId; + mTimestamp = 0; } public String getAccountId() { return mAccountId; } - public String getContactId() { - return mContactId; + public Uri getUri() { + return mRequestUri; + } + + public String getConversationId() { + return mConversationId; } public String getFullname() { @@ -73,7 +80,7 @@ public class TrustRequest { public String getDisplayname() { boolean hasUsername = mContactUsername != null && !mContactUsername.isEmpty(); - return hasUsername ? mContactUsername : mContactId; + return hasUsername ? mContactUsername : mRequestUri.toString(); } public boolean isNameResolved() { @@ -104,4 +111,8 @@ public class TrustRequest { public void setMessage(String message) { mMessage = message; } + + public void setConversationId(String conversationId) { + mConversationId = conversationId; + } } diff --git a/ring-android/libringclient/src/main/java/cx/ring/model/Uri.java b/ring-android/libringclient/src/main/java/cx/ring/model/Uri.java index 0c38a9168fc8aff1c7ea146684480ce907c1b0d1..4116fd8683b360e3fede883bd346b748a958b46c 100644 --- a/ring-android/libringclient/src/main/java/cx/ring/model/Uri.java +++ b/ring-android/libringclient/src/main/java/cx/ring/model/Uri.java @@ -20,81 +20,67 @@ package cx.ring.model; import java.io.Serializable; +import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; import cx.ring.utils.StringUtils; +import cx.ring.utils.Tuple; public class Uri implements Serializable { - private String mDisplayName = null; - private String mScheme = null; - private String mUsername = null; - private String mHost = null; - private String mPort = null; + private final String mScheme; + private final String mUsername; + private final String mHost; + private final String mPort; private static final Pattern ANGLE_BRACKETS_PATTERN = Pattern.compile("^\\s*([^<>]+)?\\s*<([^<>]+)>\\s*$"); - private static final Pattern RING_ID_PATTERN = Pattern.compile("^\\p{XDigit}{40}$", Pattern.CASE_INSENSITIVE); + private static final Pattern HEX_ID_PATTERN = Pattern.compile("^\\p{XDigit}{40}$", Pattern.CASE_INSENSITIVE); private static final Pattern RING_URI_PATTERN = Pattern.compile("^\\s*(?:ring(?:[\\s\\:]+))?(\\p{XDigit}{40})(?:@ring\\.dht)?\\s*$", Pattern.CASE_INSENSITIVE); private static final Pattern URI_PATTERN = Pattern.compile("^\\s*(\\w+:)?(?:([\\w.]+)@)?(?:([\\d\\w\\.\\-]+)(?::(\\d+))?)\\s*$", Pattern.CASE_INSENSITIVE); public static final String RING_URI_SCHEME = "ring:"; public static final String JAMI_URI_SCHEME = "jami:"; - public static final String SWARM_URI_SCHEME = "swarm:"; + public static final String SWARM_SCHEME = "swarm:"; private static final String ipv4Pattern = "(([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.){3}([01]?\\d\\d?|2[0-4]\\d|25[0-5])"; private static final String ipv6Pattern = "([0-9a-f]{1,4}:){7}([0-9a-f]){1,4}"; private static final Pattern VALID_IPV4_PATTERN = Pattern.compile(ipv4Pattern, Pattern.CASE_INSENSITIVE); private static final Pattern VALID_IPV6_PATTERN = Pattern.compile(ipv6Pattern, Pattern.CASE_INSENSITIVE); - - public Uri(String uri) { - if (uri != null) { - parseUri(uri); - } + public Uri(String scheme, String user, String host, String port) { + mScheme = scheme; + mUsername = user; + mHost = host; + mPort = port; } - public String getRawUriString() { - if (isRingId()) { - if (isSwarm()) - return getScheme() + getRawRingId(); - return RING_URI_SCHEME + getRawRingId(); - } - - StringBuilder builder = new StringBuilder(64); - if (getUsername() != null && !getUsername().isEmpty()) { - builder.append(getUsername()).append("@"); - } - if (getHost() != null) { - builder.append(getHost()); - } - if (getPort() != null && !getPort().isEmpty()) { - builder.append(":").append(getPort()); - } - return builder.toString(); + public Uri(String scheme, String host) { + mScheme = scheme; + mUsername = null; + mHost = host; + mPort = null; } - public String getUriString() { - if (getDisplayName() == null || getDisplayName().isEmpty()) { - return getRawUriString(); + static public Uri fromString(String uri) { + Matcher m = URI_PATTERN.matcher(uri); + if (m.find()) { + return new Uri(m.group(1), m.group(2), m.group(3), m.group(4)); + } else { + return new Uri(null, null, uri, null); } - return getDisplayName() + " <" + getRawUriString() + ">"; - } - - @Override - public String toString() { - return getUriString(); } - public boolean isSingleIp() { - return (getUsername() == null || getUsername().isEmpty()) && isIpAddress(getHost()); + static public Tuple<Uri, String> fromStringWithName(String uriString) { + Matcher m = ANGLE_BRACKETS_PATTERN.matcher(uriString); + if (m.find()) { + return new Tuple<>(fromString(m.group(2)), m.group(1)); + } else { + return new Tuple<>(fromString(uriString), null); + } } - public boolean isRingId() { - return (getHost() != null && RING_ID_PATTERN.matcher(getHost()).find()) - || (getUsername() != null && RING_ID_PATTERN.matcher(getUsername()).find()); - } - public boolean isSwarm() { - return SWARM_URI_SCHEME.equals(getScheme()); + public static Uri fromId(String conversationId) { + return new Uri(null, null, conversationId, null); } public String getRawRingId() { @@ -105,43 +91,51 @@ public class Uri implements Serializable { } } - private void parseUri(String uri) { - Matcher m = ANGLE_BRACKETS_PATTERN.matcher(uri); - if (m.find()) { - setDisplayName(m.group(1)); - parseUriRaw(m.group(2)); - } else { - parseUriRaw(uri); - } + public String getUri() { + if (isSwarm()) + return getScheme() + getRawRingId(); + if (isHexId()) + return getRawRingId(); + return toString(); } - private void parseUriRaw(String uri) { - Matcher m = URI_PATTERN.matcher(uri); - if (m.find()) { - setScheme(m.group(1)); - setUsername(m.group(2)); - setHost(m.group(3)); - setPort(m.group(4)); - } else { - setHost(uri); + public String getRawUriString() { + if (isSwarm()) + return getScheme() + getRawRingId(); + if (isHexId()) { + return RING_URI_SCHEME + getRawRingId(); } + return toString(); } - public String getUri() { - if (isSwarm()) - return getScheme() + getRawRingId(); - if (isRingId()) - return getRawRingId(); - else { - StringBuilder builder = new StringBuilder(64); - if (!StringUtils.isEmpty(getUsername())) { - builder.append(getUsername()).append("@"); - } - if (getHost() != null) { - builder.append(getHost()); - } - return builder.toString(); + @Override + public String toString() { + StringBuilder builder = new StringBuilder(64); + if (!StringUtils.isEmpty(mScheme)) { + builder.append(mScheme); + } + if (!StringUtils.isEmpty(mUsername)) { + builder.append(mUsername).append('@'); + } + if (!StringUtils.isEmpty(mHost)) { + builder.append(mHost); + } + if (!StringUtils.isEmpty(mPort)) { + builder.append(':').append(mPort); } + return builder.toString(); + } + + public boolean isSingleIp() { + return (getUsername() == null || getUsername().isEmpty()) && isIpAddress(getHost()); + } + + public boolean isHexId() { + return (getHost() != null && HEX_ID_PATTERN.matcher(getHost()).find()) + || (getUsername() != null && HEX_ID_PATTERN.matcher(getUsername()).find()); + } + public boolean isSwarm() { + return SWARM_SCHEME.equals(getScheme()); } @Override @@ -153,12 +147,12 @@ public class Uri implements Serializable { return false; } Uri uo = (Uri) o; - return (getUsername() == null ? uo.getUsername() == null : getUsername().equals(uo.getUsername())) - && (getHost() == null ? uo.getHost() == null : getHost().equals(uo.getHost())); + return Objects.equals(getUsername(), uo.getUsername()) + && Objects.equals(getHost(), uo.getHost()); } public boolean isEmpty() { - return (getUsername() == null || getUsername().isEmpty()) && (getHost() == null || getHost().isEmpty()); + return StringUtils.isEmpty(getUsername()) && StringUtils.isEmpty(getHost()); } /** @@ -171,7 +165,6 @@ public class Uri implements Serializable { * <code>false</code> otherwise. */ public static boolean isIpAddress(String ipAddress) { - Matcher m1 = VALID_IPV4_PATTERN.matcher(ipAddress); if (m1.matches()) { return true; @@ -180,43 +173,20 @@ public class Uri implements Serializable { return m2.matches(); } - public String getDisplayName() { - return mDisplayName; - } - - public void setDisplayName(String displayName) { - this.mDisplayName = displayName; - } - public String getScheme() { return mScheme; } - public void setScheme(String scheme) { - this.mScheme = scheme; - } - public String getUsername() { return mUsername; } - public void setUsername(String username) { - this.mUsername = username; - } - public String getHost() { return mHost; } - public void setHost(String host) { - this.mHost = host; - } - public String getPort() { return mPort; } - public void setPort(String port) { - this.mPort = port; - } } diff --git a/ring-android/libringclient/src/main/java/cx/ring/services/AccountService.java b/ring-android/libringclient/src/main/java/cx/ring/services/AccountService.java index 3b6195dbc5b0b86f38d2117c4ac3620497bfdc21..4fd2f9d90d0b3d21656165834eefd4815098ac10 100644 --- a/ring-android/libringclient/src/main/java/cx/ring/services/AccountService.java +++ b/ring-android/libringclient/src/main/java/cx/ring/services/AccountService.java @@ -26,10 +26,15 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import java.io.File; +import java.io.IOException; 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; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -58,6 +63,7 @@ import cx.ring.model.DataTransfer; import cx.ring.model.DataTransferError; import cx.ring.model.Interaction; import cx.ring.model.Interaction.InteractionStatus; +import cx.ring.model.SipCall; import cx.ring.model.TextMessage; import cx.ring.model.TrustRequest; import cx.ring.model.Uri; @@ -67,6 +73,7 @@ import cx.ring.utils.Log; import cx.ring.utils.StringUtils; import cx.ring.utils.SwigNativeConverter; import cx.ring.utils.VCardUtils; +import ezvcard.Ezvcard; import ezvcard.VCard; import io.reactivex.Completable; import io.reactivex.Maybe; @@ -78,7 +85,7 @@ import io.reactivex.subjects.PublishSubject; import io.reactivex.subjects.Subject; /** - * This service handles the accounts (Ring and SIP) + * This service handles the accounts * - Load and manage the accounts stored in the daemon * - Keep a local cache of the accounts * - handle the callbacks that are send by the daemon @@ -112,7 +119,6 @@ public class AccountService { private boolean mHasSipAccount; private boolean mHasRingAccount; - private final HashMap<Long, DataTransfer> mDataTransfers = new HashMap<>(); private DataTransfer mStartingTransfer = null; private final BehaviorSubject<List<Account>> accountsSubject = BehaviorSubject.create(); @@ -168,6 +174,7 @@ public class AccountService { } private final Subject<Message> incomingMessageSubject = PublishSubject.create(); + private final Subject<Interaction> incomingSwarmMessageSubject = PublishSubject.create(); private final Observable<TextMessage> incomingTextMessageSubject = incomingMessageSubject .flatMapMaybe(msg -> { @@ -204,7 +211,7 @@ public class AccountService { l.time = obj.get("time").getAsLong(); l.accountId = msg.accountId; l.callId = msg.callId; - l.peer = new Uri(msg.author); + l.peer = Uri.fromId(msg.author); return Maybe.just(l); } catch (Exception e) { Log.w(TAG, "Failed to receive geolocation", e); @@ -291,6 +298,12 @@ public class AccountService { return incomingTextMessageSubject; } + public Observable<TextMessage> getIncomingSwarmMessages() { + return incomingSwarmMessageSubject + .filter(i -> i instanceof TextMessage) + .map(i -> (TextMessage) i); + } + public Observable<Location> getLocationUpdates() { return incomingLocationSubject; } @@ -364,7 +377,6 @@ public class AccountService { account.setVolatileDetails(volatileAccountDetails); } - if (account.isSip()) { hasSip = true; } else if (account.isJami()) { @@ -377,20 +389,56 @@ public class AccountService { for (Map<String, String> requestInfo : requests) { TrustRequest request = new TrustRequest(accountId, requestInfo); account.addRequest(request); - CallContact contact = account.getContactFromCache(request.getContactId()); - if (!contact.detailsLoaded) { - mVCardService.loadVCardProfile(request.getVCard()) - .subscribeOn(Schedulers.computation()) - .subscribe(profile -> contact.setProfile(profile.first, profile.second)); + } + Log.w(TAG, accountId + " loading conversations"); + List<String> conversations = Ringservice.getConversations(account.getAccountID()); + for (String conversationId : conversations) { + Map<String, String> info = Ringservice.conversationInfos(accountId, conversationId); + /*for (Map.Entry<String, String> i : info.entrySet()) { + Log.w(TAG, "conversation info: " + i.getKey() + " " + i.getValue()); + }*/ + Conversation.Mode mode = Conversation.Mode.values()[Integer.parseInt(info.get("mode"))]; + Conversation conversation = account.newSwarm(conversationId, mode); + conversation.setLastMessageRead(mHistoryService.getLastMessageRead(accountId, conversation.getUri())); + for (Map<String, String> member : Ringservice.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")); + CallContact contact = conversation.findContact(uri); + if (contact == null) { + contact = account.getContactFromCache(uri); + conversation.addContact(contact); + } + } + Log.w(TAG, accountId + " " + conversation.getUri().getRawUriString() + " " + conversation.getContacts().size()); + account.conversationStarted(conversation); + //account.addSwarmConversation(conversationId, members); + } + for (Map<String, String> requestData : Ringservice.getConversationRequests(account.getAccountID()).toNative()) { + /*for (Map.Entry<String, String> e : requestData.entrySet()) { + Log.e(TAG, "Request: " + e.getKey() + " " + e.getValue()); + }*/ + String conversationId = requestData.get("id"); + Uri from = Uri.fromString(requestData.get("from")); + TrustRequest request = account.getRequest(from); + if (request != null) { + request.setConversationId(conversationId); + } else { + account.addRequest(new TrustRequest(account.getAccountID(), from, conversationId)); } - // If name is in cache this can be synchronous - if (enabled) - Ringservice.lookupAddress(accountId, "", request.getContactId()); } + mExecutor.execute(() -> { + for (String conversationId : conversations) + Ringservice.loadConversationMessages(accountId, conversationId, "", 2); + }); + if (enabled) { for (CallContact contact : account.getContacts().values()) { if (!contact.isUsernameLoaded()) - Ringservice.lookupAddress(accountId, "", contact.getPrimaryUri().getRawRingId()); + Ringservice.lookupAddress(accountId, "", contact.getUri().getRawRingId()); } } } @@ -406,22 +454,6 @@ public class AccountService { } } - // migration to multi accounts - mHistoryService.migrateDatabase(accountIds); - mHistoryService.getMigrationStatus().firstOrError().subscribe(migrationStatus -> { - if (migrationStatus == HistoryService.MigrationStatus.SUCCESSFUL) { - mVCardService.migrateProfiles(accountIds); - for (String accountId : accountIds) { - Account account = getAccount(accountId); - if (account.isJami()) { - mVCardService.migrateContact(account.getContacts(), accountId); - migrateContactEvents(account, account.getContacts(), account.getRequestsMigration()); - } - } - mVCardService.deleteLegacyProfiles(); - } - }, e -> Log.e(TAG, "Error completing profile migration", e)); - accountsSubject.onNext(newAccounts); } @@ -618,7 +650,7 @@ public class AccountService { Random r = new Random(System.currentTimeMillis()); int key = Math.abs(r.nextInt()); - Log.d(TAG, "sendProfile, vcard " + stringVCard); + Log.d(TAG, "sendProfile, vcard " + callId); while (i <= nbTotal) { HashMap<String, String> chunk = new HashMap<>(); @@ -639,6 +671,35 @@ public class AccountService { mExecutor.execute(() -> Ringservice.setMessageDisplayed(accountId, contactId, messageId, 3)); } + public Single<Conversation> startConversation(String accountId, Collection<String> initialMembers) { + Account account = getAccount(accountId); + return Single.fromCallable(() -> { + Log.w(TAG, "startConversation"); + String id = Ringservice.startConversation(accountId); + Conversation conversation = account.getSwarm(id);//new Conversation(accountId, new Uri(id)); + for (String member : initialMembers) { + Log.w(TAG, "addConversationMember " + member); + Ringservice.addConversationMember(accountId, id, member); + conversation.addContact(account.getContactFromCache(member)); + } + account.conversationStarted(conversation); + Log.w(TAG, "loadConversationMessages"); + Ringservice.loadConversationMessages(accountId, id, id, 2); + return conversation; + }).subscribeOn(Schedulers.from(mExecutor)); + } + + public void loadConversationHistory(String accountId, Uri conversationUri, String root, long n) { + Ringservice.loadConversationMessages(accountId, conversationUri.getRawRingId(), root, n); + } + + public void sendConversationMessage(String accountId, Uri conversationUri, String txt) { + mExecutor.execute(() -> { + Log.w(TAG, "sendConversationMessages " + conversationUri.getRawRingId() + " : " + txt); + Ringservice.sendMessage(accountId, conversationUri.getRawRingId(), txt, ""); + }); + } + /** * @return Account Ids list from Daemon */ @@ -1023,7 +1084,7 @@ public class AccountService { } } account.removeRequest(from); - handleTrustRequest(accountId, from.getUri(), null, ContactType.INVITATION_ACCEPTED); + //handleTrustRequest(accountId, from, null, ContactType.INVITATION_ACCEPTED); } mExecutor.execute(() -> Ringservice.acceptTrustRequest(accountId, from.getRawRingId())); } @@ -1032,67 +1093,32 @@ public class AccountService { /** * Handles adding contacts and is the initial point of conversation creation * - * @param accountId the user's account id + * @param conversation the user's account * @param contactUri the contacts raw string uri */ - private void handleTrustRequest(String accountId, String contactUri, TrustRequest request, ContactType type) { + private void handleTrustRequest(Conversation conversation, Uri contactUri, TrustRequest request, ContactType type) { ContactEvent event = new ContactEvent(); switch (type) { case ADDED: break; case INVITATION_RECEIVED: event.setStatus(Interaction.InteractionStatus.UNKNOWN); - event.setAuthor(contactUri); + event.setAuthor(contactUri.getRawRingId()); event.setTimestamp(request.getTimestamp()); break; case INVITATION_ACCEPTED: event.setStatus(Interaction.InteractionStatus.SUCCESS); - event.setAuthor(contactUri); + event.setAuthor(contactUri.getRawRingId()); break; case INVITATION_DISCARDED: - mHistoryService.clearHistory(contactUri, accountId, true).subscribe(); + mHistoryService.clearHistory(contactUri.getRawRingId(), conversation.getAccountId(), true).subscribe(); return; default: return; } - insertContactEvent(accountId, contactUri, event); - } - - /** - * Migrates contact events including trust requests to the database. This is necessary because the old database did not store contact events. - * Should only be called in the migration process. - * - * @param account the user's account object - * @param contacts the user's contacts (if they exist) - * @param requests the user's trust requests (if they exist) - */ - private void migrateContactEvents(Account account, Map<String, CallContact> contacts, Map<String, TrustRequest> requests) { - String accountId = account.getAccountID(); - for (TrustRequest request : requests.values()) { - CallContact contact = account.getContactFromCache(request.getContactId()); - ContactEvent event = new ContactEvent(contact, request); - insertContactEvent(accountId, contact.getPrimaryUri().getUri(), event); - } - for (CallContact contact : contacts.values()) { - ContactEvent event = new ContactEvent(contact); - insertContactEvent(accountId, contact.getPrimaryUri().getUri(), event); - - } + mHistoryService.insertInteraction(conversation.getAccountId(), conversation, event).subscribe(); } - /** - * Inserts a contact event, and if needed, a conversation, into the database - * - * @param accountId the user's account ID - * @param participantUri the contact's string uri - * @param event the contact event - */ - private void insertContactEvent(String accountId, String participantUri, ContactEvent event) { - Conversation conversation = getAccount(accountId).getByUri(participantUri); - mHistoryService.insertInteraction(accountId, conversation, event).subscribe(); - } - - private enum ContactType { ADDED, INVITATION_RECEIVED, INVITATION_ACCEPTED, INVITATION_DISCARDED } @@ -1100,24 +1126,24 @@ public class AccountService { /** * Refuses and blocks a pending trust request */ - public boolean discardTrustRequest(final String accountId, final Uri contact) { + public boolean discardTrustRequest(final String accountId, final Uri contactUri) { Account account = getAccount(accountId); boolean removed = false; if (account != null) { - removed = account.removeRequest(contact); - handleTrustRequest(accountId, contact.getUri(), null, ContactType.INVITATION_DISCARDED); + removed = account.removeRequest(contactUri); + mHistoryService.clearHistory(contactUri.getRawRingId(), accountId, true).subscribe(); } - mExecutor.execute(() -> Ringservice.discardTrustRequest(accountId, contact.getRawRingId())); + mExecutor.execute(() -> Ringservice.discardTrustRequest(accountId, contactUri.getRawRingId())); return removed; } /** * Sends a new trust request */ - public void sendTrustRequest(final String accountId, final String to, final Blob message) { - Log.i(TAG, "sendTrustRequest() " + accountId + " " + to); - handleTrustRequest(accountId, new Uri(to).getUri(), null, ContactType.ADDED); - mExecutor.execute(() -> Ringservice.sendTrustRequest(accountId, to, message == null ? new Blob() : message)); + public void sendTrustRequest(Conversation conversation, final Uri to, final Blob message) { + Log.i(TAG, "sendTrustRequest() " + conversation.getAccountId() + " " + to); + handleTrustRequest(conversation, to, null, ContactType.ADDED); + mExecutor.execute(() -> Ringservice.sendTrustRequest(conversation.getAccountId(), to.getRawRingId(), message == null ? new Blob() : message)); } /** @@ -1125,7 +1151,7 @@ public class AccountService { */ public void addContact(final String accountId, final String uri) { Log.i(TAG, "addContact() " + accountId + " " + uri); - handleTrustRequest(accountId, new Uri(uri).getUri(), null, ContactType.ADDED); + //handleTrustRequest(accountId, Uri.fromString(uri), null, ContactType.ADDED); mExecutor.execute(() -> Ringservice.addContact(accountId, uri)); } @@ -1198,7 +1224,7 @@ public class AccountService { } public void setPushNotificationToken(final String pushNotificationToken) { - Log.i(TAG, "setPushNotificationToken()"); + //Log.i(TAG, "setPushNotificationToken()"); mExecutor.execute(() -> Ringservice.setPushNotificationToken(pushNotificationToken)); } @@ -1216,7 +1242,7 @@ public class AccountService { } void registrationStateChanged(String accountId, String newState, int code, String detailString) { - Log.d(TAG, "registrationStateChanged: " + accountId + ", " + newState + ", " + code + ", " + detailString); + //Log.d(TAG, "registrationStateChanged: " + accountId + ", " + newState + ", " + code + ", " + detailString); Account account = getAccount(accountId); if (account == null) { @@ -1253,7 +1279,7 @@ public class AccountService { if (account == null) { return; } - Log.d(TAG, "volatileAccountDetailsChanged: " + accountId + " " + details.size()); + //Log.d(TAG, "volatileAccountDetailsChanged: " + accountId + " " + details.size()); account.setVolatileDetails(details); accountSubject.onNext(account); } @@ -1288,17 +1314,24 @@ public class AccountService { incomingMessageSubject.onNext(message); } - void accountMessageStatusChanged(String accountId, long messageId, String to, int status) { - Log.d(TAG, "accountMessageStatusChanged: " + accountId + ", " + messageId + ", " + to + ", " + status); - mHistoryService - .accountMessageStatusChanged(accountId, messageId, to, status) - .subscribe(textMessageSubject::onNext, e -> Log.e(TAG, "Error updating message: " + e.getLocalizedMessage())); + void accountMessageStatusChanged(String accountId, String conversationId, String messageId, String peer, int status) { + Log.d(TAG, "accountMessageStatusChanged: " + accountId + ", " + conversationId + ", " + messageId + ", " + peer + ", " + status); + if (StringUtils.isEmpty(conversationId)) { + mHistoryService + .accountMessageStatusChanged(accountId, messageId, peer, status) + .subscribe(textMessageSubject::onNext, e -> Log.e(TAG, "Error updating message: " + e.getLocalizedMessage())); + } else { + TextMessage msg = new TextMessage(peer, accountId, messageId, null, null); + msg.setStatus(status); + msg.setSwarmInfo(conversationId, messageId, null); + textMessageSubject.onNext(msg); + } } - public void composingStatusChanged(String accountId, String contactUri, int status) { - Log.d(TAG, "composingStatusChanged: " + accountId + ", " + contactUri + " " + status); + public void composingStatusChanged(String accountId, String conversationId, String contactUri, int status) { + Log.d(TAG, "composingStatusChanged: " + accountId + ", " + contactUri + ", " + conversationId + ", " + status); getAccountSingle(accountId) - .subscribe(account -> account.composingStatusChanged(new Uri(contactUri), Account.ComposingStatus.fromInt(status))); + .subscribe(account -> account.composingStatusChanged(conversationId, Uri.fromId(contactUri), Account.ComposingStatus.fromInt(status))); } void errorAlert(int alert) { @@ -1366,24 +1399,30 @@ public class AccountService { mDeviceRevocationSubject.onNext(result); } - void incomingTrustRequest(String accountId, String from, String message, long received) { - Log.d(TAG, "incomingTrustRequest: " + accountId + ", " + from + ", " + received); + void incomingTrustRequest(String accountId, String conversationId, String from, String message, long received) { + Log.d(TAG, "incomingTrustRequest: " + accountId + ", " + conversationId + ", " + from + ", " + received); Account account = getAccount(accountId); if (account != null) { - TrustRequest request = new TrustRequest(accountId, from, received * 1000L, message); + Uri fromUri = Uri.fromString(from); + TrustRequest request = account.getRequest(fromUri); + if (request == null) + request = new TrustRequest(accountId, fromUri, received * 1000L, message, conversationId); + else + request.setVCard(Ezvcard.parse(message).first()); + VCard vcard = request.getVCard(); if (vcard != null) { - CallContact contact = account.getContactFromCache(request.getContactId()); + CallContact contact = account.getContactFromCache(fromUri); if (!contact.detailsLoaded) { - VCardUtils.savePeerProfileToDisk(vcard, accountId, from + ".vcf", mDeviceRuntimeService.provideFilesDir()); + // VCardUtils.savePeerProfileToDisk(vcard, accountId, from + ".vcf", mDeviceRuntimeService.provideFilesDir()); mVCardService.loadVCardProfile(vcard) .subscribeOn(Schedulers.computation()) .subscribe(profile -> contact.setProfile(profile.first, profile.second)); } } account.addRequest(request); - handleTrustRequest(accountId, new Uri(from).getUri(), request, ContactType.INVITATION_RECEIVED); + // handleTrustRequest(account, Uri.fromString(from), request, ContactType.INVITATION_RECEIVED); if (account.isEnabled()) lookupAddress(accountId, "", from); incomingRequestsSubject.onNext(request); @@ -1409,21 +1448,24 @@ public class AccountService { } void registeredNameFound(String accountId, int state, String address, String name) { - // Log.d(TAG, "registeredNameFound: " + accountId + ", " + state + ", " + name + ", " + address); - - if (!StringUtils.isEmpty(address)) { - Account account = getAccount(accountId); - if (account != null) { - account.registeredNameFound(state, address, name); + try { + //Log.d(TAG, "registeredNameFound: " + accountId + ", " + state + ", " + name + ", " + address); + if (!StringUtils.isEmpty(address)) { + Account account = getAccount(accountId); + if (account != null) { + account.registeredNameFound(state, address, name); + } } - } - RegisteredName r = new RegisteredName(); - r.accountId = accountId; - r.address = address; - r.name = name; - r.state = state; - registeredNameSubject.onNext(r); + RegisteredName r = new RegisteredName(); + r.accountId = accountId; + r.address = address; + r.name = name; + r.state = state; + registeredNameSubject.onNext(r); + } catch (Exception e) { + Log.w(TAG, "registeredNameFound exception", e); + } } public void userSearchEnded(String accountId, int state, String query, ArrayList<Map<String, String>> results) { @@ -1447,21 +1489,187 @@ public class AccountService { searchResultSubject.onNext(r); } - public DataTransferError sendFile(final DataTransfer dataTransfer, File file) { - mStartingTransfer = dataTransfer; + 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(); + String type = message.get("type"); + String author = message.get("author"); + Uri authorUri = Uri.fromId(author); - DataTransferInfo dataTransferInfo = new DataTransferInfo(); - dataTransferInfo.setAccountId(dataTransfer.getAccount()); - dataTransferInfo.setPeer(dataTransfer.getPeerId()); - dataTransferInfo.setPath(file.getPath()); - dataTransferInfo.setDisplayName(dataTransfer.getDisplayName()); + //Log.w(TAG, "addMessage2 " + type + " " + author + " id:" + id + " parents:" + parents); - Log.i(TAG, "sendFile() id=" + dataTransfer.getId() + " accountId=" + dataTransferInfo.getAccountId() + ", peer=" + dataTransferInfo.getPeer() + ", filePath=" + dataTransferInfo.getPath()); + long timestamp = Long.parseLong(message.get("timestamp")) * 1000; + CallContact contact = conversation.findContact(authorUri); + if (contact == null) { + contact = account.getContactFromCache(authorUri); + } + Interaction interaction; + switch (type) { + case "member": + contact.setAddedDate(new Date(timestamp)); + interaction = new ContactEvent(contact); + break; + case "text/plain": + interaction = new TextMessage(author, account.getAccountID(), timestamp, conversation, message.get("body"), !contact.isUser()); + break; + case "application/data-transfer+json": { + String transferId = message.get("tid"); + long tid = Long.parseUnsignedLong(transferId); + interaction = account.getDataTransfer(tid); + if (interaction == null) { + interaction = new DataTransfer(tid, account.getAccountID(), author, contact.isUser(), timestamp, 0, 0); + } + break; + } + case "application/call-history+json": + interaction = new SipCall(null, account.getAccountID(), authorUri.getRawUriString(), contact.isUser() ? SipCall.Direction.OUTGOING : SipCall.Direction.INCOMING, timestamp); + ((SipCall) interaction).setDuration(Long.parseLong(message.get("duration"))); + break; + case "merge": + default: + interaction = new Interaction(); + interaction.setAccount(account.getAccountID()); + interaction.setConversation(conversation); + interaction.setType(Interaction.InteractionType.INVALID); + break; + } + interaction.setContact(contact); + interaction.setSwarmInfo(conversation.getUri().getRawRingId(), id, parents); + if (conversation.addSwarmElement(interaction)) { + if (conversation.isVisible()) + mHistoryService.setMessageRead(account.getAccountID(), conversation.getUri(), interaction.getMessageId()); + } + return interaction; + } + + public void conversationLoaded(String accountId, String conversationId, List<Map<String, String>> messages) { try { - return getDataTransferError(mExecutor.submit(() -> Ringservice.sendFile(dataTransferInfo, 0)).get()); - } catch (Exception ignored) { + 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); + } + account.conversationChanged(); + } catch (Exception e) { + Log.e(TAG, "Exception loading message", e); } - return DataTransferError.UNKNOWN; + } + + private enum ConversationMemberEvent { + Add, Join, Remove, Ban + } + + public void conversationMemberEvent(String accountId, String conversationId, String peerUri, int event) { + Log.w(TAG, "ConversationCallback: conversationMemberEvent " + accountId + "/" + conversationId); + Account account = getAccount(accountId); + if (account == null) { + Log.w(TAG, "conversationMemberEvent: can't find account"); + return; + } + Conversation conversation = account.getSwarm(conversationId); + Uri uri = Uri.fromId(peerUri); + switch (ConversationMemberEvent.values()[event]) { + case Add: + case Join: { + CallContact contact = account.getContactFromCache(uri); + conversation.addContact(contact); + break; + } + case Remove: + case Ban: { + CallContact contact = conversation.findContact(uri); + if (contact != null) { + conversation.removeContact(contact); + } + break; + } + } + } + + public void conversationReady(String accountId, String conversationId) { + Log.w(TAG, "ConversationCallback: conversationReady " + accountId + "/" + conversationId); + Account account = getAccount(accountId); + if (account == null) { + Log.w(TAG, "conversationReady: can't find account"); + return; + } + StringMap info = Ringservice.conversationInfos(accountId, conversationId); + /*for (Map.Entry<String, String> i : info.entrySet()) { + Log.w(TAG, "conversation info: " + i.getKey() + " " + i.getValue()); + }*/ + int modeInt = Integer.parseInt(info.get("mode")); + Conversation.Mode mode = Conversation.Mode.values()[modeInt]; + Conversation conversation = account.newSwarm(conversationId, mode); + + for (Map<String, String> member : Ringservice.getConversationMembers(accountId, conversationId)) { + Uri uri = Uri.fromId(member.get("uri")); + CallContact contact = conversation.findContact(uri); + if (contact == null) { + contact = account.getContactFromCache(uri); + conversation.addContact(contact); + } + } + account.conversationStarted(conversation); + } + + public void conversationRemoved(String accountId, String conversationId) { + Account account = getAccount(accountId); + if (account == null) { + Log.w(TAG, "conversationRemoved: can't find account"); + return; + } + account.removeSwarm(conversationId); + } + + public void conversationRequestReceived(String accountId, String conversationId, Map<String, String> metadata) { + Log.w(TAG, "ConversationCallback: conversationRequestReceived " + accountId + "/" + conversationId + " " + metadata.size()); + Account account = getAccount(accountId); + if (account == null) { + Log.w(TAG, "conversationRequestReceived: can't find account"); + return; + } + Uri contactUri = Uri.fromId(metadata.get("from")); + account.addRequest(new TrustRequest(account.getAccountID(), contactUri, conversationId)); + } + + public void messageReceived(String accountId, String conversationId, Map<String, String> message) { + 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) { + account.conversationUpdated(conversation); + boolean isIncoming = !interaction.getContact().isUser(); + if (isIncoming) { + incomingSwarmMessageSubject.onNext(interaction); + } + } + } + + public Completable sendFile(final DataTransfer dataTransfer) { + return Completable.fromAction(() -> { + mStartingTransfer = dataTransfer; + + DataTransferInfo dataTransferInfo = new DataTransferInfo(); + dataTransferInfo.setAccountId(dataTransfer.getAccount()); + dataTransferInfo.setConversationId(dataTransfer.getConversationId()); + dataTransferInfo.setPeer(dataTransfer.getConversationId()); + dataTransferInfo.setPath(dataTransfer.destination.getAbsolutePath()); + dataTransferInfo.setDisplayName(dataTransfer.getDisplayName()); + + Log.i(TAG, "sendFile() id=" + dataTransfer.getId() + " accountId=" + dataTransferInfo.getAccountId() + ", peer=" + dataTransferInfo.getPeer() + ", filePath=" + dataTransferInfo.getPath()); + DataTransferError err = getDataTransferError(Ringservice.sendFile(dataTransferInfo, 0)); + if (err != DataTransferError.SUCCESS) { + throw new IOException(err.name()); + } + }).subscribeOn(Schedulers.from(mExecutor)); } public List<cx.ring.daemon.Message> getLastMessages(String accountId, long baseTime) { @@ -1473,32 +1681,41 @@ public class AccountService { return new ArrayList<>(); } - public void acceptFileTransfer(long id) { - acceptFileTransfer(getDataTransfer(id)); + public void acceptFileTransfer(final String accountId, final Uri conversationUri, long id) { + Account account = getAccount(accountId); + if (account != null) { + Conversation conversation = account.getByUri(conversationUri); + acceptFileTransfer(conversation, account.getDataTransfer(id)); + } } - public void acceptFileTransfer(DataTransfer transfer) { + public void acceptFileTransfer(Conversation conversation, DataTransfer transfer) { if (transfer == null) return; - File path = mDeviceRuntimeService.getTemporaryPath(transfer.getPeerId(), transfer.getStoragePath()); - acceptFileTransfer(transfer.getDaemonId(), path.getAbsolutePath(), 0); + File path = mDeviceRuntimeService.getTemporaryPath(conversation.getUri().getRawRingId(), transfer.getStoragePath()); + String conversationId = conversation.getUri().getRawRingId(); + acceptFileTransfer(conversation.getAccountId(), conversationId, transfer.getDaemonId(), path.getAbsolutePath(), 0); } - private void acceptFileTransfer(final Long dataTransferId, final String filePath, final long offset) { + private void acceptFileTransfer(final String accountId, final String conversationId, final Long dataTransferId, final String filePath, long offset) { Log.i(TAG, "acceptFileTransfer() id=" + dataTransferId + ", path=" + filePath + ", offset=" + offset); - mExecutor.execute(() -> Ringservice.acceptFileTransfer(dataTransferId, filePath, offset)); + mExecutor.execute(() -> Ringservice.acceptFileTransfer(accountId, conversationId, dataTransferId, filePath, offset)); } - public void cancelDataTransfer(final Long dataTransferId) { + public void cancelDataTransfer(final String accountId, final String conversationId, long dataTransferId) { Log.i(TAG, "cancelDataTransfer() id=" + dataTransferId); - mExecutor.execute(() -> Ringservice.cancelDataTransfer(dataTransferId)); + mExecutor.execute(() -> Ringservice.cancelDataTransfer(accountId, conversationId, dataTransferId)); } private class DataTransferRefreshTask implements Runnable { + private final Account mAccount; + private final Conversation mConversation; private final DataTransfer mToUpdate; public ScheduledFuture<?> scheduledTask; - DataTransferRefreshTask(DataTransfer t) { + DataTransferRefreshTask(Account account, Conversation conversation, DataTransfer t) { + mAccount = account; + mConversation = conversation; mToUpdate = t; } @@ -1506,7 +1723,7 @@ public class AccountService { public void run() { synchronized (mToUpdate) { if (mToUpdate.getStatus() == Interaction.InteractionStatus.TRANSFER_ONGOING) { - dataTransferEvent(mToUpdate.getDaemonId(), 5); + dataTransferEvent(mAccount, mConversation, mToUpdate.getDaemonId(), 5); } else { scheduledTask.cancel(false); scheduledTask = null; @@ -1515,55 +1732,67 @@ public class AccountService { } } - void dataTransferEvent(final long transferId, int eventCode) { + void dataTransferEvent(String accountId, String conversationId, final long transferId, int eventCode) { + Account account = getAccount(accountId); + if (account != null) { + Conversation conversation = StringUtils.isEmpty(conversationId) ? null : account.getSwarm(conversationId); + if (conversation == null) + conversation = account.getByUri(conversationId); + if (conversation == null) + return; + dataTransferEvent(account, conversation, transferId, eventCode); + } + } + void dataTransferEvent(Account account, Conversation conversation, final long transferId, int eventCode) { Interaction.InteractionStatus transferStatus = getDataTransferEventCode(eventCode); Log.d(TAG, "Data Transfer " + transferStatus); DataTransferInfo info = new DataTransferInfo(); - if (getDataTransferError(Ringservice.dataTransferInfo(transferId, info)) != DataTransferError.SUCCESS) + if (getDataTransferError(Ringservice.dataTransferInfo(account.getAccountID(), conversation.getUri().getRawRingId(), transferId, info)) != DataTransferError.SUCCESS) return; - Account account = getAccount(info.getAccountId()); - Conversation c = account.getByUri(new Uri(info.getPeer()).getUri()); - boolean outgoing = info.getFlags() == 0; - DataTransfer transfer = mDataTransfers.get(transferId); + DataTransfer transfer = account.getDataTransfer(transferId); if (transfer == null) { - if (outgoing && mStartingTransfer != null) { transfer = mStartingTransfer; mStartingTransfer = null; } else { - transfer = new DataTransfer(c, account.getAccountID(), info.getDisplayName(), + transfer = new DataTransfer(conversation, info.getPeer(), account.getAccountID(), info.getDisplayName(), outgoing, info.getTotalSize(), info.getBytesProgress(), transferId); - - mHistoryService.insertInteraction(account.getAccountID(), c, transfer).blockingAwait(); + if (conversation.isSwarm()) { + transfer.setSwarmInfo(conversation.getUri().getRawRingId(), null, null); + } else { + mHistoryService.insertInteraction(account.getAccountID(), conversation, transfer).blockingAwait(); + } } - mDataTransfers.put(transferId, transfer); + account.putDataTransfer(transferId, transfer); } else synchronized (transfer) { InteractionStatus oldState = transfer.getStatus(); if (oldState != transferStatus) { if (transferStatus == Interaction.InteractionStatus.TRANSFER_ONGOING) { - DataTransferRefreshTask task = new DataTransferRefreshTask(transfer); + DataTransferRefreshTask task = new DataTransferRefreshTask(account, conversation, transfer); task.scheduledTask = mExecutor.scheduleAtFixedRate(task, DATA_TRANSFER_REFRESH_PERIOD, DATA_TRANSFER_REFRESH_PERIOD, TimeUnit.MILLISECONDS); } else if (transferStatus.isError()) { if (!transfer.isOutgoing()) { - File tmpPath = mDeviceRuntimeService.getTemporaryPath(transfer.getPeerId(), transfer.getStoragePath()); + File tmpPath = mDeviceRuntimeService.getTemporaryPath(conversation.getUri().getRawRingId(), transfer.getStoragePath()); tmpPath.delete(); } } else if (transferStatus == (Interaction.InteractionStatus.TRANSFER_FINISHED)) { if (!transfer.isOutgoing()) { - File tmpPath = mDeviceRuntimeService.getTemporaryPath(transfer.getPeerId(), transfer.getStoragePath()); - File path = mDeviceRuntimeService.getConversationPath(transfer.getPeerId(), transfer.getStoragePath()); + File tmpPath = mDeviceRuntimeService.getTemporaryPath(conversation.getUri().getRawRingId(), transfer.getStoragePath()); + File path = mDeviceRuntimeService.getConversationPath(conversation.getUri().getRawRingId(), transfer.getStoragePath()); FileUtils.moveFile(tmpPath, path); } } } transfer.setStatus(transferStatus); transfer.setBytesProgress(info.getBytesProgress()); - mHistoryService.updateInteraction(transfer, account.getAccountID()).subscribe(); + if (!conversation.isSwarm()) { + mHistoryService.updateInteraction(transfer, account.getAccountID()).subscribe(); + } } dataTransferSubject.onNext(transfer); @@ -1580,21 +1809,16 @@ public class AccountService { } private static DataTransferError getDataTransferError(Long errorCode) { - DataTransferError dataTransferError = DataTransferError.UNKNOWN; if (errorCode == null) { Log.e(TAG, "getDataTransferError: invalid error code"); } else { try { - dataTransferError = DataTransferError.values()[errorCode.intValue()]; + return DataTransferError.values()[errorCode.intValue()]; } catch (ArrayIndexOutOfBoundsException ignored) { Log.e(TAG, "getDataTransferError: invalid data transfer error from daemon"); } } - return dataTransferError; - } - - public DataTransfer getDataTransfer(long id) { - return mDataTransfers.get(id); + return DataTransferError.UNKNOWN; } public Subject<DataTransfer> getDataTransfers() { diff --git a/ring-android/libringclient/src/main/java/cx/ring/services/CallService.java b/ring-android/libringclient/src/main/java/cx/ring/services/CallService.java index 6f5103ccd2a62b2e8c62b18c65be03ed0824b551..984004593777aa87bcaa3f027851430f7b69d3c2 100644 --- a/ring-android/libringclient/src/main/java/cx/ring/services/CallService.java +++ b/ring-android/libringclient/src/main/java/cx/ring/services/CallService.java @@ -43,6 +43,7 @@ import cx.ring.model.Conversation; import cx.ring.model.SipCall; import cx.ring.model.Uri; import cx.ring.utils.Log; +import cx.ring.utils.StringUtils; import ezvcard.VCard; import io.reactivex.Completable; import io.reactivex.Observable; @@ -128,7 +129,7 @@ public class CallService { List<Conference.ParticipantInfo> newInfo = new ArrayList<>(info.size()); if (conference.isConference()) { for (Map<String, String> i : info) { - SipCall call = conference.findCallByContact(new Uri(i.get("uri"))); + SipCall call = conference.findCallByContact(Uri.fromString(i.get("uri"))); if (call != null) { newInfo.add(new Conference.ParticipantInfo(call.getContact(), i)); } else { @@ -138,7 +139,7 @@ public class CallService { } else { Account account = mAccountService.getAccount(conference.getCall().getAccount()); for (Map<String, String> i : info) - newInfo.add(new Conference.ParticipantInfo(account.getContactFromCache(new Uri(i.get("uri"))), i)); + newInfo.add(new Conference.ParticipantInfo(account.getContactFromCache(Uri.fromString(i.get("uri"))), i)); } conference.setInfo(newInfo); } @@ -226,25 +227,27 @@ public class CallService { return call == null ? Observable.error(new IllegalArgumentException()) : getCallUpdates(call); }*/ - public Observable<SipCall> placeCallObservable(final String accountId, final String number, final boolean audioOnly) { - return placeCall(accountId, number, audioOnly) + public Observable<SipCall> placeCallObservable(final String accountId, final Uri conversationUri, final Uri number, final boolean audioOnly) { + return placeCall(accountId, conversationUri, number, audioOnly) .flatMapObservable(this::getCallUpdates); } - public Single<SipCall> placeCall(final String account, final String number, final boolean audioOnly) { + public Single<SipCall> placeCall(final String account, final Uri conversationUri, final Uri number, final boolean audioOnly) { return Single.fromCallable(() -> { Log.i(TAG, "placeCall() thread running... " + number + " audioOnly: " + audioOnly); HashMap<String, String> volatileDetails = new HashMap<>(); volatileDetails.put(SipCall.KEY_AUDIO_ONLY, String.valueOf(audioOnly)); - String callId = Ringservice.placeCall(account, number, StringMap.toSwig(volatileDetails)); + String callId = Ringservice.placeCall(account, number.getUri(), StringMap.toSwig(volatileDetails)); if (callId == null || callId.isEmpty()) return null; if (audioOnly) { Ringservice.muteLocalMedia(callId, "MEDIA_TYPE_VIDEO", true); } SipCall call = addCall(account, callId, number, SipCall.Direction.OUTGOING); + if (conversationUri.isSwarm()) + call.setSwarmInfo(conversationUri.getRawRingId()); call.muteVideo(audioOnly); updateConnectionCount(); return call; @@ -445,7 +448,7 @@ public class CallService { public SipCall getCurrentCallForContactId(String contactId) { for (SipCall call : currentCalls.values()) { - if (contactId.contains(call.getContact().getPhones().get(0).getNumber().toString())) { + if (contactId.contains(call.getContact().getPrimaryNumber())) { return call; } } @@ -459,15 +462,15 @@ public class CallService { } } - private SipCall addCall(String accountId, String callId, String from, SipCall.Direction direction) { + private SipCall addCall(String accountId, String callId, Uri from, SipCall.Direction direction) { synchronized (currentCalls) { SipCall call = currentCalls.get(callId); if (call == null) { Account account = mAccountService.getAccount(accountId); - Uri fromUri = new Uri(from); - Conversation conversation = account.getByUri(fromUri); - CallContact contact = mContactService.findContact(account, fromUri); - call = new SipCall(callId, new Uri(from).getUri(), accountId, conversation, contact, direction); + CallContact contact = mContactService.findContact(account, from); + Uri conversationUri = contact.getConversationUri().blockingFirst(); + Conversation conversation = conversationUri.equals(from) ? account.getByUri(from) : account.getSwarm(conversationUri.getRawRingId()); + call = new SipCall(callId, from.getUri(), accountId, conversation, contact, direction); currentCalls.put(callId, call); } else { Log.w(TAG, "Call already existed ! " + callId + " " + from); @@ -486,8 +489,6 @@ public class CallService { conference = new Conference(call); currentConferences.put(confId, conference); conferenceSubject.onNext(conference); - } else { - Log.w(TAG, "Conference already existed ! " + confId); } return conference; } @@ -499,23 +500,24 @@ public class CallService { sipCall.setCallState(callState); sipCall.setDetails(Ringservice.getCallDetails(callId).toNative()); } else if (callState != SipCall.CallStatus.OVER && callState != SipCall.CallStatus.FAILURE) { - Map<String, String> callDetails = Ringservice.getCallDetails(callId).toNative(); + Map<String, String> callDetails = Ringservice.getCallDetails(callId); sipCall = new SipCall(callId, callDetails); - if (!callDetails.containsKey(SipCall.KEY_PEER_NUMBER)) { + if (StringUtils.isEmpty(sipCall.getContactNumber())) { Log.w(TAG, "No number"); return null; } + sipCall.setCallState(callState); - CallContact contact = mContactService.findContact(mAccountService.getAccount(sipCall.getAccount()), new Uri(sipCall.getContactNumber())); - String registeredName = callDetails.get("REGISTERED_NAME"); + CallContact contact = mContactService.findContact(mAccountService.getAccount(sipCall.getAccount()), Uri.fromString(sipCall.getContactNumber())); + String registeredName = callDetails.get(SipCall.KEY_REGISTERED_NAME); if (registeredName != null && !registeredName.isEmpty()) { contact.setUsername(registeredName); } sipCall.setContact(contact); Account account = mAccountService.getAccount(sipCall.getAccount()); - sipCall.setConversation(account.getByUri(contact.getPrimaryUri())); + sipCall.setConversation(account.getByUri(contact.getUri())); currentCalls.put(callId, sipCall); updateConnectionCount(); @@ -560,7 +562,7 @@ public class CallService { void incomingCall(String accountId, String callId, String from) { Log.d(TAG, "incoming call: " + accountId + ", " + callId + ", " + from); - SipCall call = addCall(accountId, callId, from, SipCall.Direction.INCOMING); + SipCall call = addCall(accountId, callId, Uri.fromStringWithName(from).first, SipCall.Direction.INCOMING); callSubject.onNext(call); updateConnectionCount(); } diff --git a/ring-android/libringclient/src/main/java/cx/ring/services/ContactService.java b/ring-android/libringclient/src/main/java/cx/ring/services/ContactService.java index 1229cc9b899bf8427fcff60cd71427bb2287155b..fb6e2699842b3ff161ea7f7608b60a2dbb00f6a6 100644 --- a/ring-android/libringclient/src/main/java/cx/ring/services/ContactService.java +++ b/ring-android/libringclient/src/main/java/cx/ring/services/ContactService.java @@ -88,7 +88,10 @@ public abstract class ContactService { } public Observable<CallContact> observeContact(String accountId, CallContact contact, boolean withPresence) { - Uri uri = contact.getPrimaryUri(); + //Log.w(TAG, "observeContact " + accountId + " " + contact.getUri() + " " + contact.isUser()); + if (contact.isUser()) + withPresence = false; + Uri uri = contact.getUri(); String uriString = uri.getRawUriString(); synchronized (contact) { if (contact.getPresenceUpdates() == null) { @@ -126,12 +129,19 @@ public abstract class ContactService { } public Observable<List<CallContact>> observeContact(String accountId, List<CallContact> contacts, boolean withPresence) { - if (contacts.size() == 1) { - return observeContact(accountId, contacts.get(0), withPresence).map(Collections::singletonList); - } else { + if (contacts.isEmpty()) { + return Observable.just(Collections.emptyList()); + } /*else if (contacts.size() == 1 || contacts.size() == 2) { + + return observeContact(accountId, contacts.get(contacts.size() - 1), withPresence).map(Collections::singletonList); + } */else { List<Observable<CallContact>> observables = new ArrayList<>(contacts.size()); - for (CallContact contact : contacts) - observables.add(observeContact(accountId, contact, false)); + for (CallContact contact : contacts) { + if (!contact.isUser()) + observables.add(observeContact(accountId, contact, withPresence)); + } + if (observables.isEmpty()) + return Observable.just(Collections.emptyList()); return Observable.combineLatest(observables, a -> { List<CallContact> obs = new ArrayList<>(a.length); for (Object o : a) @@ -141,22 +151,31 @@ public abstract class ContactService { } } - public Single<CallContact> getLoadedContact(String accountId, CallContact contact) { - return observeContact(accountId, contact, false) + public Single<CallContact> getLoadedContact(String accountId, CallContact contact, boolean withPresence) { + return observeContact(accountId, contact, withPresence) .filter(c -> c.isUsernameLoaded() && c.detailsLoaded) .firstOrError(); } + public Single<CallContact> getLoadedContact(String accountId, CallContact contact) { + return getLoadedContact(accountId, contact, false); + } - public Single<List<CallContact>> getLoadedContact(String accountId, List<CallContact> contacts) { - return observeContact(accountId, contacts, false) - .filter(cts -> { - for (CallContact c : cts) { - if (!c.isUsernameLoaded() || !c.detailsLoaded) - return false; - } - return true; - }) - .firstOrError(); + public Single<List<CallContact>> getLoadedContact(String accountId, List<CallContact> contacts, boolean withPresence) { + if (contacts.isEmpty()) + return Single.just(Collections.emptyList()); + return Observable.fromIterable(contacts) + .flatMapSingle(contact -> getLoadedContact(accountId, contact, withPresence)) + .toList(contacts.size()); + } + + public List<Observable<CallContact>> observeLoadedContact(String accountId, List<CallContact> contacts, boolean withPresence) { + if (contacts.isEmpty()) + return Collections.emptyList(); + List<Observable<CallContact>> ret = new ArrayList<>(contacts.size()); + for (CallContact contact : contacts) + ret.add(observeContact(accountId, contact, withPresence) + .filter(c -> c.isUsernameLoaded() && c.detailsLoaded)); + return ret; } /** @@ -169,7 +188,7 @@ public abstract class ContactService { if (StringUtils.isEmpty(number) || account == null) { return null; } - return findContact(account, new Uri(number)); + return findContact(account, Uri.fromString(number)); } public CallContact findContact(Account account, Uri uri) { diff --git a/ring-android/libringclient/src/main/java/cx/ring/services/DaemonService.java b/ring-android/libringclient/src/main/java/cx/ring/services/DaemonService.java index 57b338a159c01c9975f1504cfeda5bbd5ddd4426..9871aee81ab016fa8832aebe9a19cbc30650e9f8 100644 --- a/ring-android/libringclient/src/main/java/cx/ring/services/DaemonService.java +++ b/ring-android/libringclient/src/main/java/cx/ring/services/DaemonService.java @@ -156,11 +156,11 @@ public class DaemonService { @Override public void accountProfileReceived(String account_id, String name, String photo) { - mExecutor.submit(() -> mAccountService.accountProfileReceived(account_id, name, photo)); + mAccountService.accountProfileReceived(account_id, name, photo); } @Override - public void incomingAccountMessage(String accountId, String messageId, String from, StringMap messages) { + public void incomingAccountMessage(String accountId, String from, String messageId, StringMap messages) { if (messages == null || messages.isEmpty()) return; Map<String, String> jmessages = messages.toNativeFromUtf8(); @@ -168,13 +168,13 @@ public class DaemonService { } @Override - public void accountMessageStatusChanged(String accountId, long messageId, String to, int status) { - mExecutor.submit(() -> mAccountService.accountMessageStatusChanged(accountId, messageId, to, status)); + public void accountMessageStatusChanged(String accountId, String conversationId, String peer, String messageId, int status) { + mExecutor.submit(() -> mAccountService.accountMessageStatusChanged(accountId, conversationId, messageId, peer, status)); } @Override - public void composingStatusChanged(String accountId, String contactUri, int status) { - mExecutor.submit(() -> mAccountService.composingStatusChanged(accountId, contactUri, status)); + public void composingStatusChanged(String accountId, String conversationId, String contactUri, int status) { + mExecutor.submit(() -> mAccountService.composingStatusChanged(accountId, conversationId, contactUri, status)); } @Override @@ -234,9 +234,9 @@ public class DaemonService { } @Override - public void incomingTrustRequest(String accountId, String from, Blob message, long received) { + public void incomingTrustRequest(String accountId, String conversationId, String from, Blob message, long received) { String jmessage = message.toJavaString(); - mExecutor.submit(() -> mAccountService.incomingTrustRequest(accountId, from, jmessage, received)); + mExecutor.submit(() -> mAccountService.incomingTrustRequest(accountId, conversationId, from, jmessage, received)); } @Override @@ -269,7 +269,7 @@ public class DaemonService { @Override public void remoteRecordingChanged(String call_id, String peer_number, boolean state) { - mCallService.remoteRecordingChanged(call_id, new Uri(peer_number), state); + mCallService.remoteRecordingChanged(call_id, Uri.fromString(peer_number), state); } @Override @@ -380,35 +380,42 @@ public class DaemonService { class DaemonDataTransferCallback extends DataTransferCallback { @Override - public void dataTransferEvent(long transferId, int eventCode) { - Log.d(TAG, "dataTransferEvent: transferId=" + transferId + ", eventCode=" + eventCode); - mAccountService.dataTransferEvent(transferId, eventCode); + public void dataTransferEvent(String accountId, String conversationId, long transferId, int eventCode) { + Log.d(TAG, "dataTransferEvent: conversationId=" + conversationId + ", transferId=" + transferId + ", eventCode=" + eventCode); + mAccountService.dataTransferEvent(accountId, conversationId, transferId, eventCode); } } class ConversationCallbackImpl extends ConversationCallback { @Override public void conversationLoaded(long id, String accountId, String conversationId, VectMap messages) { + mAccountService.conversationLoaded(accountId, conversationId, messages.toNative()); } @Override public void conversationReady(String accountId, String conversationId) { + mAccountService.conversationReady(accountId, conversationId); } @Override public void conversationRemoved(String accountId, String conversationId) { + mAccountService.conversationRemoved(accountId, conversationId); } @Override public void conversationRequestReceived(String accountId, String conversationId, StringMap metadata) { + mAccountService.conversationRequestReceived(accountId, conversationId, metadata.toNative()); } @Override public void conversationMemberEvent(String accountId, String conversationId, String uri, int event) { + mAccountService.conversationMemberEvent(accountId, conversationId, uri, event); } @Override public void messageReceived(String accountId, String conversationId, StringMap message) { + mAccountService.messageReceived(accountId, conversationId, message.toNative()); } } + } diff --git a/ring-android/libringclient/src/main/java/cx/ring/services/HistoryService.java b/ring-android/libringclient/src/main/java/cx/ring/services/HistoryService.java index c68f4ea0c34a0424c720d75ed12c3f059541815d..0e475b479b8b6dcab2957c5105119a7e298ccf45 100644 --- a/ring-android/libringclient/src/main/java/cx/ring/services/HistoryService.java +++ b/ring-android/libringclient/src/main/java/cx/ring/services/HistoryService.java @@ -36,7 +36,6 @@ import cx.ring.model.Uri; import cx.ring.utils.Log; import cx.ring.utils.StringUtils; import io.reactivex.Completable; -import io.reactivex.Observable; import io.reactivex.Scheduler; import io.reactivex.Single; import io.reactivex.schedulers.Schedulers; @@ -56,6 +55,9 @@ public abstract class HistoryService { protected abstract void migrateDatabase(List<String> accounts); + public abstract void setMessageRead(String accountId, Uri conversationUri, String lastId); + public abstract String getLastMessageRead(String accountId, Uri conversationUri); + protected abstract void deleteAccountHistory(String accountId); public abstract Observable<MigrationStatus> getMigrationStatus(); @@ -152,16 +154,11 @@ public abstract class HistoryService { return Completable.fromAction(() -> { Log.d(TAG, "Inserting interaction for account -> " + accountId); Dao<ConversationHistory, Integer> conversationDataDao = getConversationDataDao(accountId); - ConversationHistory history = conversationDataDao.queryBuilder().where().eq(ConversationHistory.COLUMN_PARTICIPANT, conversation.getParticipant()).queryForFirst(); - if (history == null) { history = conversationDataDao.createIfNotExists(new ConversationHistory(conversation.getParticipant())); - interaction.setConversation(history); - } else - interaction.setConversation(history); - - + } + //interaction.setConversation(conversation); conversation.setId(history.getId()); getInteractionDataDao(accountId).create(interaction); }) @@ -185,7 +182,8 @@ public abstract class HistoryService { "WHERE conversations.id = final.conversation\n" + "GROUP BY final.conversation\n", (columnNames, resultColumns) -> new Interaction(resultColumns[0], resultColumns[1], new ConversationHistory(Integer.parseInt(resultColumns[2]), resultColumns[12]), resultColumns[3], resultColumns[4], resultColumns[5], resultColumns[6], resultColumns[7], resultColumns[8], resultColumns[9])).getResults()) - .subscribeOn(scheduler).doOnError(e -> Log.e(TAG, "Can't load smartlist from database", e)) + .subscribeOn(scheduler) + .doOnError(e -> Log.e(TAG, "Can't load smartlist from database", e)) .onErrorReturn(e -> new ArrayList<>()); } @@ -212,20 +210,17 @@ public abstract class HistoryService { Single<TextMessage> incomingMessage(final String accountId, final String daemonId, final String from, final String message) { return Single.fromCallable(() -> { - String f = new Uri(from).getUri(); + String fromUri = Uri.fromString(from).getUri(); Dao<ConversationHistory, Integer> conversationDataDao = getConversationDataDao(accountId); - - ConversationHistory conversation = conversationDataDao.queryBuilder().where().eq(ConversationHistory.COLUMN_PARTICIPANT, f).queryForFirst(); - + ConversationHistory conversation = conversationDataDao.queryBuilder().where().eq(ConversationHistory.COLUMN_PARTICIPANT, fromUri).queryForFirst(); if (conversation == null) { - conversation = new ConversationHistory(f); + conversation = new ConversationHistory(fromUri); conversation.setId(conversationDataDao.extractId(conversationDataDao.createIfNotExists(conversation))); } - TextMessage txt = new TextMessage(f, accountId, daemonId, conversation, message); + TextMessage txt = new TextMessage(fromUri, accountId, daemonId, conversation, message); txt.setStatus(Interaction.InteractionStatus.SUCCESS); - Log.w(TAG, "New text messsage " + txt.getAuthor() + " " + txt.getDaemonId() + " " + txt.getBody()); getInteractionDataDao(accountId).create(txt); return txt; @@ -233,14 +228,14 @@ public abstract class HistoryService { } - Single<TextMessage> accountMessageStatusChanged(String accountId, long daemonId, String to, int status) { + Single<TextMessage> accountMessageStatusChanged(String accountId, String daemonId, String peer, int status) { return Single.fromCallable(() -> { - List<Interaction> textList = getInteractionDataDao(accountId).queryForEq(Interaction.COLUMN_DAEMON_ID, Long.toString(daemonId)); + List<Interaction> textList = getInteractionDataDao(accountId).queryForEq(Interaction.COLUMN_DAEMON_ID, daemonId); if (textList == null || textList.isEmpty()) { throw new RuntimeException("accountMessageStatusChanged: not able to find message with id " + daemonId + " in database"); } Interaction text = textList.get(0); - String participant = (new Uri(to)).getUri(); + String participant = Uri.fromString(peer).getUri(); if (!text.getConversation().getParticipant().equals(participant)) { throw new RuntimeException("accountMessageStatusChanged: received an invalid text message"); } @@ -251,4 +246,5 @@ public abstract class HistoryService { return msg; }).subscribeOn(scheduler); } + } \ No newline at end of file diff --git a/ring-android/libringclient/src/main/java/cx/ring/services/NotificationService.java b/ring-android/libringclient/src/main/java/cx/ring/services/NotificationService.java index d945a248cc5c50b0fdc927d5bf391bd26fc37c68..2ae71a52cad4149c195263c8671ccb3b8e63eb3f 100644 --- a/ring-android/libringclient/src/main/java/cx/ring/services/NotificationService.java +++ b/ring-android/libringclient/src/main/java/cx/ring/services/NotificationService.java @@ -44,7 +44,7 @@ public interface NotificationService { void cancelTextNotification(Uri contact); - void cancelTextNotification(String ringId); + void cancelTextNotification(String accountId, Uri conversationUri); void cancelAll(); @@ -52,7 +52,7 @@ public interface NotificationService { void cancelTrustRequestNotification(String accountID); - void showFileTransferNotification(DataTransfer info, CallContact contact); + void showFileTransferNotification(Conversation conversation, DataTransfer info); void showMissedCallNotification(SipCall call); @@ -64,7 +64,7 @@ public interface NotificationService { void handleCallNotification(Conference conference, boolean remove); - void handleDataTransferNotification(DataTransfer transfer, CallContact contact, boolean remove); + void handleDataTransferNotification(DataTransfer transfer, Conversation contact, boolean remove); void removeTransferNotification(long transferId); diff --git a/ring-android/libringclient/src/main/java/cx/ring/services/VCardService.java b/ring-android/libringclient/src/main/java/cx/ring/services/VCardService.java index a07081f898a661436a0e92df6eeb7281063fe231..46fe2a84b772f97216c2f74552a176bdf12c3cb9 100644 --- a/ring-android/libringclient/src/main/java/cx/ring/services/VCardService.java +++ b/ring-android/libringclient/src/main/java/cx/ring/services/VCardService.java @@ -38,9 +38,6 @@ public abstract class VCardService { public abstract Single<Tuple<String, Object>> loadVCardProfile(VCard vcard); public abstract Single<Tuple<String, Object>> peerProfileReceived(String accountId, String peerId, File vcard); - public abstract void migrateContact(Map<String, CallContact> contacts, String accountId); - public abstract void migrateProfiles(List<String> accountIds); - public abstract void deleteLegacyProfiles(); public abstract Object base64ToBitmap(String base64); } diff --git a/ring-android/libringclient/src/main/java/cx/ring/smartlist/SmartListPresenter.java b/ring-android/libringclient/src/main/java/cx/ring/smartlist/SmartListPresenter.java index 72530d30d52ee4964d74b129cda22a334d1f4ba9..315b9e46938c0df25dd6198cc26f0033935e12a1 100644 --- a/ring-android/libringclient/src/main/java/cx/ring/smartlist/SmartListPresenter.java +++ b/ring-android/libringclient/src/main/java/cx/ring/smartlist/SmartListPresenter.java @@ -103,7 +103,7 @@ public class SmartListPresenter extends RootPresenter<SmartListView> { } public void conversationClicked(SmartListViewModel viewModel) { - startConversation(viewModel.getAccountId(), viewModel.getContact()); + startConversation(viewModel.getAccountId(), viewModel.getUri()); } public void conversationLongClicked(SmartListViewModel smartListViewModel) { @@ -118,10 +118,11 @@ public class SmartListPresenter extends RootPresenter<SmartListView> { getView().displayMenuItem(); } - private void startConversation(String accountId, CallContact c) { + private void startConversation(String accountId, Uri conversationUri) { + Log.w(TAG, "startConversation " + accountId + " " + conversationUri); SmartListView view = getView(); - if (view != null && c != null) { - view.goToConversation(accountId, c.getPrimaryUri()); + if (view != null && conversationUri != null) { + view.goToConversation(accountId, conversationUri); } } @@ -130,26 +131,26 @@ public class SmartListPresenter extends RootPresenter<SmartListView> { } public void copyNumber(SmartListViewModel smartListViewModel) { - getView().copyNumber(smartListViewModel.getContact()); + getView().copyNumber(smartListViewModel.getUri()); } public void clearConversation(SmartListViewModel smartListViewModel) { - getView().displayClearDialog(smartListViewModel.getContact()); + getView().displayClearDialog(smartListViewModel.getUri()); } - public void clearConversation(final CallContact callContact) { + public void clearConversation(final Uri uri) { mConversationDisposable.add(mConversationFacade - .clearHistory(mAccount.getAccountID(), callContact.getPrimaryUri()) + .clearHistory(mAccount.getAccountID(), uri) .subscribeOn(Schedulers.computation()).subscribe()); } public void removeConversation(SmartListViewModel smartListViewModel) { - getView().displayDeleteDialog(smartListViewModel.getContact()); + getView().displayDeleteDialog(smartListViewModel.getUri()); } - public void removeConversation(CallContact callContact) { + public void removeConversation(Uri uri) { mConversationDisposable.add(mConversationFacade - .removeConversation(mAccount.getAccountID(), callContact.getPrimaryUri()) + .removeConversation(mAccount.getAccountID(), uri) .subscribeOn(Schedulers.computation()).subscribe()); } @@ -169,7 +170,7 @@ public class SmartListPresenter extends RootPresenter<SmartListView> { vms.add((SmartListViewModel) ob); return vms; })) - //.throttleLatest(150, TimeUnit.MILLISECONDS, mUiScheduler) + .throttleLatest(150, TimeUnit.MILLISECONDS, mUiScheduler) .observeOn(mUiScheduler) .subscribe(viewModels -> { final SmartListView view = getView(); @@ -189,7 +190,8 @@ public class SmartListPresenter extends RootPresenter<SmartListView> { } public void banContact(SmartListViewModel smartListViewModel) { - CallContact contact = smartListViewModel.getContact(); - mAccountService.removeContact(mAccount.getAccountID(), contact.getPrimaryNumber(), true); + //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/cx/ring/smartlist/SmartListView.java b/ring-android/libringclient/src/main/java/cx/ring/smartlist/SmartListView.java index 11178ee8feeacd1c0b0189a7392891a750e6d50d..f678c3cf0e3b3e538c34944135a78163ec6dc5d4 100644 --- a/ring-android/libringclient/src/main/java/cx/ring/smartlist/SmartListView.java +++ b/ring-android/libringclient/src/main/java/cx/ring/smartlist/SmartListView.java @@ -33,11 +33,11 @@ public interface SmartListView extends BaseView { void displayConversationDialog(SmartListViewModel smartListViewModel); - void displayClearDialog(CallContact callContact); + void displayClearDialog(Uri callContact); - void displayDeleteDialog(CallContact callContact); + void displayDeleteDialog(Uri callContact); - void copyNumber(CallContact callContact); + void copyNumber(Uri uri); void setLoading(boolean display); @@ -53,7 +53,7 @@ public interface SmartListView extends BaseView { void goToConversation(String accountId, Uri contactId); - void goToCallActivity(String accountId, String contactId); + void goToCallActivity(String accountId, Uri conversationUri, String contactId); void goToQRFragment(); diff --git a/ring-android/libringclient/src/main/java/cx/ring/smartlist/SmartListViewModel.java b/ring-android/libringclient/src/main/java/cx/ring/smartlist/SmartListViewModel.java index 7847976a6756f9cf27e08e99c48ce062d9bc089a..b9354bc12a400d4c1571e7655b90876b9ab2a2e0 100644 --- a/ring-android/libringclient/src/main/java/cx/ring/smartlist/SmartListViewModel.java +++ b/ring-android/libringclient/src/main/java/cx/ring/smartlist/SmartListViewModel.java @@ -22,12 +22,12 @@ package cx.ring.smartlist; import java.util.Collections; import java.util.List; +import java.util.Objects; import cx.ring.model.CallContact; import cx.ring.model.Conversation; import cx.ring.model.Interaction; import cx.ring.model.Uri; -import cx.ring.services.AccountService; import io.reactivex.Observable; import io.reactivex.Single; @@ -47,7 +47,7 @@ public class SmartListViewModel private boolean hasOngoingCall; private final boolean showPresence; - //private boolean isOnline = false; + private boolean isOnline = false; private boolean isChecked = false; private final Interaction lastEvent; @@ -61,7 +61,7 @@ public class SmartListViewModel public SmartListViewModel(String accountId, CallContact contact, Interaction lastEvent) { this.accountId = accountId; this.contact = Collections.singletonList(contact); - this.uri = contact.getPrimaryUri(); + this.uri = contact.getUri(); uuid = uri.getRawUriString(); this.contactName = contact.getDisplayName(); hasUnreadTextMessage = (lastEvent != null) && !lastEvent.isRead(); @@ -74,14 +74,14 @@ public class SmartListViewModel public SmartListViewModel(String accountId, CallContact contact, String id, Interaction lastEvent) { this.accountId = accountId; this.contact = Collections.singletonList(contact); - uri = contact.getPrimaryUri(); + uri = contact.getUri(); this.uuid = id; this.contactName = contact.getDisplayName(); hasUnreadTextMessage = (lastEvent != null) && !lastEvent.isRead(); this.hasOngoingCall = false; this.lastEvent = lastEvent; showPresence = true; - //isOnline = contact.isOnline(); + isOnline = contact.isOnline(); title = Title.None; } public SmartListViewModel(Conversation conversation, List<CallContact> contacts, boolean presence) { @@ -94,12 +94,20 @@ public class SmartListViewModel hasUnreadTextMessage = (lastEvent != null) && !lastEvent.isRead(); this.hasOngoingCall = false; this.lastEvent = lastEvent; + for (CallContact contact : contacts) { + if (contact.isUser()) + continue; + if (contact.isOnline()) { + isOnline = true; + break; + } + } //isOnline = contact.isOnline(); showPresence = presence; title = Title.None; } public SmartListViewModel(Conversation conversation, boolean presence) { - this(conversation, Collections.singletonList(conversation.getContact()), presence); + this(conversation, conversation.getContacts(), presence); } private SmartListViewModel(Title title) { @@ -118,8 +126,8 @@ public class SmartListViewModel return uri; } - public CallContact getContact() { - return contact == null ? null : contact.get(0); + public List<CallContact> getContact() { + return contact; } public String getContactName() { @@ -177,8 +185,8 @@ public class SmartListViewModel return other.getHeaderTitle() == getHeaderTitle() && (getHeaderTitle() != Title.None || (contact == other.contact - && contactName.equals(other.contactName) - //&& isOnline == other.isOnline + && Objects.equals(contactName, other.contactName) + && isOnline == other.isOnline && lastEvent == other.lastEvent && hasOngoingCall == other.hasOngoingCall && hasUnreadTextMessage == other.hasUnreadTextMessage)); diff --git a/ring-android/libringclient/src/main/java/cx/ring/utils/StringUtils.java b/ring-android/libringclient/src/main/java/cx/ring/utils/StringUtils.java index 96701d49d01ef3a5c36f5c310b66359bdbabaeda..9756fa63a706476b1cacbe17d6eb77a15842d6f9 100644 --- a/ring-android/libringclient/src/main/java/cx/ring/utils/StringUtils.java +++ b/ring-android/libringclient/src/main/java/cx/ring/utils/StringUtils.java @@ -23,6 +23,7 @@ package cx.ring.utils; import java.util.Arrays; import java.util.HashSet; import java.util.Iterator; +import java.util.List; public final class StringUtils { @@ -120,4 +121,35 @@ public final class StringUtils { return true; } + public static String join(String separator, List<String> values) { + if (values.isEmpty()) return "";//need at least one element + if (values.size() == 1) return values.get(0); + //all string operations use a new array, so minimize all calls possible + char[] sep = separator.toCharArray(); + + // determine final size and normalize nulls + int totalSize = (values.size() - 1) * sep.length;// separator size + for (int i = 0; i < values.size(); i++) { + totalSize += values.get(i).length(); + } + + //exact size; no bounds checks or resizes + char[] joined = new char[totalSize]; + int pos = 0; + //note, we are iterating all the elements except the last one + for (int i = 0, end = values.size()-1; i < end; i++) { + System.arraycopy(values.get(i).toCharArray(), 0, + joined, pos, values.get(i).length()); + pos += values.get(i).length(); + System.arraycopy(sep, 0, joined, pos, sep.length); + pos += sep.length; + } + //now, add the last element; + //this is why we checked values.length == 0 off the hop + System.arraycopy(values.get(values.size()-1).toCharArray(), 0, + joined, pos, values.get(values.size()-1).length()); + + return new String(joined); + } + }