From c41033e83ce5bb3b583853d19f2adcc78d3b48e6 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 25 Apr 2020 20:13:08 +0200 Subject: [PATCH 01/19] only take udp candidates from transport-info --- .../conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java index 1e7ada424..a56f60d93 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java @@ -194,6 +194,9 @@ public class IceUdpTransportInfo extends GenericTransportInfo { checkNotNullNoWhitespace(component, "component"); final String transport = this.getAttribute("protocol"); checkNotNullNoWhitespace(transport, "protocol"); + if (!"udp".equals(transport)) { + throw new IllegalArgumentException(String.format("'%s' is not a supported protocol", transport)); + } final String priority = this.getAttribute("priority"); checkNotNullNoWhitespace(priority, "priority"); final String connectionAddress = this.getAttribute("ip"); From 006d7447a3f8a3c84638b77ac6a66adf9d24325c Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 25 Apr 2020 20:13:20 +0200 Subject: [PATCH 02/19] put version code into crash report --- .../conversations/utils/ExceptionHelper.java | 151 +++++++++--------- 1 file changed, 77 insertions(+), 74 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java b/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java index 8defc9976..e9f3bcb24 100644 --- a/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java @@ -29,82 +29,85 @@ import eu.siacs.conversations.ui.XmppActivity; public class ExceptionHelper { - private static final String FILENAME = "stacktrace.txt"; - private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH); + private static final String FILENAME = "stacktrace.txt"; + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH); - public static void init(Context context) { - if (!(Thread.getDefaultUncaughtExceptionHandler() instanceof ExceptionHandler)) { - Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler( - context)); - } - } + public static void init(Context context) { + if (!(Thread.getDefaultUncaughtExceptionHandler() instanceof ExceptionHandler)) { + Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler( + context)); + } + } - public static boolean checkForCrash(XmppActivity activity) { - try { - final XmppConnectionService service = activity == null ? null : activity.xmppConnectionService; - if (service == null) { - return false; - } - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity); - boolean neverSend = preferences.getBoolean("never_send", false); - if (neverSend || Config.BUG_REPORTS == null) { - return false; - } - final Account account = AccountUtils.getFirstEnabled(service); - if (account == null) { - return false; - } - FileInputStream file = activity.openFileInput(FILENAME); - InputStreamReader inputStreamReader = new InputStreamReader(file); - BufferedReader stacktrace = new BufferedReader(inputStreamReader); - final StringBuilder report = new StringBuilder(); - PackageManager pm = activity.getPackageManager(); - PackageInfo packageInfo; - try { - packageInfo = pm.getPackageInfo(activity.getPackageName(), PackageManager.GET_SIGNATURES); - report.append("Version: ").append(packageInfo.versionName).append('\n'); - report.append("Last Update: ").append(DATE_FORMAT.format(new Date(packageInfo.lastUpdateTime))).append('\n'); - Signature[] signatures = packageInfo.signatures; - if (signatures != null && signatures.length >= 1) { - report.append("SHA-1: ").append(CryptoHelper.getFingerprintCert(packageInfo.signatures[0].toByteArray())).append('\n'); - } - report.append('\n'); - } catch (Exception e) { - e.printStackTrace(); - return false; - } - String line; - while ((line = stacktrace.readLine()) != null) { - report.append(line); - report.append('\n'); - } - file.close(); - activity.deleteFile(FILENAME); - AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setTitle(activity.getString(R.string.crash_report_title)); - builder.setMessage(activity.getText(R.string.crash_report_message)); - builder.setPositiveButton(activity.getText(R.string.send_now), (dialog, which) -> { + public static boolean checkForCrash(XmppActivity activity) { + try { + final XmppConnectionService service = activity == null ? null : activity.xmppConnectionService; + if (service == null) { + return false; + } + final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity); + boolean neverSend = preferences.getBoolean("never_send", false); + if (neverSend || Config.BUG_REPORTS == null) { + return false; + } + final Account account = AccountUtils.getFirstEnabled(service); + if (account == null) { + return false; + } + FileInputStream file = activity.openFileInput(FILENAME); + InputStreamReader inputStreamReader = new InputStreamReader(file); + BufferedReader stacktrace = new BufferedReader(inputStreamReader); + final StringBuilder report = new StringBuilder(); + PackageManager pm = activity.getPackageManager(); + PackageInfo packageInfo; + try { + packageInfo = pm.getPackageInfo(activity.getPackageName(), PackageManager.GET_SIGNATURES); + final String versionName = packageInfo.versionName; + final int versionCode = packageInfo.versionCode; + final int version = versionCode > 10000 ? (versionCode / 100) : versionCode; + report.append(String.format(Locale.ROOT, "Version: %s(%d)", versionName, version)).append('\n'); + report.append("Last Update: ").append(DATE_FORMAT.format(new Date(packageInfo.lastUpdateTime))).append('\n'); + Signature[] signatures = packageInfo.signatures; + if (signatures != null && signatures.length >= 1) { + report.append("SHA-1: ").append(CryptoHelper.getFingerprintCert(packageInfo.signatures[0].toByteArray())).append('\n'); + } + report.append('\n'); + } catch (Exception e) { + e.printStackTrace(); + return false; + } + String line; + while ((line = stacktrace.readLine()) != null) { + report.append(line); + report.append('\n'); + } + file.close(); + activity.deleteFile(FILENAME); + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setTitle(activity.getString(R.string.crash_report_title)); + builder.setMessage(activity.getText(R.string.crash_report_message)); + builder.setPositiveButton(activity.getText(R.string.send_now), (dialog, which) -> { - Log.d(Config.LOGTAG, "using account=" + account.getJid().asBareJid() + " to send in stack trace"); - Conversation conversation = service.findOrCreateConversation(account, Config.BUG_REPORTS, false, true); - Message message = new Message(conversation, report.toString(), Message.ENCRYPTION_NONE); - service.sendMessage(message); - }); - builder.setNegativeButton(activity.getText(R.string.send_never), (dialog, which) -> preferences.edit().putBoolean("never_send", true).apply()); - builder.create().show(); - return true; - } catch (final IOException ignored) { - return false; - } - } + Log.d(Config.LOGTAG, "using account=" + account.getJid().asBareJid() + " to send in stack trace"); + Conversation conversation = service.findOrCreateConversation(account, Config.BUG_REPORTS, false, true); + Message message = new Message(conversation, report.toString(), Message.ENCRYPTION_NONE); + service.sendMessage(message); + }); + builder.setNegativeButton(activity.getText(R.string.send_never), (dialog, which) -> preferences.edit().putBoolean("never_send", true).apply()); + builder.create().show(); + return true; + } catch (final IOException ignored) { + return false; + } + } - static void writeToStacktraceFile(Context context, String msg) { - try { - OutputStream os = context.openFileOutput(FILENAME, Context.MODE_PRIVATE); - os.write(msg.getBytes()); - os.flush(); - os.close(); - } catch (IOException ignored) { - } - } + static void writeToStacktraceFile(Context context, String msg) { + try { + OutputStream os = context.openFileOutput(FILENAME, Context.MODE_PRIVATE); + os.write(msg.getBytes()); + os.flush(); + os.close(); + } catch (IOException ignored) { + } + } } From 9fbf73d1ea7d587b4c4dcad73e25fc428eeae521 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 26 Apr 2020 10:38:19 +0200 Subject: [PATCH 03/19] do not log failed calls that never rang --- .../conversations/xmpp/jingle/JingleRtpConnection.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 0052228b1..0dcac3326 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -657,9 +657,12 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } private void sendSessionTerminate(final Reason reason, final String text) { + final State previous = this.state; final State target = reasonToState(reason); transitionOrThrow(target); - writeLogMessage(target); + if (previous != State.NULL) { + writeLogMessage(target); + } final JinglePacket jinglePacket = new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId); jinglePacket.setReason(reason, text); Log.d(Config.LOGTAG, jinglePacket.toString()); @@ -672,7 +675,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web try { final RtpContentMap rtpContentMap = isInitiator() ? this.initiatorRtpContentMap : this.responderRtpContentMap; transportInfo = rtpContentMap.transportInfo(contentName, candidate); - } catch (Exception e) { + } catch (final Exception e) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to prepare transport-info from candidate for content=" + contentName); return; } From 07911b2094696b17c56fbe5ff6afc8dfc8326c88 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 27 Apr 2020 11:53:31 +0200 Subject: [PATCH 04/19] indicate ongoing call. fixes #3675 --- .../ui/ConversationFragment.java | 34 ++++++++++++-- .../ui/adapter/ConversationAdapter.java | 44 ++++++++++++------ .../xmpp/jingle/JingleConnectionManager.java | 18 +++++-- .../ic_phone_in_talk_black_18dp.png | Bin 0 -> 374 bytes .../ic_phone_in_talk_white_18dp.png | Bin 0 -> 393 bytes .../ic_phone_in_talk_white_24dp.png | Bin 0 -> 483 bytes .../ic_phone_in_talk_black_18dp.png | Bin 0 -> 253 bytes .../ic_phone_in_talk_white_18dp.png | Bin 0 -> 261 bytes .../ic_phone_in_talk_white_24dp.png | Bin 0 -> 325 bytes .../ic_phone_in_talk_black_18dp.png | Bin 0 -> 477 bytes .../ic_phone_in_talk_white_18dp.png | Bin 0 -> 483 bytes .../ic_phone_in_talk_white_24dp.png | Bin 0 -> 601 bytes .../ic_phone_in_talk_black_18dp.png | Bin 0 -> 685 bytes .../ic_phone_in_talk_white_18dp.png | Bin 0 -> 704 bytes .../ic_phone_in_talk_white_24dp.png | Bin 0 -> 882 bytes .../ic_phone_in_talk_black_18dp.png | Bin 0 -> 868 bytes .../ic_phone_in_talk_white_18dp.png | Bin 0 -> 882 bytes .../ic_phone_in_talk_white_24dp.png | Bin 0 -> 1162 bytes src/main/res/menu/fragment_conversation.xml | 6 +++ src/main/res/values/attrs.xml | 2 + src/main/res/values/strings.xml | 1 + src/main/res/values/themes.xml | 4 ++ 22 files changed, 89 insertions(+), 20 deletions(-) create mode 100644 src/main/res/drawable-hdpi/ic_phone_in_talk_black_18dp.png create mode 100644 src/main/res/drawable-hdpi/ic_phone_in_talk_white_18dp.png create mode 100644 src/main/res/drawable-hdpi/ic_phone_in_talk_white_24dp.png create mode 100644 src/main/res/drawable-mdpi/ic_phone_in_talk_black_18dp.png create mode 100644 src/main/res/drawable-mdpi/ic_phone_in_talk_white_18dp.png create mode 100644 src/main/res/drawable-mdpi/ic_phone_in_talk_white_24dp.png create mode 100644 src/main/res/drawable-xhdpi/ic_phone_in_talk_black_18dp.png create mode 100644 src/main/res/drawable-xhdpi/ic_phone_in_talk_white_18dp.png create mode 100644 src/main/res/drawable-xhdpi/ic_phone_in_talk_white_24dp.png create mode 100644 src/main/res/drawable-xxhdpi/ic_phone_in_talk_black_18dp.png create mode 100644 src/main/res/drawable-xxhdpi/ic_phone_in_talk_white_18dp.png create mode 100644 src/main/res/drawable-xxhdpi/ic_phone_in_talk_white_24dp.png create mode 100644 src/main/res/drawable-xxxhdpi/ic_phone_in_talk_black_18dp.png create mode 100644 src/main/res/drawable-xxxhdpi/ic_phone_in_talk_white_18dp.png create mode 100644 src/main/res/drawable-xxxhdpi/ic_phone_in_talk_white_24dp.png diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 0f47f6936..e8016fc01 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -52,6 +52,8 @@ import android.widget.PopupMenu; import android.widget.TextView.OnEditorActionListener; import android.widget.Toast; +import com.google.common.base.Optional; + import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -117,6 +119,7 @@ import eu.siacs.conversations.utils.TimeframeUtils; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.chatstate.ChatState; +import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection; import eu.siacs.conversations.xmpp.jingle.JingleFileTransferConnection; import eu.siacs.conversations.xmpp.jingle.RtpCapability; import rocks.xmpp.addr.Jid; @@ -956,6 +959,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke final MenuItem menuMute = menu.findItem(R.id.action_mute); final MenuItem menuUnmute = menu.findItem(R.id.action_unmute); final MenuItem menuCall = menu.findItem(R.id.action_call); + final MenuItem menuOngoingCall = menu.findItem(R.id.action_ongoing_call); final MenuItem menuVideoCall = menu.findItem(R.id.action_video_call); @@ -965,10 +969,18 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke menuInviteContact.setVisible(conversation.getMucOptions().canInvite()); menuMucDetails.setTitle(conversation.getMucOptions().isPrivateAndNonAnonymous() ? R.string.action_muc_details : R.string.channel_details); menuCall.setVisible(false); + menuOngoingCall.setVisible(false); } else { - final RtpCapability.Capability rtpCapability = RtpCapability.check(conversation.getContact()); - menuCall.setVisible(rtpCapability != RtpCapability.Capability.NONE); - menuVideoCall.setVisible(rtpCapability == RtpCapability.Capability.VIDEO); + final Optional ongoingRtpSession = activity.xmppConnectionService.getJingleConnectionManager().getOngoingRtpConnection(conversation.getContact()); + if (ongoingRtpSession.isPresent()) { + menuOngoingCall.setVisible(true); + menuCall.setVisible(false); + } else { + menuOngoingCall.setVisible(false); + final RtpCapability.Capability rtpCapability = RtpCapability.check(conversation.getContact()); + menuCall.setVisible(rtpCapability != RtpCapability.Capability.NONE); + menuVideoCall.setVisible(rtpCapability == RtpCapability.Capability.VIDEO); + } menuContactDetails.setVisible(!this.conversation.withSelf()); menuMucDetails.setVisible(false); final XmppConnectionService service = activity.xmppConnectionService; @@ -1245,12 +1257,28 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke case R.id.action_video_call: checkPermissionAndTriggerVideoCall(); break; + case R.id.action_ongoing_call: + returnToOngoingCall(); + break; default: break; } return super.onOptionsItemSelected(item); } + private void returnToOngoingCall() { + final Optional ongoingRtpSession = activity.xmppConnectionService.getJingleConnectionManager().getOngoingRtpConnection(conversation.getContact()); + if (ongoingRtpSession.isPresent()) { + final AbstractJingleConnection.Id id = ongoingRtpSession.get(); + final Intent intent = new Intent(getActivity(), RtpSessionActivity.class); + intent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, id.account.getJid().asBareJid().toEscapedString()); + intent.putExtra(RtpSessionActivity.EXTRA_WITH, id.with.toEscapedString()); + intent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, id.sessionId); + startActivity(intent); + } + + } + private void checkPermissionAndTriggerAudioCall() { if (activity.mUseTor || conversation.getAccount().isOnion()) { Toast.makeText(activity, R.string.disable_tor_to_make_call, Toast.LENGTH_SHORT).show(); diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java index 3ea451653..dacfc8df0 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java @@ -9,11 +9,14 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import com.google.common.base.Optional; + import java.util.List; import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.ConversationListRowBinding; import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Conversational; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.ui.ConversationFragment; import eu.siacs.conversations.ui.XmppActivity; @@ -22,6 +25,7 @@ import eu.siacs.conversations.ui.util.StyledAttributes; import eu.siacs.conversations.utils.EmojiWrapper; import eu.siacs.conversations.utils.IrregularUnicodeDetector; import eu.siacs.conversations.utils.UIHelper; +import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection; import rocks.xmpp.addr.Jid; public class ConversationAdapter extends RecyclerView.Adapter { @@ -160,21 +164,35 @@ public class ConversationAdapter extends RecyclerView.Adapter= System.currentTimeMillis()) { - viewHolder.binding.notificationStatus.setVisibility(View.VISIBLE); - int ic_notifications_paused = activity.getThemeResource(R.attr.icon_notifications_paused, R.drawable.ic_notifications_paused_black_24dp); - viewHolder.binding.notificationStatus.setImageResource(ic_notifications_paused); - } else if (conversation.alwaysNotify()) { - viewHolder.binding.notificationStatus.setVisibility(View.GONE); + + final Optional ongoingCall; + if (conversation.getMode() == Conversational.MODE_MULTI) { + ongoingCall = Optional.absent(); } else { + ongoingCall = activity.xmppConnectionService.getJingleConnectionManager().getOngoingRtpConnection(conversation.getContact()); + } + + if (ongoingCall.isPresent()) { viewHolder.binding.notificationStatus.setVisibility(View.VISIBLE); - int ic_notifications_none = activity.getThemeResource(R.attr.icon_notifications_none, R.drawable.ic_notifications_none_black_24dp); - viewHolder.binding.notificationStatus.setImageResource(ic_notifications_none); + final int ic_ongoing_call = activity.getThemeResource(R.attr.ic_ongoing_call_hint, R.drawable.ic_phone_in_talk_black_18dp); + viewHolder.binding.notificationStatus.setImageResource(ic_ongoing_call); + } else { + final long muted_till = conversation.getLongAttribute(Conversation.ATTRIBUTE_MUTED_TILL, 0); + if (muted_till == Long.MAX_VALUE) { + viewHolder.binding.notificationStatus.setVisibility(View.VISIBLE); + int ic_notifications_off = activity.getThemeResource(R.attr.icon_notifications_off, R.drawable.ic_notifications_off_black_24dp); + viewHolder.binding.notificationStatus.setImageResource(ic_notifications_off); + } else if (muted_till >= System.currentTimeMillis()) { + viewHolder.binding.notificationStatus.setVisibility(View.VISIBLE); + int ic_notifications_paused = activity.getThemeResource(R.attr.icon_notifications_paused, R.drawable.ic_notifications_paused_black_24dp); + viewHolder.binding.notificationStatus.setImageResource(ic_notifications_paused); + } else if (conversation.alwaysNotify()) { + viewHolder.binding.notificationStatus.setVisibility(View.GONE); + } else { + viewHolder.binding.notificationStatus.setVisibility(View.VISIBLE); + int ic_notifications_none = activity.getThemeResource(R.attr.icon_notifications_none, R.drawable.ic_notifications_none_black_24dp); + viewHolder.binding.notificationStatus.setImageResource(ic_notifications_none); + } } long timestamp; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 4c410a5be..a6c14a384 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -1,19 +1,16 @@ package eu.siacs.conversations.xmpp.jingle; -import android.os.SystemClock; import android.util.Base64; import android.util.Log; -import com.google.common.base.Function; import com.google.common.base.Objects; +import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableSet; -import org.checkerframework.checker.nullness.compatqual.NullableDecl; - import java.lang.ref.WeakReference; import java.security.SecureRandom; import java.util.Collection; @@ -108,6 +105,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { return; } connections.put(id, connection); + mXmppConnectionService.updateConversationUi(); connection.deliverPacket(packet); } else { Log.d(Config.LOGTAG, "unable to route jingle packet: " + packet); @@ -353,6 +351,18 @@ public class JingleConnectionManager extends AbstractConnectionManager { connection.init(message); } + public Optional getOngoingRtpConnection(final Contact contact) { + for(final Map.Entry entry : this.connections.entrySet()) { + if (entry.getValue() instanceof JingleRtpConnection) { + final AbstractJingleConnection.Id id = entry.getKey(); + if (id.account == contact.getAccount() && id.with.asBareJid().equals(contact.getJid().asBareJid())) { + return Optional.of(id); + } + } + } + return Optional.absent(); + } + void finishConnection(final AbstractJingleConnection connection) { this.connections.remove(connection.getId()); } diff --git a/src/main/res/drawable-hdpi/ic_phone_in_talk_black_18dp.png b/src/main/res/drawable-hdpi/ic_phone_in_talk_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..37bf2abc3fb59959aeb099269243022b827016aa GIT binary patch literal 374 zcmV-+0g3*JP))Z}fTR-}c%~#yzxDuWt-6?Y`@d>98necL0^tTQ_AJeJNW2 z%y{LC)O%u-wp&v68j3UChWYg%5EXdH0Bln-@y7Ly{5YPj5pfW6klTkus}(=hClc&6#1_+uZI#XT*v zV#_2@98{_FFrzZ9WynXdp=Ur7eu-tn1~ez)sS@I_llIW*@$9Er{mV8Y{wZxuGknbE9oWA{gARnJ2v8!SvNhf;E3JhKX!Gk US`J97$N&HU07*qoM6N<$f@&bAcmMzZ literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-hdpi/ic_phone_in_talk_white_18dp.png b/src/main/res/drawable-hdpi/ic_phone_in_talk_white_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..ecb24eb76071ee89345a6ae9573eb50f03e83589 GIT binary patch literal 393 zcmV;40e1e0P)ltVm53j+@E@T7t<4Uc5?oU^Sn>zSLaRt(WC8-jod&>%B{NPvV(aJm6s6!u9sZ} znCkwG4FE7KyERXJ0Nl6pPpl7s=h8yQy#TK>4D2ERK9^ln`B2X<+!L8&0H9Ou_R}jT z^Lz!Ij%?Xi0H5qF47T+0X^cr^$zug+m(6ZnZ!$a~Spf$kI~P#Sb`!kJ^G95YY{@%3 zltO3l!w$(NZbp`@W6MfldsHe-$(qWl$TYmgOf$exWJ$+2Y+9<#N*BNbi*TPKwnY}Z z2Apf)QtoHwh>B0cMoF8~Bq5XAP=wGnk)2=WAbF;4100000NkvXXu0mjf@BFOy literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-hdpi/ic_phone_in_talk_white_24dp.png b/src/main/res/drawable-hdpi/ic_phone_in_talk_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..f2c89424ef6c4a8fc641b695d15aea61138985b7 GIT binary patch literal 483 zcmV<90UZ8`P)Ky0J#aZ%sZ_et~>HCY{-~XxUHbz0?FOs4oWf$?u~mwAlq;0M}YWdla`( zD7MLCz)&lwOL50VR^2gcv+rf5z=G~_D9p)kRAfQJ4eK)M33#Pe4&9XBnH&V~?8vV5 z6&x;y^5?!HQkOgj%<0PB5$=~m9lis&DhloQ35*G0izXUYltE#t>~T{EKqEW*4A@%^ z?E*h+6Qxaqqe2)5C(EJm2;3H>T?RwamDF27-J0N___IQ>G4S5{s!*p{Fl~7?DC{;5 z?pj(63Il+9g}++jtO>ogwuMsX0WWNj7?S`#d*hi&tJ^{09N>dPiTQPd?V&JW9&pF@ zQihQ>P&)gpv?@^OG^$wktCG`aOc#Dh6f>p4e;zocrr)ss-6x2h0@rkd6{87Rp7>!uAZ?Z z)32u?co!aZl|mnYj&I=zz`C%h@asfoUswddhikmJm6MEL6eTl3DO)~~YZ4-1X+jHsOwu$-&!?jH5c^!n300000NkvXXu0mjf DB1dNb literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-mdpi/ic_phone_in_talk_white_18dp.png b/src/main/res/drawable-mdpi/ic_phone_in_talk_white_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..833386bbcd7d403260e5f4e17ea79de10ae630a8 GIT binary patch literal 261 zcmV+g0s8)lP)5>2 zzb*p-901^-G}VbW;01Is*(9~{5C-W&3`Qx9L?W@_1sKEwkoc2}TudYeiFgBQZNltx zan3bGr+#0=P+5~oC8i?d!oIoa=SprVGH$)$#zgdTBDWP^)HHQHn~ajSToM^)R#eq> zrBTw5Ye&W-u2nVBh=Ph&?8G;VSXT3HFG@Bs9X~udjd15YO2&2E8;!v%;Lu16riCN5 z$k>(I)>Myv)_r&|<3J&a-^_c_D~-Px)6!8$<3B29Z8&ymHHPXFyN1GZ*_DqY00000 LNkvXXu0mjfAe3xu literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-mdpi/ic_phone_in_talk_white_24dp.png b/src/main/res/drawable-mdpi/ic_phone_in_talk_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..e6f98af95bf0027481d1c66ab3fc99354458dd8e GIT binary patch literal 325 zcmV-L0lNN)P)Y5a?hDBK78)1nDb?lTJ#_HAxQD2M9OC$xk3SiHJBy*jivDgy5nLNmDe1 zgogHfa?~p~@AY4fo|D{GR-I}ZPQhPW<_+a?4wWWTQ2MMl7ui;tNkO|lZ~VZHft+Vu zX(bgpEn-J+>dY%Ob>^5oA3aI5S;VK*d7<>hog8y&I&s%e45muMp%ZJmQ*h5K`xr@# zV=i^x;j8Xcc#RK<$M}{y1K8G+3SIb<=)iu?^BxPiIgjjOEH~#l4m{02+I8hRCUM|p z3K}-FteQ1tIF~cw#FG0OHl1naDxTTJZ{OAAY93m4L9VCYTP+p&KM#zWv*wqUmE2ZG XfRjN?g6O-M00000NkvXXu0mjfGVqI( literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xhdpi/ic_phone_in_talk_black_18dp.png b/src/main/res/drawable-xhdpi/ic_phone_in_talk_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..0eea8bcb7a6ccbbd600b177752f0f24743a37e07 GIT binary patch literal 477 zcmV<30V4j1P)fwpqNS<5H*1K7T7oEu8tes^2qXxnULg=FAsVV#OC?BXh+3i{ zihdwXW&H=C4K-9y6l8gWXSm4Wpcn4y=UF`WfiK>3Rdp*g^cyZ`P=}Hs=Nz(LCH)GE zXPi4m4v=Z^|kM=S$C7rh4icWWDn@^ zGtS4WT=xwn*Sr=_&FKasae6GP!|yV4Q|hp9u^9t9EX3=slQp&E$2}tMN*9=k(?MDL zV)@T5dDF2!3C82+v+_KP^?iBQ%fBGvmbB$*ihl%0)ixHP&$g(k(`=-styR;0^O1UJ zwVDPLF^`hpYL!bS^x3JhDKy0J#aZ%sZ_et~>HCY{-~XxUHbz0?FOs4oWf$?u~mwAlq;0M}YWdla`( zD7MLCz)&lwOL50VR^2gcv+rf5z=G~_D9p)kRAfQJ4eK)M33#Pe4&9XBnH&V~?8vV5 z6&x;y^5?!HQkOgj%<0PB5$=~m9lis&DhloQ35*G0izXUYltE#t>~T{EKqEW*4A@%^ z?E*h+6Qxaqqe2)5C(EJm2;3H>T?RwamDF27-J0N___IQ>G4S5{s!*p{Fl~7?DC{;5 z?pj(63Il+9g}++jtO>ogwuMsX0WWNj7?S`#d*hi&tJ^{09N>dPiTQPd?V&JW9&pF@ zQihQ>P&)gpv?@^OG^$w2+&ocHoAp8q}GJ>GNwyVJYvJ|C&3rlJ@iiq{JL@?f2x%rnh(4yas2 z$rqIex!!P*29=H!e(IEi%@*lXsrZebX63-o1DaF{zTu}`IcQ`rCm7}hX|mq2N5!## zpHuQh2NR@lEz+i9c#5AJDu^IUxZbi;c8uZYkt}Fo7T0~*aTPx+)N2Fld}fBr1eC!J zW^tX74PE#bmM}ijpu(u*2l@WcrVZp8r(O(aaiI2}1HkHxZ04G|Is%jBB!C5?6vj6~s~E z7?&=A(XDDAL>eQ?5&2?2K^nAGjN*#Xt2~5> z12JY8rjHJGQ?K$chAYmHJP4B{-wORI2V-PS(W(r>B=Hwjc^D;)E5d;OBTSNlR4ziS zk~PN&Ni0-8I{84!v=uaRi*-t(t)i1zN}%oFB+n^A+eMHGRw+o8z%D{OBuc(zRT77| n!X2J4&sTntWSO(7si~-co;IymT`&Bo00000NkvXXu0mjf&bbB- literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxhdpi/ic_phone_in_talk_black_18dp.png b/src/main/res/drawable-xxhdpi/ic_phone_in_talk_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..ed36763d8b83cd0454587725b217f3d3ab42e2be GIT binary patch literal 685 zcmV;e0#f~nP)j0tpmlCW6Y>F5FbpDq7Vp#Vpc{);+c@qK9P>(xQb# z<)$Eu+J-HxKtjSw7iBqi+gv_A?%dOv2Pa(2@4x%_nBP6;eCG^Hm=Y!GNTHLYhHRSH z!CH#sI(|g@2@%w1c+6=Mg|5nvOooamndB9RiIXQS_=apW$t3S-BQJIvW!ud%l_AQ> znS@c+0%-PekZy+f9O|2(h8$UElpP`(k3HODQFTtUlbne0%OD%H4AwKP%XM`V2H7M2 zjmIe#)Ml20oreb5H{xiTs0me|gCEj=8#@f*8`7LLjdP69iHATZ8R>7uN{3O_i$-7Q zT?q83t&C%#ibaF0%XrV`IU;qBG3x{?UPh{J;h8Tre@GKRy?pLCR?bGg4B;)=&k3IB z)v4QcU#hiOsLfK{G46f~pOCYYweOW$4_01l6?chaW&7X)-mE%e^Y@Mm3 z^gfWMi2(q;@&Q#?xggD#uyaznHvs@$QmMmA6|+I}0tsv! zke_apS=ALQXQcf(F&wj!4|;%I(=M{#G~%3_qMKqpj;Ul)bZ;ofF%8U%Zj3SAKzWe zG^;t!jBA;ujAM+umua@r#|z_RCU8I{tqd^2JANwFH5}uyhHW&^OoAm#i4uPSgM+m> TUhnp>00000NkvXXu0mjfTx~b_ literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxhdpi/ic_phone_in_talk_white_18dp.png b/src/main/res/drawable-xxhdpi/ic_phone_in_talk_white_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..ec67c6ef906c2a9c5873d795e5190bcfb143ae9a GIT binary patch literal 704 zcmV;x0zdtUP)zIv`EWLfq?{u;)|fgF#l^+NrI}hyzy{Vb zU!~y^VY>7WGCuKuBg8Zr#0i(8N&y7*vkOPXp^03UD1eDx)1qRqo?KQdfN(ujs!+tp zrAYy7W;=)I;R8Vf)T%HnAeWsAMTCvqVhqm^bt(i=eh}`I@*vJ*JVVr~5DXCRp7J8X zK}InKsFWRd3HJp@K5U>CM*+0)4da^ZI7Kdv^5L%$?$Ay|<7g*^u}wC#k;`HE;OS?b z#?gr}fTR4VVT^D)6q8=Otu!a3R z#FL~>StKx4DL+>6uj@DpMHBBZ`iUrwO&FcB;W>UcaFi2^d5h7iG@=ZVabGsE z6{A6EY{6)d9mg>Gh^oAp#T$%Hl^d-XKWR|8aRuW&i&S1zGl=nmN|hJ&Br$rKt@2_I zo?a>y3Wp9(;4(v{p&QRj7HI-1NRTnYea_H<%NmwZt>SS3&wCmaKm`fHrRY+zh;Say zPYw}P78NAOXSs?;H-3G@^#>Is$gf$G!5)%$Mmfhq64$*B??Bid2L zU?H10M!CJrkv%>pT1koUFy9fhmz->|lQ`Ea5gghW$8WoA@gi|HC^4ED#IIF$SVx>r zSzs|oFpaTXHdsoW6Dq?+&T@nuv@llzT+TO``elc+#A#9ho>4kzln;v;#PqamaF95U zD*(S?TIItXjN=(1ujF`%I4c!Ext%yNum#hzN{*F8+oLjsIe;S<4&UQBMNY|ahS}}I zQ5nL$B?Id)-KXT}W;Q)I3PGMGHu5P!tuiox=WQj&{em5Z%B;rW#-RB!CSv`_WM0){YcQoXSr(*$>`-q?j{ zn445z6!-ztAWKwV+{_rJ<1A8r(ZV#Q<1A4uR`C=oR2#J6_Y+Nuz&t+Te?RgjPjI8s z#_RZvuu8LF9(@FjF~C8(d5u@t$_=U>yYZXkIfbA=A8{_QTGb-QZi4o+P(BpsBZ&c} zfj0;m;t{zh&`UDYN(XIB6V$_X`VR$qNla;>g+B@Ui*~N&3oIx-tl$LY{=tgUM1d}< zq_puUzflFHj|CK&qC%<-ZsT(*qS|3CUt>+R#l3vM3>H*-H1R5bkdQ77E@KT{3=yZK zON-mt%KHp6n@-gS*YXfsDDojkImJ2tW{MJ>Cv9H}}!od(`&MR7uB}x30w!~KXv3BPn95irfyfI}I?>e!zoxDNq z`UFni(w6Q~g`4-Z)oJoNwd->@X+vrA7;f&@)-I5Dxpw^r94(07iqFMGgLt3v&KBP; zM>nCgc@Y=o+RA;)bqBK|>1MoZ;y?@fku&jb)Huhyt0W_mdho7Y1nqGx-c1bl{!4TlpT@u6*YL)LC#33!>)yUY2=usP-4C#Ng=244(h-8mS?z|iwNCQM$#rN z1#&(_8MDJOGfeU%kJHbEgy1=pn0+23NQKA=o?TigS$*B??Bid2L zU?H10M!CJrkv%>pT1koUFy9fhmz->|lQ`Ea5gghW$8WoA@gi|HC^4ED#IIF$SVx>r zSzs|oFpaTXHdsoW6Dq?+&T@nuv@llzT+TO``elc+#A#9ho>4kzln;v;#PqamaF95U zD*(S?TIItXjN=(1ujF`%I4c!Ext%yNum#hzN{*F8+oLjsIe;S<4&UQBMNY|ahS}}I zQ5nL$B?Id)-KXT}W;Q)I3PGMGHu5P!tuiox=WQj&{em5Z%B;rW#-RB!CSv`_WM0){YcQoXSr(*$>`-q?j{ zn445z6!-ztAWKwV+{_rJ<1A8r(ZV#Q<1A4uR`C=oR2#J6_Y+Nuz&t+Te?RgjPjI8s z#_RZvuu8LF9(@FjF~C8(d5u@t$_=U>yYZXkIfbA=A8{_QTGb-QZi4o+P(BpsBZ&c} zfj0;m;t{zh&`UDYN(XIB6V$_X`VR$qNla;>g+B@Ui*~N&3oIx-tl$LY{=tgUM1d}< zq_puUzflFHj|CK&qC%<-ZsT(*qS|3CUt>+R#l3vM3>H*-H1R5bkdQ77E@KT{3=yZK zON-mt%KHp6n@-gS*YXfsDDojkImJ2tW{MJ>@G4~iIiQa^k$Y>Xjl|sLJ&gg?4l6K zk|@#B3@wqeBnq;!CSqM!GtHRW+0o5y?&aG>UHE@qj{oyK=bY`p^Z7`TNF)-8L?RIn z)R5pj>2%RdjnZO}9?sIjJ~s0N4UAHLP(unEDW)n7oGx;J=a{5iBY~}i(!kGQ9-~aT z!+C6VDGkzT<5k8fHyFfTk$iC_NnRwT?4t*JG5NyA@7%3yBZa+j@`bIfT&-;4EcT|z z7xs=(t?c1A_Uh$}7~`459G3DH-!niuDdsC%_y&6mWQnmn$Zq=Z)6WuR2fMMiS{b2| zb)3W75RWSx*n+)xl`+Qf65V(iVu`}z1?+vNK)8_vUiz7@uvmz_i1~k%<+Nk^TETIGD@oCyG>B2fJT~wL-Wuh=E0~6usGxYA^inj) z3Rkg+R@~YcF9Q>}gy|Ut#Wd1M(Vz@a&Q9Dm%Yg)@eF~0W@snbvGR8)nda06ug_yc2 zQBbVJTZ)+qfFeG^X}t`LV-QnZK{1|Q+)~U^KvZxFQ!7O>@B^mh3Xb=2N-;|zuo$Pf z3~a>owt`~@PM<0iiun`M8W~uQX|KZIAWm_G!kd^rmVvu49aR|2$LTAD!hM*2k%1|g z&L|wd#i>ytFcDLm3{1e(t#G&pr&h`p7^5-u$-rn#0}6|SIBilPVLAht2AQKm!aF#f zq(X&*3AEv~LxqG#a9gHA!hW23h^ugL14*1tQ>DT|BW_11SK(kAZl5zsg@Q7E#_bb| zRVcWL4%|MZM1_KT>BnssF=c>q7O{p#u2KH63@=|$AxqSAhW}jROV%=*(aI;@z{`(R z$pZCs;?&0xwy}mrW^*%@lv1QzVk=%w6W0|`PbYS|Xr@NFK@r>W(!+A4L7Yx9kz%TH zgJQPhZ3koJi8x6zl~ArwL=)ala;IFVBT44EluN9~+Yno*&}FD2Ng&ECmeP;6zj#>x z;|`Jpq+DYj?f5xFy+kb?1g6|$3P0g5K?flyA1LEJazy#ULORF+ + + + diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 48ca72cea..f7445bf86 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -919,6 +919,7 @@ Video call Your microphone is unavailable You can only have one call at a time. + Return to ongoing call View %1$d Participant View %1$d Participants diff --git a/src/main/res/values/themes.xml b/src/main/res/values/themes.xml index 2f8ecc434..b9e971cf1 100644 --- a/src/main/res/values/themes.xml +++ b/src/main/res/values/themes.xml @@ -99,6 +99,8 @@ @drawable/ic_attach_file_white_24dp @drawable/ic_lock_open_white_24dp @drawable/ic_call_white_24dp + @drawable/ic_phone_in_talk_white_24dp + @drawable/ic_phone_in_talk_black_18dp @drawable/ic_delete_black_24dp @drawable/ic_search_white_24dp @drawable/ic_lock_open_white_24dp @@ -219,6 +221,8 @@ @drawable/ic_attach_file_white_24dp @drawable/ic_lock_open_white_24dp @drawable/ic_call_white_24dp + @drawable/ic_phone_in_talk_white_24dp + @drawable/ic_phone_in_talk_white_18dp @drawable/ic_delete_white_24dp @drawable/ic_search_white_24dp @drawable/ic_lock_open_white_24dp From fc4397e5b9780a9be6845fe9ad0332829e72cc46 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 27 Apr 2020 17:51:38 +0200 Subject: [PATCH 05/19] play busy and dial tones --- .../xmpp/jingle/JingleConnectionManager.java | 19 +++- .../xmpp/jingle/JingleRtpConnection.java | 6 +- .../xmpp/jingle/ToneManager.java | 105 ++++++++++++++++++ 3 files changed, 123 insertions(+), 7 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index a6c14a384..975f25114 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -50,9 +50,10 @@ import eu.siacs.conversations.xmpp.stanzas.MessagePacket; import rocks.xmpp.addr.Jid; public class JingleConnectionManager extends AbstractConnectionManager { - private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); + public static final ScheduledExecutorService SCHEDULED_EXECUTOR_SERVICE = Executors.newSingleThreadScheduledExecutor(); + public final ToneManager toneManager = new ToneManager(); private final HashMap rtpSessionProposals = new HashMap<>(); - private final Map connections = new ConcurrentHashMap<>(); + private final ConcurrentHashMap connections = new ConcurrentHashMap<>(); private final Cache endedSessions = CacheBuilder.newBuilder() .expireAfterWrite(30, TimeUnit.MINUTES) @@ -141,7 +142,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { } ScheduledFuture schedule(final Runnable runnable, final long delay, final TimeUnit timeUnit) { - return this.scheduledExecutorService.schedule(runnable, delay, timeUnit); + return this.SCHEDULED_EXECUTOR_SERVICE.schedule(runnable, delay, timeUnit); } void respondWithJingleError(final Account account, final IqPacket original, String jingleCondition, String condition, String conditionType) { @@ -268,6 +269,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { synchronized (rtpSessionProposals) { if (rtpSessionProposals.remove(proposal) != null) { writeLogMissedOutgoing(account, proposal.with, proposal.sessionId, serverMsgId, timestamp); + toneManager.transition(true, RtpEndUserState.DECLINED_OR_BUSY); mXmppConnectionService.notifyJingleRtpConnectionUpdate(account, proposal.with, proposal.sessionId, RtpEndUserState.DECLINED_OR_BUSY); } else { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": no rtp session proposal found for " + from + " to deliver reject"); @@ -352,7 +354,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { } public Optional getOngoingRtpConnection(final Contact contact) { - for(final Map.Entry entry : this.connections.entrySet()) { + for (final Map.Entry entry : this.connections.entrySet()) { if (entry.getValue() instanceof JingleRtpConnection) { final AbstractJingleConnection.Id id = entry.getKey(); if (id.account == contact.getAccount() && id.with.asBareJid().equals(contact.getJid().asBareJid())) { @@ -423,6 +425,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { } } if (matchingProposal != null) { + toneManager.transition(true, RtpEndUserState.ENDED); Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": retracting rtp session proposal with " + with); this.rtpSessionProposals.remove(matchingProposal); final MessagePacket messagePacket = mXmppConnectionService.getMessageGenerator().sessionRetract(matchingProposal); @@ -439,11 +442,13 @@ public class JingleConnectionManager extends AbstractConnectionManager { if (proposal.account == account && with.asBareJid().equals(proposal.with)) { final DeviceDiscoveryState preexistingState = entry.getValue(); if (preexistingState != null && preexistingState != DeviceDiscoveryState.FAILED) { + final RtpEndUserState endUserState = preexistingState.toEndUserState(); + toneManager.transition(true, endUserState); mXmppConnectionService.notifyJingleRtpConnectionUpdate( account, with, proposal.sessionId, - preexistingState.toEndUserState() + endUserState ); return; } @@ -529,7 +534,9 @@ public class JingleConnectionManager extends AbstractConnectionManager { return; } this.rtpSessionProposals.put(sessionProposal, target); - mXmppConnectionService.notifyJingleRtpConnectionUpdate(account, sessionProposal.with, sessionProposal.sessionId, target.toEndUserState()); + final RtpEndUserState endUserState = target.toEndUserState(); + toneManager.transition(true, endUserState); + mXmppConnectionService.notifyJingleRtpConnectionUpdate(account, sessionProposal.with, sessionProposal.sessionId, endUserState); Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": flagging session " + sessionId + " as " + target); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 0dcac3326..7817c2d2e 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -1027,7 +1027,11 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } private void updateEndUserState() { - xmppConnectionService.notifyJingleRtpConnectionUpdate(id.account, id.with, id.sessionId, getEndUserState()); + final RtpEndUserState endUserState = getEndUserState(); + final RtpContentMap contentMap = initiatorRtpContentMap; + final Set media = contentMap == null ? Collections.emptySet() : contentMap.getMedia(); + jingleConnectionManager.toneManager.transition(isInitiator(), endUserState, media); + xmppConnectionService.notifyJingleRtpConnectionUpdate(id.account, id.with, id.sessionId, endUserState); } private void updateOngoingCallNotification() { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java new file mode 100644 index 000000000..d805e6b24 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java @@ -0,0 +1,105 @@ +package eu.siacs.conversations.xmpp.jingle; + +import android.media.AudioManager; +import android.media.ToneGenerator; +import android.util.Log; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import eu.siacs.conversations.Config; + +import static java.util.Arrays.asList; + +public class ToneManager { + + private final ToneGenerator toneGenerator; + + private ToneState state = null; + private ScheduledFuture currentTone; + + public ToneManager() { + this.toneGenerator = new ToneGenerator(AudioManager.STREAM_VOICE_CALL, 35); + } + + public void transition(final boolean isInitiator, final RtpEndUserState state) { + transition(of(isInitiator, state, Collections.emptySet())); + } + + public void transition(final boolean isInitiator, final RtpEndUserState state, final Set media) { + transition(of(isInitiator, state, media)); + } + + private static ToneState of(final boolean isInitiator, final RtpEndUserState state, final Set media) { + if (isInitiator) { + if (asList(RtpEndUserState.RINGING, RtpEndUserState.CONNECTING).contains(state)) { + return ToneState.RINGING; + } + if (state == RtpEndUserState.DECLINED_OR_BUSY) { + return ToneState.BUSY; + } + } + if (state == RtpEndUserState.ENDING_CALL) { + if (media.contains(Media.VIDEO)) { + return ToneState.NULL; + } else { + return ToneState.ENDING_CALL; + } + } + return ToneState.NULL; + } + + private synchronized void transition(ToneState state) { + if (this.state == state) { + return; + } + if (state == ToneState.NULL && this.state == ToneState.ENDING_CALL) { + return; + } + cancelCurrentTone(); + Log.d(Config.LOGTAG, getClass().getName() + ".transition(" + state + ")"); + switch (state) { + case RINGING: + scheduleWaitingTone(); + break; + case BUSY: + scheduleBusy(); + break; + case ENDING_CALL: + scheduleEnding(); + break; + } + this.state = state; + } + + private void scheduleEnding() { + this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(() -> { + this.toneGenerator.startTone(ToneGenerator.TONE_CDMA_CONFIRM, 600); + }, 0, TimeUnit.SECONDS); + } + + private void scheduleBusy() { + this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(() -> { + this.toneGenerator.startTone(ToneGenerator.TONE_CDMA_NETWORK_BUSY, 2500); + }, 0, TimeUnit.SECONDS); + } + + private void scheduleWaitingTone() { + this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.scheduleAtFixedRate(() -> { + this.toneGenerator.startTone(ToneGenerator.TONE_CDMA_DIAL_TONE_LITE, 750); + }, 0, 3, TimeUnit.SECONDS); + } + + private void cancelCurrentTone() { + if (currentTone != null) { + currentTone.cancel(true); + } + toneGenerator.stopTone(); + } + + private enum ToneState { + NULL, RINGING, BUSY, ENDING_CALL + } +} From 418cecad118329492d640a75af6173bf86973ba8 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 28 Apr 2020 06:50:04 +0200 Subject: [PATCH 06/19] remove XEP-0357 support for group chats --- .../services/PushManagementService.java | 8 --- .../conversations/entities/MucOptions.java | 4 -- .../siacs/conversations/parser/IqParser.java | 23 ------- .../services/XmppConnectionService.java | 60 ------------------- .../services/PushManagementService.java | 60 ------------------- 5 files changed, 155 deletions(-) diff --git a/src/free/java/eu/siacs/conversations/services/PushManagementService.java b/src/free/java/eu/siacs/conversations/services/PushManagementService.java index 9fac3655e..f436da434 100644 --- a/src/free/java/eu/siacs/conversations/services/PushManagementService.java +++ b/src/free/java/eu/siacs/conversations/services/PushManagementService.java @@ -15,18 +15,10 @@ public class PushManagementService { //stub implementation. only affects playstore flavor } - void registerPushTokenOnServer(Conversation conversation) { - //stub implementation. only affects playstore flavor - } - void unregisterChannel(Account account, String hash) { //stub implementation. only affects playstore flavor } - void disablePushOnServer(Conversation conversation) { - //stub implementation. only affects playstore flavor - } - public boolean available(Account account) { return false; } diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java index 74a3c538d..f45d93330 100644 --- a/src/main/java/eu/siacs/conversations/entities/MucOptions.java +++ b/src/main/java/eu/siacs/conversations/entities/MucOptions.java @@ -114,10 +114,6 @@ public class MucOptions { return MessageArchiveService.Version.has(getFeatures()); } - public boolean push() { - return getFeatures().contains(Namespace.PUSH); - } - public boolean updateConfiguration(ServiceDiscoveryResult serviceDiscoveryResult) { this.serviceDiscoveryResult = serviceDiscoveryResult; String name; diff --git a/src/main/java/eu/siacs/conversations/parser/IqParser.java b/src/main/java/eu/siacs/conversations/parser/IqParser.java index e5ef662bb..f825e464d 100644 --- a/src/main/java/eu/siacs/conversations/parser/IqParser.java +++ b/src/main/java/eu/siacs/conversations/parser/IqParser.java @@ -387,29 +387,6 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { response = mXmppConnectionService.getIqGenerator().entityTimeResponse(packet); } mXmppConnectionService.sendIqPacket(account, response, null); - } else if (packet.hasChild("pubsub", Namespace.PUBSUB) && packet.getType() == IqPacket.TYPE.SET) { - final Jid server = packet.getFrom(); - final Element pubsub = packet.findChild("pubsub", Namespace.PUBSUB); - final Element publish = pubsub == null ? null : pubsub.findChild("publish"); - final String node = publish == null ? null : publish.getAttribute("node"); - final Element item = publish == null ? null : publish.findChild("item"); - final Element notification = item == null ? null : item.findChild("notification", Namespace.PUSH); - if (notification != null && node != null && server != null) { - final Conversation conversation = mXmppConnectionService.findConversationByUuid(node); - if (conversation != null && conversation.getAccount() == account && conversation.getJid().getDomain().equals(server.getDomain())) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": received muc push event for "+conversation.getJid().asBareJid()); - mXmppConnectionService.sendIqPacket(account, packet.generateResponse(IqPacket.TYPE.RESULT), null); - mXmppConnectionService.mucSelfPingAndRejoin(conversation); - } else { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": received push event for unknown conference from "+server); - final IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR); - final Element error = response.addChild("error"); - error.setAttribute("type", "cancel"); - error.addChild("item-not-found", "urn:ietf:params:xml:ns:xmpp-stanzas"); - mXmppConnectionService.sendIqPacket(account, response, null); - } - } - } else { if (packet.getType() == IqPacket.TYPE.GET || packet.getType() == IqPacket.TYPE.SET) { final IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR); diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 0efa27618..b9204d9d5 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -610,7 +610,6 @@ public class XmppConnectionService extends Service { toggleForegroundService(true); } String pushedAccountHash = null; - String pushedChannelHash = null; boolean interactive = false; if (action != null) { final String uuid = intent.getStringExtra("uuid"); @@ -735,7 +734,6 @@ public class XmppConnectionService extends Service { break; case ACTION_FCM_MESSAGE_RECEIVED: pushedAccountHash = intent.getStringExtra("account"); - pushedChannelHash = intent.getStringExtra("channel"); Log.d(Config.LOGTAG, "push message arrived in service. account=" + pushedAccountHash); break; case Intent.ACTION_SEND: @@ -758,9 +756,6 @@ public class XmppConnectionService extends Service { "ui".equals(action), pushWasMeantForThisAccount, pingCandidates); - if (pushWasMeantForThisAccount && pushedChannelHash != null) { - checkMucStillJoined(account, pushedAccountHash, androidId); - } } if (pingNow) { for (Account account : pingCandidates) { @@ -853,20 +848,6 @@ public class XmppConnectionService extends Service { return pingNow; } - private void checkMucStillJoined(final Account account, final String hash, final String androidId) { - for (final Conversation conversation : this.conversations) { - if (conversation.getAccount() == account && conversation.getMode() == Conversational.MODE_MULTI) { - Jid jid = conversation.getJid().asBareJid(); - final String currentHash = CryptoHelper.getFingerprint(jid, androidId); - if (currentHash.equals(hash)) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received cloud push notification for MUC " + jid); - return; - } - } - } - mPushManagementService.unregisterChannel(account, hash); - } - public void reinitializeMuclumbusService() { mChannelDiscoveryService.initializeMuclumbusService(); } @@ -2156,10 +2137,6 @@ public class XmppConnectionService extends Service { } } } - if (conversation.getMucOptions().push()) { - disableDirectMucPush(conversation); - mPushManagementService.disablePushOnServer(conversation); - } leaveMuc(conversation); } else { if (conversation.getContact().getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { @@ -2758,9 +2735,6 @@ public class XmppConnectionService extends Service { } } } - if (mucOptions.push()) { - enableMucPush(conversation); - } synchronized (account.inProgressConferenceJoins) { account.inProgressConferenceJoins.remove(conversation); sendUnsentMessages(conversation); @@ -2805,40 +2779,6 @@ public class XmppConnectionService extends Service { updateConversationUi(); } } - - private void enableDirectMucPush(final Conversation conversation) { - final Account account = conversation.getAccount(); - final Jid room = conversation.getJid().asBareJid(); - final IqPacket enable = mIqGenerator.enablePush(conversation.getAccount().getJid(), conversation.getUuid(), null); - enable.setTo(room); - sendIqPacket(account, enable, (a, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { - Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": enabled direct push for muc " + room); - } else if (response.getType() == IqPacket.TYPE.ERROR) { - Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": unable to enable direct push for muc " + room + " " + response.getError()); - } - }); - } - - private void enableMucPush(final Conversation conversation) { - enableDirectMucPush(conversation); - mPushManagementService.registerPushTokenOnServer(conversation); - } - - private void disableDirectMucPush(final Conversation conversation) { - final Account account = conversation.getAccount(); - final Jid room = conversation.getJid().asBareJid(); - final IqPacket disable = mIqGenerator.disablePush(conversation.getAccount().getJid(), conversation.getUuid()); - disable.setTo(room); - sendIqPacket(account, disable, (a, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { - Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": disabled direct push for muc " + room); - } else if (response.getType() == IqPacket.TYPE.ERROR) { - Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": unable to disable direct push for muc " + room + " " + response.getError()); - } - }); - } - private void fetchConferenceMembers(final Conversation conversation) { final Account account = conversation.getAccount(); final AxolotlService axolotlService = account.getAxolotlService(); diff --git a/src/playstore/java/eu/siacs/conversations/services/PushManagementService.java b/src/playstore/java/eu/siacs/conversations/services/PushManagementService.java index e99de1b14..8469225b0 100644 --- a/src/playstore/java/eu/siacs/conversations/services/PushManagementService.java +++ b/src/playstore/java/eu/siacs/conversations/services/PushManagementService.java @@ -75,34 +75,6 @@ public class PushManagementService { } }); } - - void registerPushTokenOnServer(final Conversation conversation) { - Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": room "+conversation.getJid().asBareJid()+" has push support"); - retrieveFcmInstanceToken(token -> { - final Jid muc = conversation.getJid().asBareJid(); - final String androidId = PhoneHelper.getAndroidId(mXmppConnectionService); - final IqPacket packet = mXmppConnectionService.getIqGenerator().pushTokenToAppServer(getAppServer(), token, androidId, muc); - packet.setTo(muc); - mXmppConnectionService.sendIqPacket(conversation.getAccount(), packet, (a, response) -> { - final Data data = findResponseData(response); - if (response.getType() == IqPacket.TYPE.RESULT && data != null) { - try { - final String node = data.getValue("node"); - final String secret = data.getValue("secret"); - final Jid jid = Jid.of(data.getValue("jid")); - if (node != null && secret != null) { - enablePushOnServer(conversation, jid, node, secret); - } - } catch (IllegalArgumentException e) { - e.printStackTrace(); - } - } else { - Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": invalid response from app server"); - } - }); - }); - } - private void enablePushOnServer(final Account account, final Jid appServer, final String node, final String secret) { final IqPacket enable = mXmppConnectionService.getIqGenerator().enablePush(appServer, node, secret); mXmppConnectionService.sendIqPacket(account, enable, (a, p) -> { @@ -114,38 +86,6 @@ public class PushManagementService { }); } - private void enablePushOnServer(final Conversation conversation, final Jid appServer, final String node, final String secret) { - final Jid muc = conversation.getJid().asBareJid(); - final IqPacket enable = mXmppConnectionService.getIqGenerator().enablePush(appServer, node, secret); - enable.setTo(muc); - mXmppConnectionService.sendIqPacket(conversation.getAccount(), enable, (a, p) -> { - if (p.getType() == IqPacket.TYPE.RESULT) { - Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": successfully enabled push on " + muc); - if (conversation.setAttribute(Conversation.ATTRIBUTE_ALWAYS_NOTIFY, node)) { - mXmppConnectionService.updateConversation(conversation); - } - } else if (p.getType() == IqPacket.TYPE.ERROR) { - Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": enabling push on " + muc + " failed"); - } - }); - } - - public void disablePushOnServer(final Conversation conversation) { - final Jid muc = conversation.getJid().asBareJid(); - final String node = conversation.getAttribute(Conversation.ATTRIBUTE_PUSH_NODE); - if (node != null) { - final IqPacket disable = mXmppConnectionService.getIqGenerator().disablePush(getAppServer(), node); - disable.setTo(muc); - mXmppConnectionService.sendIqPacket(conversation.getAccount(), disable, (account, response) -> { - if (response.getType() == IqPacket.TYPE.ERROR) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": unable to disable push for room "+muc); - } - }); - } else { - Log.d(Config.LOGTAG,conversation.getAccount().getJid().asBareJid()+": room "+muc+" has no stored node. unable to disable push"); - } - } - private void retrieveFcmInstanceToken(final OnGcmInstanceTokenRetrieved instanceTokenRetrieved) { final FirebaseInstanceId firebaseInstanceId; try { From 27bf871472f83d4615bd12ec2afddfd93c51721d Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 28 Apr 2020 07:30:27 +0200 Subject: [PATCH 07/19] play beep when voice call connects --- .../xmpp/jingle/ToneManager.java | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java index d805e6b24..b64e28845 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java @@ -48,6 +48,13 @@ public class ToneManager { return ToneState.ENDING_CALL; } } + if (state == RtpEndUserState.CONNECTED) { + if (media.contains(Media.VIDEO)) { + return ToneState.NULL; + } else { + return ToneState.CONNECTED; + } + } return ToneState.NULL; } @@ -64,6 +71,9 @@ public class ToneManager { case RINGING: scheduleWaitingTone(); break; + case CONNECTED: + scheduleConnected(); + break; case BUSY: scheduleBusy(); break; @@ -74,9 +84,15 @@ public class ToneManager { this.state = state; } + private void scheduleConnected() { + this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(() -> { + this.toneGenerator.startTone(ToneGenerator.TONE_CDMA_ONE_MIN_BEEP, 145); + }, 0, TimeUnit.SECONDS); + } + private void scheduleEnding() { this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(() -> { - this.toneGenerator.startTone(ToneGenerator.TONE_CDMA_CONFIRM, 600); + this.toneGenerator.startTone(ToneGenerator.TONE_CDMA_CALLDROP_LITE, 375); }, 0, TimeUnit.SECONDS); } @@ -100,6 +116,6 @@ public class ToneManager { } private enum ToneState { - NULL, RINGING, BUSY, ENDING_CALL + NULL, RINGING, CONNECTED, BUSY, ENDING_CALL } } From 8183c54ba028cb4fbfeac7d2219655c42c27cde8 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 28 Apr 2020 08:25:21 +0200 Subject: [PATCH 08/19] use stanza-id for display markers in group chats --- .../conversations/entities/Conversation.java | 2014 +++++++++-------- .../generator/MessageGenerator.java | 23 +- .../conversations/parser/MessageParser.java | 39 +- .../services/XmppConnectionService.java | 9 +- 4 files changed, 1056 insertions(+), 1029 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index 53d2d74b3..94efb8fcf 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -33,1011 +33,1023 @@ import static eu.siacs.conversations.entities.Bookmark.printableValue; public class Conversation extends AbstractEntity implements Blockable, Comparable, Conversational, AvatarService.Avatarable { - public static final String TABLENAME = "conversations"; - - public static final int STATUS_AVAILABLE = 0; - public static final int STATUS_ARCHIVED = 1; - - public static final String NAME = "name"; - public static final String ACCOUNT = "accountUuid"; - public static final String CONTACT = "contactUuid"; - public static final String CONTACTJID = "contactJid"; - public static final String STATUS = "status"; - public static final String CREATED = "created"; - public static final String MODE = "mode"; - public static final String ATTRIBUTES = "attributes"; - - public static final String ATTRIBUTE_MUTED_TILL = "muted_till"; - public static final String ATTRIBUTE_ALWAYS_NOTIFY = "always_notify"; - public static final String ATTRIBUTE_PUSH_NODE = "push_node"; - public static final String ATTRIBUTE_LAST_CLEAR_HISTORY = "last_clear_history"; - static final String ATTRIBUTE_MUC_PASSWORD = "muc_password"; - private static final String ATTRIBUTE_NEXT_MESSAGE = "next_message"; - private static final String ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP = "next_message_timestamp"; - private static final String ATTRIBUTE_CRYPTO_TARGETS = "crypto_targets"; - private static final String ATTRIBUTE_NEXT_ENCRYPTION = "next_encryption"; - private static final String ATTRIBUTE_CORRECTING_MESSAGE = "correcting_message"; - static final String ATTRIBUTE_MEMBERS_ONLY = "members_only"; - static final String ATTRIBUTE_MODERATED = "moderated"; - static final String ATTRIBUTE_NON_ANONYMOUS = "non_anonymous"; - public static final String ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS = "formerly_private_non_anonymous"; - protected final ArrayList messages = new ArrayList<>(); - public AtomicBoolean messagesLoaded = new AtomicBoolean(true); - protected Account account = null; - private String draftMessage; - private String name; - private String contactUuid; - private String accountUuid; - private Jid contactJid; - private int status; - private long created; - private int mode; - private JSONObject attributes; - private Jid nextCounterpart; - private transient MucOptions mucOptions = null; - private boolean messagesLeftOnServer = true; - private ChatState mOutgoingChatState = Config.DEFAULT_CHAT_STATE; - private ChatState mIncomingChatState = Config.DEFAULT_CHAT_STATE; - private String mFirstMamReference = null; - - public Conversation(final String name, final Account account, final Jid contactJid, - final int mode) { - this(java.util.UUID.randomUUID().toString(), name, null, account - .getUuid(), contactJid, System.currentTimeMillis(), - STATUS_AVAILABLE, mode, ""); - this.account = account; - } - - public Conversation(final String uuid, final String name, final String contactUuid, - final String accountUuid, final Jid contactJid, final long created, final int status, - final int mode, final String attributes) { - this.uuid = uuid; - this.name = name; - this.contactUuid = contactUuid; - this.accountUuid = accountUuid; - this.contactJid = contactJid; - this.created = created; - this.status = status; - this.mode = mode; - try { - this.attributes = new JSONObject(attributes == null ? "" : attributes); - } catch (JSONException e) { - this.attributes = new JSONObject(); - } - } - - public static Conversation fromCursor(Cursor cursor) { - return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)), - cursor.getString(cursor.getColumnIndex(NAME)), - cursor.getString(cursor.getColumnIndex(CONTACT)), - cursor.getString(cursor.getColumnIndex(ACCOUNT)), - JidHelper.parseOrFallbackToInvalid(cursor.getString(cursor.getColumnIndex(CONTACTJID))), - cursor.getLong(cursor.getColumnIndex(CREATED)), - cursor.getInt(cursor.getColumnIndex(STATUS)), - cursor.getInt(cursor.getColumnIndex(MODE)), - cursor.getString(cursor.getColumnIndex(ATTRIBUTES))); - } - - public boolean hasMessagesLeftOnServer() { - return messagesLeftOnServer; - } - - public void setHasMessagesLeftOnServer(boolean value) { - this.messagesLeftOnServer = value; - } - - public Message getFirstUnreadMessage() { - Message first = null; - synchronized (this.messages) { - for (int i = messages.size() - 1; i >= 0; --i) { - if (messages.get(i).isRead()) { - return first; - } else { - first = messages.get(i); - } - } - } - return first; - } - - public Message findUnsentMessageWithUuid(String uuid) { - synchronized (this.messages) { - for (final Message message : this.messages) { - final int s = message.getStatus(); - if ((s == Message.STATUS_UNSEND || s == Message.STATUS_WAITING) && message.getUuid().equals(uuid)) { - return message; - } - } - } - return null; - } - - public void findWaitingMessages(OnMessageFound onMessageFound) { - final ArrayList results = new ArrayList<>(); - synchronized (this.messages) { - for (Message message : this.messages) { - if (message.getStatus() == Message.STATUS_WAITING) { - results.add(message); - } - } - } - for(Message result : results) { - onMessageFound.onMessageFound(result); - } - } - - public void findUnreadMessages(OnMessageFound onMessageFound) { - final ArrayList results = new ArrayList<>(); - synchronized (this.messages) { - for (Message message : this.messages) { - if (!message.isRead()) { - results.add(message); - } - } - } - for(Message result : results) { - onMessageFound.onMessageFound(result); - } - } - - public Message findMessageWithFileAndUuid(final String uuid) { - synchronized (this.messages) { - for (final Message message : this.messages) { - if (message.getUuid().equals(uuid) - && message.getEncryption() != Message.ENCRYPTION_PGP - && (message.isFileOrImage() || message.treatAsDownloadable())) { - return message; - } - } - } - return null; - } - - public boolean markAsDeleted(final List uuids) { - boolean deleted = false; - final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService(); - synchronized (this.messages) { - for(Message message : this.messages) { - if (uuids.contains(message.getUuid())) { - message.setDeleted(true); - deleted = true; - if (message.getEncryption() == Message.ENCRYPTION_PGP && pgpDecryptionService != null) { - pgpDecryptionService.discard(message); - } - } - } - } - return deleted; - } - - public boolean markAsChanged(final List files) { - boolean changed = false; - final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService(); - synchronized (this.messages) { - for(Message message : this.messages) { - for(final DatabaseBackend.FilePathInfo file : files) - if (file.uuid.toString().equals(message.getUuid())) { - message.setDeleted(file.deleted); - changed = true; - if (file.deleted && message.getEncryption() == Message.ENCRYPTION_PGP && pgpDecryptionService != null) { - pgpDecryptionService.discard(message); - } - } - } - } - return changed; - } - - public void clearMessages() { - synchronized (this.messages) { - this.messages.clear(); - } - } - - public boolean setIncomingChatState(ChatState state) { - if (this.mIncomingChatState == state) { - return false; - } - this.mIncomingChatState = state; - return true; - } - - public ChatState getIncomingChatState() { - return this.mIncomingChatState; - } - - public boolean setOutgoingChatState(ChatState state) { - if (mode == MODE_SINGLE && !getContact().isSelf() || (isPrivateAndNonAnonymous() && getNextCounterpart() == null)) { - if (this.mOutgoingChatState != state) { - this.mOutgoingChatState = state; - return true; - } - } - return false; - } - - public ChatState getOutgoingChatState() { - return this.mOutgoingChatState; - } - - public void trim() { - synchronized (this.messages) { - final int size = messages.size(); - final int maxsize = Config.PAGE_SIZE * Config.MAX_NUM_PAGES; - if (size > maxsize) { - List discards = this.messages.subList(0, size - maxsize); - final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService(); - if (pgpDecryptionService != null) { - pgpDecryptionService.discard(discards); - } - discards.clear(); - untieMessages(); - } - } - } - - public void findUnsentTextMessages(OnMessageFound onMessageFound) { - final ArrayList results = new ArrayList<>(); - synchronized (this.messages) { - for (Message message : this.messages) { - if ((message.getType() == Message.TYPE_TEXT || message.hasFileOnRemoteHost()) && message.getStatus() == Message.STATUS_UNSEND) { - results.add(message); - } - } - } - for(Message result : results) { - onMessageFound.onMessageFound(result); - } - } - - public Message findSentMessageWithUuidOrRemoteId(String id) { - synchronized (this.messages) { - for (Message message : this.messages) { - if (id.equals(message.getUuid()) - || (message.getStatus() >= Message.STATUS_SEND - && id.equals(message.getRemoteMsgId()))) { - return message; - } - } - } - return null; - } - - public Message findMessageWithRemoteIdAndCounterpart(String id, Jid counterpart, boolean received, boolean carbon) { - synchronized (this.messages) { - for (int i = this.messages.size() - 1; i >= 0; --i) { - final Message message = messages.get(i); - final Jid mcp = message.getCounterpart(); - if (mcp == null) { - continue; - } - final boolean counterpartMatch = mode == MODE_SINGLE ? - counterpart.asBareJid().equals(mcp.asBareJid()) : - counterpart.equals(mcp); - if (counterpartMatch && ((message.getStatus() == Message.STATUS_RECEIVED) == received) - && (carbon == message.isCarbon() || received)) { - final boolean idMatch = id.equals(message.getRemoteMsgId()) || message.remoteMsgIdMatchInEdit(id); - if (idMatch && !message.isFileOrImage() && !message.treatAsDownloadable()) { - return message; - } else { - return null; - } - } - } - } - return null; - } - - public Message findSentMessageWithUuid(String id) { - synchronized (this.messages) { - for (Message message : this.messages) { - if (id.equals(message.getUuid())) { - return message; - } - } - } - return null; - } - - public Message findMessageWithRemoteId(String id, Jid counterpart) { - synchronized (this.messages) { - for (Message message : this.messages) { - if (counterpart.equals(message.getCounterpart()) - && (id.equals(message.getRemoteMsgId()) || id.equals(message.getUuid()))) { - return message; - } - } - } - return null; - } - - public boolean hasMessageWithCounterpart(Jid counterpart) { - synchronized (this.messages) { - for (Message message : this.messages) { - if (counterpart.equals(message.getCounterpart())) { - return true; - } - } - } - return false; - } - - public void populateWithMessages(final List messages) { - synchronized (this.messages) { - messages.clear(); - messages.addAll(this.messages); - } - for (Iterator iterator = messages.iterator(); iterator.hasNext(); ) { - if (iterator.next().wasMergedIntoPrevious()) { - iterator.remove(); - } - } - } - - @Override - public boolean isBlocked() { - return getContact().isBlocked(); - } - - @Override - public boolean isDomainBlocked() { - return getContact().isDomainBlocked(); - } - - @Override - public Jid getBlockedJid() { - return getContact().getBlockedJid(); - } - - public int countMessages() { - synchronized (this.messages) { - return this.messages.size(); - } - } - - public String getFirstMamReference() { - return this.mFirstMamReference; - } - - public void setFirstMamReference(String reference) { - this.mFirstMamReference = reference; - } - - public void setLastClearHistory(long time, String reference) { - if (reference != null) { - setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, String.valueOf(time) + ":" + reference); - } else { - setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, time); - } - } - - public MamReference getLastClearHistory() { - return MamReference.fromAttribute(getAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY)); - } - - public List getAcceptedCryptoTargets() { - if (mode == MODE_SINGLE) { - return Collections.singletonList(getJid().asBareJid()); - } else { - return getJidListAttribute(ATTRIBUTE_CRYPTO_TARGETS); - } - } - - public void setAcceptedCryptoTargets(List acceptedTargets) { - setAttribute(ATTRIBUTE_CRYPTO_TARGETS, acceptedTargets); - } - - public boolean setCorrectingMessage(Message correctingMessage) { - setAttribute(ATTRIBUTE_CORRECTING_MESSAGE,correctingMessage == null ? null : correctingMessage.getUuid()); - return correctingMessage == null && draftMessage != null; - } - - public Message getCorrectingMessage() { - final String uuid = getAttribute(ATTRIBUTE_CORRECTING_MESSAGE); - return uuid == null ? null : findSentMessageWithUuid(uuid); - } - - public boolean withSelf() { - return getContact().isSelf(); - } - - @Override - public int compareTo(@NonNull Conversation another) { - return Long.compare(another.getSortableTime(), getSortableTime()); - } - - private long getSortableTime() { - Draft draft = getDraft(); - long messageTime = getLatestMessage().getTimeSent(); - if (draft == null) { - return messageTime; - } else { - return Math.max(messageTime, draft.getTimestamp()); - } - } - - public String getDraftMessage() { - return draftMessage; - } - - public void setDraftMessage(String draftMessage) { - this.draftMessage = draftMessage; - } - - public boolean isRead() { - return (this.messages.size() == 0) || this.messages.get(this.messages.size() - 1).isRead(); - } - - public List markRead(String upToUuid) { - final List unread = new ArrayList<>(); - synchronized (this.messages) { - for (Message message : this.messages) { - if (!message.isRead()) { - message.markRead(); - unread.add(message); - } - if (message.getUuid().equals(upToUuid)) { - return unread; - } - } - } - return unread; - } - - public static Message getLatestMarkableMessage(final List messages, boolean isPrivateAndNonAnonymousMuc) { - for (int i = messages.size() - 1; i >= 0; --i) { - final Message message = messages.get(i); - if (message.getStatus() <= Message.STATUS_RECEIVED - && (message.markable || isPrivateAndNonAnonymousMuc) - && !message.isPrivateMessage()) { - return message; - } - } - return null; - } - - public Message getLatestMessage() { - synchronized (this.messages) { - if (this.messages.size() == 0) { - Message message = new Message(this, "", Message.ENCRYPTION_NONE); - message.setType(Message.TYPE_STATUS); - message.setTime(Math.max(getCreated(), getLastClearHistory().getTimestamp())); - return message; - } else { - return this.messages.get(this.messages.size() - 1); - } - } - } - - public @NonNull CharSequence getName() { - if (getMode() == MODE_MULTI) { - final String roomName = getMucOptions().getName(); - final String subject = getMucOptions().getSubject(); - final Bookmark bookmark = getBookmark(); - final String bookmarkName = bookmark != null ? bookmark.getBookmarkName() : null; - if (printableValue(roomName)) { - return roomName; - } else if (printableValue(subject)) { - return subject; - } else if (printableValue(bookmarkName, false)) { - return bookmarkName; - } else { - final String generatedName = getMucOptions().createNameFromParticipants(); - if (printableValue(generatedName)) { - return generatedName; - } else { - return contactJid.getLocal() != null ? contactJid.getLocal() : contactJid; - } - } - } else if ((QuickConversationsService.isConversations() || !Config.QUICKSY_DOMAIN.equals(contactJid.getDomain())) && isWithStranger()) { - return contactJid; - } else { - return this.getContact().getDisplayName(); - } - } - - public String getAccountUuid() { - return this.accountUuid; - } - - public Account getAccount() { - return this.account; - } - - public void setAccount(final Account account) { - this.account = account; - } - - public Contact getContact() { - return this.account.getRoster().getContact(this.contactJid); - } - - @Override - public Jid getJid() { - return this.contactJid; - } - - public int getStatus() { - return this.status; - } - - public void setStatus(int status) { - this.status = status; - } - - public long getCreated() { - return this.created; - } - - public ContentValues getContentValues() { - ContentValues values = new ContentValues(); - values.put(UUID, uuid); - values.put(NAME, name); - values.put(CONTACT, contactUuid); - values.put(ACCOUNT, accountUuid); - values.put(CONTACTJID, contactJid.toString()); - values.put(CREATED, created); - values.put(STATUS, status); - values.put(MODE, mode); - synchronized (this.attributes) { - values.put(ATTRIBUTES, attributes.toString()); - } - return values; - } - - public int getMode() { - return this.mode; - } - - public void setMode(int mode) { - this.mode = mode; - } - - /** - * short for is Private and Non-anonymous - */ - public boolean isSingleOrPrivateAndNonAnonymous() { - return mode == MODE_SINGLE || isPrivateAndNonAnonymous(); - } - - public boolean isPrivateAndNonAnonymous() { - return getMucOptions().isPrivateAndNonAnonymous(); - } - - public synchronized MucOptions getMucOptions() { - if (this.mucOptions == null) { - this.mucOptions = new MucOptions(this); - } - return this.mucOptions; - } - - public void resetMucOptions() { - this.mucOptions = null; - } - - public void setContactJid(final Jid jid) { - this.contactJid = jid; - } - - public Jid getNextCounterpart() { - return this.nextCounterpart; - } - - public void setNextCounterpart(Jid jid) { - this.nextCounterpart = jid; - } - - public int getNextEncryption() { - if (!Config.supportOmemo() && !Config.supportOpenPgp()) { - return Message.ENCRYPTION_NONE; - } - if (OmemoSetting.isAlways()) { - return suitableForOmemoByDefault(this) ? Message.ENCRYPTION_AXOLOTL : Message.ENCRYPTION_NONE; - } - final int defaultEncryption; - if (suitableForOmemoByDefault(this)) { - defaultEncryption = OmemoSetting.getEncryption(); - } else { - defaultEncryption = Message.ENCRYPTION_NONE; - } - int encryption = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, defaultEncryption); - if (encryption == Message.ENCRYPTION_OTR || encryption < 0) { - return defaultEncryption; - } else { - return encryption; - } - } - - private static boolean suitableForOmemoByDefault(final Conversation conversation) { - if (conversation.getJid().asBareJid().equals(Config.BUG_REPORTS)) { - return false; - } - if (conversation.getContact().isOwnServer()) { - return false; - } - final String contact = conversation.getJid().getDomain(); - final String account = conversation.getAccount().getServer(); - if (Config.OMEMO_EXCEPTIONS.CONTACT_DOMAINS.contains(contact) || Config.OMEMO_EXCEPTIONS.ACCOUNT_DOMAINS.contains(account)) { - return false; - } - return conversation.isSingleOrPrivateAndNonAnonymous() || conversation.getBooleanAttribute(ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS, false); - } - - public boolean setNextEncryption(int encryption) { - return this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, encryption); - } - - public String getNextMessage() { - final String nextMessage = getAttribute(ATTRIBUTE_NEXT_MESSAGE); - return nextMessage == null ? "" : nextMessage; - } - - public @Nullable - Draft getDraft() { - long timestamp = getLongAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, 0); - if (timestamp > getLatestMessage().getTimeSent()) { - String message = getAttribute(ATTRIBUTE_NEXT_MESSAGE); - if (!TextUtils.isEmpty(message) && timestamp != 0) { - return new Draft(message, timestamp); - } - } - return null; - } - - public boolean setNextMessage(final String input) { - final String message = input == null || input.trim().isEmpty() ? null : input; - boolean changed = !getNextMessage().equals(message); - this.setAttribute(ATTRIBUTE_NEXT_MESSAGE, message); - if (changed) { - this.setAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, message == null ? 0 : System.currentTimeMillis()); - } - return changed; - } - - public Bookmark getBookmark() { - return this.account.getBookmark(this.contactJid); - } - - public Message findDuplicateMessage(Message message) { - synchronized (this.messages) { - for (int i = this.messages.size() - 1; i >= 0; --i) { - if (this.messages.get(i).similar(message)) { - return this.messages.get(i); - } - } - } - return null; - } - - public boolean hasDuplicateMessage(Message message) { - return findDuplicateMessage(message) != null; - } - - public Message findSentMessageWithBody(String body) { - synchronized (this.messages) { - for (int i = this.messages.size() - 1; i >= 0; --i) { - Message message = this.messages.get(i); - if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) { - String otherBody; - if (message.hasFileOnRemoteHost()) { - otherBody = message.getFileParams().url.toString(); - } else { - otherBody = message.body; - } - if (otherBody != null && otherBody.equals(body)) { - return message; - } - } - } - return null; - } - } - - public Message findRtpSession(final String sessionId, final int s) { - synchronized (this.messages) { - for (int i = this.messages.size() - 1; i >= 0; --i) { - final Message message = this.messages.get(i); - if ((message.getStatus() == s) && (message.getType() == Message.TYPE_RTP_SESSION) && sessionId.equals(message.getRemoteMsgId())) { - return message; - } - } - } - return null; - } - - public boolean possibleDuplicate(final String serverMsgId, final String remoteMsgId) { - if (serverMsgId == null || remoteMsgId == null) { - return false; - } - synchronized (this.messages) { - for(Message message : this.messages) { - if (serverMsgId.equals(message.getServerMsgId()) || remoteMsgId.equals(message.getRemoteMsgId())) { - return true; - } - } - } - return false; - } - - public MamReference getLastMessageTransmitted() { - final MamReference lastClear = getLastClearHistory(); - MamReference lastReceived = new MamReference(0); - synchronized (this.messages) { - for (int i = this.messages.size() - 1; i >= 0; --i) { - final Message message = this.messages.get(i); - if (message.isPrivateMessage()) { - continue; //it's unsafe to use private messages as anchor. They could be coming from user archive - } - if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon() || message.getServerMsgId() != null) { - lastReceived = new MamReference(message.getTimeSent(), message.getServerMsgId()); - break; - } - } - } - return MamReference.max(lastClear, lastReceived); - } - - public void setMutedTill(long value) { - this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value)); - } - - public boolean isMuted() { - return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0); - } - - public boolean alwaysNotify() { - return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPrivateAndNonAnonymous()); - } - - public boolean setAttribute(String key, boolean value) { - return setAttribute(key, String.valueOf(value)); - } - - private boolean setAttribute(String key, long value) { - return setAttribute(key, Long.toString(value)); - } - - private boolean setAttribute(String key, int value) { - return setAttribute(key, String.valueOf(value)); - } - - public boolean setAttribute(String key, String value) { - synchronized (this.attributes) { - try { - if (value == null) { - if (this.attributes.has(key)) { - this.attributes.remove(key); - return true; - } else { - return false; - } - } else { - final String prev = this.attributes.optString(key, null); - this.attributes.put(key, value); - return !value.equals(prev); - } - } catch (JSONException e) { - throw new AssertionError(e); - } - } - } - - public boolean setAttribute(String key, List jids) { - JSONArray array = new JSONArray(); - for (Jid jid : jids) { - array.put(jid.asBareJid().toString()); - } - synchronized (this.attributes) { - try { - this.attributes.put(key, array); - return true; - } catch (JSONException e) { - return false; - } - } - } - - public String getAttribute(String key) { - synchronized (this.attributes) { - return this.attributes.optString(key, null); - } - } - - private List getJidListAttribute(String key) { - ArrayList list = new ArrayList<>(); - synchronized (this.attributes) { - try { - JSONArray array = this.attributes.getJSONArray(key); - for (int i = 0; i < array.length(); ++i) { - try { - list.add(Jid.of(array.getString(i))); - } catch (IllegalArgumentException e) { - //ignored - } - } - } catch (JSONException e) { - //ignored - } - } - return list; - } - - private int getIntAttribute(String key, int defaultValue) { - String value = this.getAttribute(key); - if (value == null) { - return defaultValue; - } else { - try { - return Integer.parseInt(value); - } catch (NumberFormatException e) { - return defaultValue; - } - } - } - - public long getLongAttribute(String key, long defaultValue) { - String value = this.getAttribute(key); - if (value == null) { - return defaultValue; - } else { - try { - return Long.parseLong(value); - } catch (NumberFormatException e) { - return defaultValue; - } - } - } - - public boolean getBooleanAttribute(String key, boolean defaultValue) { - String value = this.getAttribute(key); - if (value == null) { - return defaultValue; - } else { - return Boolean.parseBoolean(value); - } - } - - public void add(Message message) { - synchronized (this.messages) { - this.messages.add(message); - } - } - - public void prepend(int offset, Message message) { - synchronized (this.messages) { - this.messages.add(Math.min(offset, this.messages.size()), message); - } - } - - public void addAll(int index, List messages) { - synchronized (this.messages) { - this.messages.addAll(index, messages); - } - account.getPgpDecryptionService().decrypt(messages); - } - - public void expireOldMessages(long timestamp) { - synchronized (this.messages) { - for (ListIterator iterator = this.messages.listIterator(); iterator.hasNext(); ) { - if (iterator.next().getTimeSent() < timestamp) { - iterator.remove(); - } - } - untieMessages(); - } - } - - public void sort() { - synchronized (this.messages) { - Collections.sort(this.messages, (left, right) -> { - if (left.getTimeSent() < right.getTimeSent()) { - return -1; - } else if (left.getTimeSent() > right.getTimeSent()) { - return 1; - } else { - return 0; - } - }); - untieMessages(); - } - } - - private void untieMessages() { - for (Message message : this.messages) { - message.untie(); - } - } - - public int unreadCount() { - synchronized (this.messages) { - int count = 0; - for (int i = this.messages.size() - 1; i >= 0; --i) { - if (this.messages.get(i).isRead()) { - return count; - } - ++count; - } - return count; - } - } - - public int receivedMessagesCount() { - int count = 0; - synchronized (this.messages) { - for (Message message : messages) { - if (message.getStatus() == Message.STATUS_RECEIVED) { - ++count; - } - } - } - return count; - } - - public int sentMessagesCount() { - int count = 0; - synchronized (this.messages) { - for (Message message : messages) { - if (message.getStatus() != Message.STATUS_RECEIVED) { - ++count; - } - } - } - return count; - } - - public boolean isWithStranger() { - final Contact contact = getContact(); - return mode == MODE_SINGLE - && !contact.isOwnServer() - && !contact.showInContactList() - && !contact.isSelf() - && !Config.QUICKSY_DOMAIN.equals(contact.getJid().toEscapedString()) - && sentMessagesCount() == 0; - } - - public int getReceivedMessagesCountSinceUuid(String uuid) { - if (uuid == null) { - return 0; - } - int count = 0; - synchronized (this.messages) { - for (int i = messages.size() - 1; i >= 0; i--) { - final Message message = messages.get(i); - if (uuid.equals(message.getUuid())) { - return count; - } - if (message.getStatus() <= Message.STATUS_RECEIVED) { - ++count; - } - } - } - return 0; - } - - @Override - public int getAvatarBackgroundColor() { - return UIHelper.getColorForName(getName().toString()); - } + public static final String TABLENAME = "conversations"; + + public static final int STATUS_AVAILABLE = 0; + public static final int STATUS_ARCHIVED = 1; + + public static final String NAME = "name"; + public static final String ACCOUNT = "accountUuid"; + public static final String CONTACT = "contactUuid"; + public static final String CONTACTJID = "contactJid"; + public static final String STATUS = "status"; + public static final String CREATED = "created"; + public static final String MODE = "mode"; + public static final String ATTRIBUTES = "attributes"; + + public static final String ATTRIBUTE_MUTED_TILL = "muted_till"; + public static final String ATTRIBUTE_ALWAYS_NOTIFY = "always_notify"; + public static final String ATTRIBUTE_PUSH_NODE = "push_node"; + public static final String ATTRIBUTE_LAST_CLEAR_HISTORY = "last_clear_history"; + public static final String ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS = "formerly_private_non_anonymous"; + static final String ATTRIBUTE_MUC_PASSWORD = "muc_password"; + static final String ATTRIBUTE_MEMBERS_ONLY = "members_only"; + static final String ATTRIBUTE_MODERATED = "moderated"; + static final String ATTRIBUTE_NON_ANONYMOUS = "non_anonymous"; + private static final String ATTRIBUTE_NEXT_MESSAGE = "next_message"; + private static final String ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP = "next_message_timestamp"; + private static final String ATTRIBUTE_CRYPTO_TARGETS = "crypto_targets"; + private static final String ATTRIBUTE_NEXT_ENCRYPTION = "next_encryption"; + private static final String ATTRIBUTE_CORRECTING_MESSAGE = "correcting_message"; + protected final ArrayList messages = new ArrayList<>(); + public AtomicBoolean messagesLoaded = new AtomicBoolean(true); + protected Account account = null; + private String draftMessage; + private String name; + private String contactUuid; + private String accountUuid; + private Jid contactJid; + private int status; + private long created; + private int mode; + private JSONObject attributes; + private Jid nextCounterpart; + private transient MucOptions mucOptions = null; + private boolean messagesLeftOnServer = true; + private ChatState mOutgoingChatState = Config.DEFAULT_CHAT_STATE; + private ChatState mIncomingChatState = Config.DEFAULT_CHAT_STATE; + private String mFirstMamReference = null; + + public Conversation(final String name, final Account account, final Jid contactJid, + final int mode) { + this(java.util.UUID.randomUUID().toString(), name, null, account + .getUuid(), contactJid, System.currentTimeMillis(), + STATUS_AVAILABLE, mode, ""); + this.account = account; + } + + public Conversation(final String uuid, final String name, final String contactUuid, + final String accountUuid, final Jid contactJid, final long created, final int status, + final int mode, final String attributes) { + this.uuid = uuid; + this.name = name; + this.contactUuid = contactUuid; + this.accountUuid = accountUuid; + this.contactJid = contactJid; + this.created = created; + this.status = status; + this.mode = mode; + try { + this.attributes = new JSONObject(attributes == null ? "" : attributes); + } catch (JSONException e) { + this.attributes = new JSONObject(); + } + } + + public static Conversation fromCursor(Cursor cursor) { + return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)), + cursor.getString(cursor.getColumnIndex(NAME)), + cursor.getString(cursor.getColumnIndex(CONTACT)), + cursor.getString(cursor.getColumnIndex(ACCOUNT)), + JidHelper.parseOrFallbackToInvalid(cursor.getString(cursor.getColumnIndex(CONTACTJID))), + cursor.getLong(cursor.getColumnIndex(CREATED)), + cursor.getInt(cursor.getColumnIndex(STATUS)), + cursor.getInt(cursor.getColumnIndex(MODE)), + cursor.getString(cursor.getColumnIndex(ATTRIBUTES))); + } + + public static Message getLatestMarkableMessage(final List messages, boolean isPrivateAndNonAnonymousMuc) { + for (int i = messages.size() - 1; i >= 0; --i) { + final Message message = messages.get(i); + if (message.getStatus() <= Message.STATUS_RECEIVED + && (message.markable || isPrivateAndNonAnonymousMuc) + && !message.isPrivateMessage()) { + return message; + } + } + return null; + } + + private static boolean suitableForOmemoByDefault(final Conversation conversation) { + if (conversation.getJid().asBareJid().equals(Config.BUG_REPORTS)) { + return false; + } + if (conversation.getContact().isOwnServer()) { + return false; + } + final String contact = conversation.getJid().getDomain(); + final String account = conversation.getAccount().getServer(); + if (Config.OMEMO_EXCEPTIONS.CONTACT_DOMAINS.contains(contact) || Config.OMEMO_EXCEPTIONS.ACCOUNT_DOMAINS.contains(account)) { + return false; + } + return conversation.isSingleOrPrivateAndNonAnonymous() || conversation.getBooleanAttribute(ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS, false); + } + + public boolean hasMessagesLeftOnServer() { + return messagesLeftOnServer; + } + + public void setHasMessagesLeftOnServer(boolean value) { + this.messagesLeftOnServer = value; + } + + public Message getFirstUnreadMessage() { + Message first = null; + synchronized (this.messages) { + for (int i = messages.size() - 1; i >= 0; --i) { + if (messages.get(i).isRead()) { + return first; + } else { + first = messages.get(i); + } + } + } + return first; + } + + public Message findUnsentMessageWithUuid(String uuid) { + synchronized (this.messages) { + for (final Message message : this.messages) { + final int s = message.getStatus(); + if ((s == Message.STATUS_UNSEND || s == Message.STATUS_WAITING) && message.getUuid().equals(uuid)) { + return message; + } + } + } + return null; + } + + public void findWaitingMessages(OnMessageFound onMessageFound) { + final ArrayList results = new ArrayList<>(); + synchronized (this.messages) { + for (Message message : this.messages) { + if (message.getStatus() == Message.STATUS_WAITING) { + results.add(message); + } + } + } + for (Message result : results) { + onMessageFound.onMessageFound(result); + } + } + + public void findUnreadMessages(OnMessageFound onMessageFound) { + final ArrayList results = new ArrayList<>(); + synchronized (this.messages) { + for (Message message : this.messages) { + if (!message.isRead()) { + results.add(message); + } + } + } + for (Message result : results) { + onMessageFound.onMessageFound(result); + } + } + + public Message findMessageWithFileAndUuid(final String uuid) { + synchronized (this.messages) { + for (final Message message : this.messages) { + if (message.getUuid().equals(uuid) + && message.getEncryption() != Message.ENCRYPTION_PGP + && (message.isFileOrImage() || message.treatAsDownloadable())) { + return message; + } + } + } + return null; + } + + public boolean markAsDeleted(final List uuids) { + boolean deleted = false; + final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService(); + synchronized (this.messages) { + for (Message message : this.messages) { + if (uuids.contains(message.getUuid())) { + message.setDeleted(true); + deleted = true; + if (message.getEncryption() == Message.ENCRYPTION_PGP && pgpDecryptionService != null) { + pgpDecryptionService.discard(message); + } + } + } + } + return deleted; + } + + public boolean markAsChanged(final List files) { + boolean changed = false; + final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService(); + synchronized (this.messages) { + for (Message message : this.messages) { + for (final DatabaseBackend.FilePathInfo file : files) + if (file.uuid.toString().equals(message.getUuid())) { + message.setDeleted(file.deleted); + changed = true; + if (file.deleted && message.getEncryption() == Message.ENCRYPTION_PGP && pgpDecryptionService != null) { + pgpDecryptionService.discard(message); + } + } + } + } + return changed; + } + + public void clearMessages() { + synchronized (this.messages) { + this.messages.clear(); + } + } + + public boolean setIncomingChatState(ChatState state) { + if (this.mIncomingChatState == state) { + return false; + } + this.mIncomingChatState = state; + return true; + } + + public ChatState getIncomingChatState() { + return this.mIncomingChatState; + } + + public boolean setOutgoingChatState(ChatState state) { + if (mode == MODE_SINGLE && !getContact().isSelf() || (isPrivateAndNonAnonymous() && getNextCounterpart() == null)) { + if (this.mOutgoingChatState != state) { + this.mOutgoingChatState = state; + return true; + } + } + return false; + } + + public ChatState getOutgoingChatState() { + return this.mOutgoingChatState; + } + + public void trim() { + synchronized (this.messages) { + final int size = messages.size(); + final int maxsize = Config.PAGE_SIZE * Config.MAX_NUM_PAGES; + if (size > maxsize) { + List discards = this.messages.subList(0, size - maxsize); + final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService(); + if (pgpDecryptionService != null) { + pgpDecryptionService.discard(discards); + } + discards.clear(); + untieMessages(); + } + } + } + + public void findUnsentTextMessages(OnMessageFound onMessageFound) { + final ArrayList results = new ArrayList<>(); + synchronized (this.messages) { + for (Message message : this.messages) { + if ((message.getType() == Message.TYPE_TEXT || message.hasFileOnRemoteHost()) && message.getStatus() == Message.STATUS_UNSEND) { + results.add(message); + } + } + } + for (Message result : results) { + onMessageFound.onMessageFound(result); + } + } + + public Message findSentMessageWithUuidOrRemoteId(String id) { + synchronized (this.messages) { + for (Message message : this.messages) { + if (id.equals(message.getUuid()) + || (message.getStatus() >= Message.STATUS_SEND + && id.equals(message.getRemoteMsgId()))) { + return message; + } + } + } + return null; + } + + public Message findMessageWithRemoteIdAndCounterpart(String id, Jid counterpart, boolean received, boolean carbon) { + synchronized (this.messages) { + for (int i = this.messages.size() - 1; i >= 0; --i) { + final Message message = messages.get(i); + final Jid mcp = message.getCounterpart(); + if (mcp == null) { + continue; + } + final boolean counterpartMatch = mode == MODE_SINGLE ? + counterpart.asBareJid().equals(mcp.asBareJid()) : + counterpart.equals(mcp); + if (counterpartMatch && ((message.getStatus() == Message.STATUS_RECEIVED) == received) + && (carbon == message.isCarbon() || received)) { + final boolean idMatch = id.equals(message.getRemoteMsgId()) || message.remoteMsgIdMatchInEdit(id); + if (idMatch && !message.isFileOrImage() && !message.treatAsDownloadable()) { + return message; + } else { + return null; + } + } + } + } + return null; + } + + public Message findSentMessageWithUuid(String id) { + synchronized (this.messages) { + for (Message message : this.messages) { + if (id.equals(message.getUuid())) { + return message; + } + } + } + return null; + } + + public Message findMessageWithRemoteId(String id, Jid counterpart) { + synchronized (this.messages) { + for (Message message : this.messages) { + if (counterpart.equals(message.getCounterpart()) + && (id.equals(message.getRemoteMsgId()) || id.equals(message.getUuid()))) { + return message; + } + } + } + return null; + } + + public Message findMessageWithServerMsgId(String id) { + synchronized (this.messages) { + for (Message message : this.messages) { + if (id != null && id.equals(message.getServerMsgId())) { + return message; + } + } + } + return null; + } + + public boolean hasMessageWithCounterpart(Jid counterpart) { + synchronized (this.messages) { + for (Message message : this.messages) { + if (counterpart.equals(message.getCounterpart())) { + return true; + } + } + } + return false; + } + + public void populateWithMessages(final List messages) { + synchronized (this.messages) { + messages.clear(); + messages.addAll(this.messages); + } + for (Iterator iterator = messages.iterator(); iterator.hasNext(); ) { + if (iterator.next().wasMergedIntoPrevious()) { + iterator.remove(); + } + } + } + + @Override + public boolean isBlocked() { + return getContact().isBlocked(); + } + + @Override + public boolean isDomainBlocked() { + return getContact().isDomainBlocked(); + } + + @Override + public Jid getBlockedJid() { + return getContact().getBlockedJid(); + } + + public int countMessages() { + synchronized (this.messages) { + return this.messages.size(); + } + } + + public String getFirstMamReference() { + return this.mFirstMamReference; + } + + public void setFirstMamReference(String reference) { + this.mFirstMamReference = reference; + } + + public void setLastClearHistory(long time, String reference) { + if (reference != null) { + setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, String.valueOf(time) + ":" + reference); + } else { + setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, time); + } + } + + public MamReference getLastClearHistory() { + return MamReference.fromAttribute(getAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY)); + } + + public List getAcceptedCryptoTargets() { + if (mode == MODE_SINGLE) { + return Collections.singletonList(getJid().asBareJid()); + } else { + return getJidListAttribute(ATTRIBUTE_CRYPTO_TARGETS); + } + } + + public void setAcceptedCryptoTargets(List acceptedTargets) { + setAttribute(ATTRIBUTE_CRYPTO_TARGETS, acceptedTargets); + } + + public boolean setCorrectingMessage(Message correctingMessage) { + setAttribute(ATTRIBUTE_CORRECTING_MESSAGE, correctingMessage == null ? null : correctingMessage.getUuid()); + return correctingMessage == null && draftMessage != null; + } + + public Message getCorrectingMessage() { + final String uuid = getAttribute(ATTRIBUTE_CORRECTING_MESSAGE); + return uuid == null ? null : findSentMessageWithUuid(uuid); + } + + public boolean withSelf() { + return getContact().isSelf(); + } + + @Override + public int compareTo(@NonNull Conversation another) { + return Long.compare(another.getSortableTime(), getSortableTime()); + } + + private long getSortableTime() { + Draft draft = getDraft(); + long messageTime = getLatestMessage().getTimeSent(); + if (draft == null) { + return messageTime; + } else { + return Math.max(messageTime, draft.getTimestamp()); + } + } + + public String getDraftMessage() { + return draftMessage; + } + + public void setDraftMessage(String draftMessage) { + this.draftMessage = draftMessage; + } + + public boolean isRead() { + return (this.messages.size() == 0) || this.messages.get(this.messages.size() - 1).isRead(); + } + + public List markRead(String upToUuid) { + final List unread = new ArrayList<>(); + synchronized (this.messages) { + for (Message message : this.messages) { + if (!message.isRead()) { + message.markRead(); + unread.add(message); + } + if (message.getUuid().equals(upToUuid)) { + return unread; + } + } + } + return unread; + } + + public Message getLatestMessage() { + synchronized (this.messages) { + if (this.messages.size() == 0) { + Message message = new Message(this, "", Message.ENCRYPTION_NONE); + message.setType(Message.TYPE_STATUS); + message.setTime(Math.max(getCreated(), getLastClearHistory().getTimestamp())); + return message; + } else { + return this.messages.get(this.messages.size() - 1); + } + } + } + + public @NonNull + CharSequence getName() { + if (getMode() == MODE_MULTI) { + final String roomName = getMucOptions().getName(); + final String subject = getMucOptions().getSubject(); + final Bookmark bookmark = getBookmark(); + final String bookmarkName = bookmark != null ? bookmark.getBookmarkName() : null; + if (printableValue(roomName)) { + return roomName; + } else if (printableValue(subject)) { + return subject; + } else if (printableValue(bookmarkName, false)) { + return bookmarkName; + } else { + final String generatedName = getMucOptions().createNameFromParticipants(); + if (printableValue(generatedName)) { + return generatedName; + } else { + return contactJid.getLocal() != null ? contactJid.getLocal() : contactJid; + } + } + } else if ((QuickConversationsService.isConversations() || !Config.QUICKSY_DOMAIN.equals(contactJid.getDomain())) && isWithStranger()) { + return contactJid; + } else { + return this.getContact().getDisplayName(); + } + } + + public String getAccountUuid() { + return this.accountUuid; + } + + public Account getAccount() { + return this.account; + } + + public void setAccount(final Account account) { + this.account = account; + } + + public Contact getContact() { + return this.account.getRoster().getContact(this.contactJid); + } + + @Override + public Jid getJid() { + return this.contactJid; + } + + public int getStatus() { + return this.status; + } + + public void setStatus(int status) { + this.status = status; + } + + public long getCreated() { + return this.created; + } + + public ContentValues getContentValues() { + ContentValues values = new ContentValues(); + values.put(UUID, uuid); + values.put(NAME, name); + values.put(CONTACT, contactUuid); + values.put(ACCOUNT, accountUuid); + values.put(CONTACTJID, contactJid.toString()); + values.put(CREATED, created); + values.put(STATUS, status); + values.put(MODE, mode); + synchronized (this.attributes) { + values.put(ATTRIBUTES, attributes.toString()); + } + return values; + } + + public int getMode() { + return this.mode; + } + + public void setMode(int mode) { + this.mode = mode; + } + + /** + * short for is Private and Non-anonymous + */ + public boolean isSingleOrPrivateAndNonAnonymous() { + return mode == MODE_SINGLE || isPrivateAndNonAnonymous(); + } + + public boolean isPrivateAndNonAnonymous() { + return getMucOptions().isPrivateAndNonAnonymous(); + } + + public synchronized MucOptions getMucOptions() { + if (this.mucOptions == null) { + this.mucOptions = new MucOptions(this); + } + return this.mucOptions; + } + + public void resetMucOptions() { + this.mucOptions = null; + } + + public void setContactJid(final Jid jid) { + this.contactJid = jid; + } + + public Jid getNextCounterpart() { + return this.nextCounterpart; + } + + public void setNextCounterpart(Jid jid) { + this.nextCounterpart = jid; + } + + public int getNextEncryption() { + if (!Config.supportOmemo() && !Config.supportOpenPgp()) { + return Message.ENCRYPTION_NONE; + } + if (OmemoSetting.isAlways()) { + return suitableForOmemoByDefault(this) ? Message.ENCRYPTION_AXOLOTL : Message.ENCRYPTION_NONE; + } + final int defaultEncryption; + if (suitableForOmemoByDefault(this)) { + defaultEncryption = OmemoSetting.getEncryption(); + } else { + defaultEncryption = Message.ENCRYPTION_NONE; + } + int encryption = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, defaultEncryption); + if (encryption == Message.ENCRYPTION_OTR || encryption < 0) { + return defaultEncryption; + } else { + return encryption; + } + } + + public boolean setNextEncryption(int encryption) { + return this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, encryption); + } + + public String getNextMessage() { + final String nextMessage = getAttribute(ATTRIBUTE_NEXT_MESSAGE); + return nextMessage == null ? "" : nextMessage; + } + + public @Nullable + Draft getDraft() { + long timestamp = getLongAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, 0); + if (timestamp > getLatestMessage().getTimeSent()) { + String message = getAttribute(ATTRIBUTE_NEXT_MESSAGE); + if (!TextUtils.isEmpty(message) && timestamp != 0) { + return new Draft(message, timestamp); + } + } + return null; + } + + public boolean setNextMessage(final String input) { + final String message = input == null || input.trim().isEmpty() ? null : input; + boolean changed = !getNextMessage().equals(message); + this.setAttribute(ATTRIBUTE_NEXT_MESSAGE, message); + if (changed) { + this.setAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, message == null ? 0 : System.currentTimeMillis()); + } + return changed; + } + + public Bookmark getBookmark() { + return this.account.getBookmark(this.contactJid); + } + + public Message findDuplicateMessage(Message message) { + synchronized (this.messages) { + for (int i = this.messages.size() - 1; i >= 0; --i) { + if (this.messages.get(i).similar(message)) { + return this.messages.get(i); + } + } + } + return null; + } + + public boolean hasDuplicateMessage(Message message) { + return findDuplicateMessage(message) != null; + } + + public Message findSentMessageWithBody(String body) { + synchronized (this.messages) { + for (int i = this.messages.size() - 1; i >= 0; --i) { + Message message = this.messages.get(i); + if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) { + String otherBody; + if (message.hasFileOnRemoteHost()) { + otherBody = message.getFileParams().url.toString(); + } else { + otherBody = message.body; + } + if (otherBody != null && otherBody.equals(body)) { + return message; + } + } + } + return null; + } + } + + public Message findRtpSession(final String sessionId, final int s) { + synchronized (this.messages) { + for (int i = this.messages.size() - 1; i >= 0; --i) { + final Message message = this.messages.get(i); + if ((message.getStatus() == s) && (message.getType() == Message.TYPE_RTP_SESSION) && sessionId.equals(message.getRemoteMsgId())) { + return message; + } + } + } + return null; + } + + public boolean possibleDuplicate(final String serverMsgId, final String remoteMsgId) { + if (serverMsgId == null || remoteMsgId == null) { + return false; + } + synchronized (this.messages) { + for (Message message : this.messages) { + if (serverMsgId.equals(message.getServerMsgId()) || remoteMsgId.equals(message.getRemoteMsgId())) { + return true; + } + } + } + return false; + } + + public MamReference getLastMessageTransmitted() { + final MamReference lastClear = getLastClearHistory(); + MamReference lastReceived = new MamReference(0); + synchronized (this.messages) { + for (int i = this.messages.size() - 1; i >= 0; --i) { + final Message message = this.messages.get(i); + if (message.isPrivateMessage()) { + continue; //it's unsafe to use private messages as anchor. They could be coming from user archive + } + if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon() || message.getServerMsgId() != null) { + lastReceived = new MamReference(message.getTimeSent(), message.getServerMsgId()); + break; + } + } + } + return MamReference.max(lastClear, lastReceived); + } + + public void setMutedTill(long value) { + this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value)); + } + + public boolean isMuted() { + return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0); + } + + public boolean alwaysNotify() { + return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPrivateAndNonAnonymous()); + } + + public boolean setAttribute(String key, boolean value) { + return setAttribute(key, String.valueOf(value)); + } + + private boolean setAttribute(String key, long value) { + return setAttribute(key, Long.toString(value)); + } + + private boolean setAttribute(String key, int value) { + return setAttribute(key, String.valueOf(value)); + } + + public boolean setAttribute(String key, String value) { + synchronized (this.attributes) { + try { + if (value == null) { + if (this.attributes.has(key)) { + this.attributes.remove(key); + return true; + } else { + return false; + } + } else { + final String prev = this.attributes.optString(key, null); + this.attributes.put(key, value); + return !value.equals(prev); + } + } catch (JSONException e) { + throw new AssertionError(e); + } + } + } + + public boolean setAttribute(String key, List jids) { + JSONArray array = new JSONArray(); + for (Jid jid : jids) { + array.put(jid.asBareJid().toString()); + } + synchronized (this.attributes) { + try { + this.attributes.put(key, array); + return true; + } catch (JSONException e) { + return false; + } + } + } + + public String getAttribute(String key) { + synchronized (this.attributes) { + return this.attributes.optString(key, null); + } + } + + private List getJidListAttribute(String key) { + ArrayList list = new ArrayList<>(); + synchronized (this.attributes) { + try { + JSONArray array = this.attributes.getJSONArray(key); + for (int i = 0; i < array.length(); ++i) { + try { + list.add(Jid.of(array.getString(i))); + } catch (IllegalArgumentException e) { + //ignored + } + } + } catch (JSONException e) { + //ignored + } + } + return list; + } + + private int getIntAttribute(String key, int defaultValue) { + String value = this.getAttribute(key); + if (value == null) { + return defaultValue; + } else { + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + return defaultValue; + } + } + } + + public long getLongAttribute(String key, long defaultValue) { + String value = this.getAttribute(key); + if (value == null) { + return defaultValue; + } else { + try { + return Long.parseLong(value); + } catch (NumberFormatException e) { + return defaultValue; + } + } + } + + public boolean getBooleanAttribute(String key, boolean defaultValue) { + String value = this.getAttribute(key); + if (value == null) { + return defaultValue; + } else { + return Boolean.parseBoolean(value); + } + } + + public void add(Message message) { + synchronized (this.messages) { + this.messages.add(message); + } + } + + public void prepend(int offset, Message message) { + synchronized (this.messages) { + this.messages.add(Math.min(offset, this.messages.size()), message); + } + } + + public void addAll(int index, List messages) { + synchronized (this.messages) { + this.messages.addAll(index, messages); + } + account.getPgpDecryptionService().decrypt(messages); + } + + public void expireOldMessages(long timestamp) { + synchronized (this.messages) { + for (ListIterator iterator = this.messages.listIterator(); iterator.hasNext(); ) { + if (iterator.next().getTimeSent() < timestamp) { + iterator.remove(); + } + } + untieMessages(); + } + } + + public void sort() { + synchronized (this.messages) { + Collections.sort(this.messages, (left, right) -> { + if (left.getTimeSent() < right.getTimeSent()) { + return -1; + } else if (left.getTimeSent() > right.getTimeSent()) { + return 1; + } else { + return 0; + } + }); + untieMessages(); + } + } + + private void untieMessages() { + for (Message message : this.messages) { + message.untie(); + } + } + + public int unreadCount() { + synchronized (this.messages) { + int count = 0; + for (int i = this.messages.size() - 1; i >= 0; --i) { + if (this.messages.get(i).isRead()) { + return count; + } + ++count; + } + return count; + } + } + + public int receivedMessagesCount() { + int count = 0; + synchronized (this.messages) { + for (Message message : messages) { + if (message.getStatus() == Message.STATUS_RECEIVED) { + ++count; + } + } + } + return count; + } + + public int sentMessagesCount() { + int count = 0; + synchronized (this.messages) { + for (Message message : messages) { + if (message.getStatus() != Message.STATUS_RECEIVED) { + ++count; + } + } + } + return count; + } + + public boolean isWithStranger() { + final Contact contact = getContact(); + return mode == MODE_SINGLE + && !contact.isOwnServer() + && !contact.showInContactList() + && !contact.isSelf() + && !Config.QUICKSY_DOMAIN.equals(contact.getJid().toEscapedString()) + && sentMessagesCount() == 0; + } + + public int getReceivedMessagesCountSinceUuid(String uuid) { + if (uuid == null) { + return 0; + } + int count = 0; + synchronized (this.messages) { + for (int i = messages.size() - 1; i >= 0; i--) { + final Message message = messages.get(i); + if (uuid.equals(message.getUuid())) { + return count; + } + if (message.getStatus() <= Message.STATUS_RECEIVED) { + ++count; + } + } + } + return 0; + } + + @Override + public int getAvatarBackgroundColor() { + return UIHelper.getColorForName(getName().toString()); + } public interface OnMessageFound { - void onMessageFound(final Message message); - } + void onMessageFound(final Message message); + } - public static class Draft { - private final String message; - private final long timestamp; + public static class Draft { + private final String message; + private final long timestamp; - private Draft(String message, long timestamp) { - this.message = message; - this.timestamp = timestamp; - } + private Draft(String message, long timestamp) { + this.message = message; + this.timestamp = timestamp; + } - public long getTimestamp() { - return timestamp; - } + public long getTimestamp() { + return timestamp; + } - public String getMessage() { - return message; - } - } + public String getMessage() { + return message; + } + } } diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index a0cb0ca1f..6969e4a5e 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -12,6 +12,7 @@ import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Conversational; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.http.P1S3UrlStreamHandler; import eu.siacs.conversations.services.XmppConnectionService; @@ -160,15 +161,23 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - public MessagePacket confirm(final Account account, final Jid to, final String id, final Jid counterpart, final boolean groupChat) { - MessagePacket packet = new MessagePacket(); + public MessagePacket confirm(final Message message) { + final boolean groupChat = message.getConversation().getMode() == Conversational.MODE_MULTI; + final Jid to = message.getCounterpart(); + final MessagePacket packet = new MessagePacket(); packet.setType(groupChat ? MessagePacket.TYPE_GROUPCHAT : MessagePacket.TYPE_CHAT); packet.setTo(groupChat ? to.asBareJid() : to); - packet.setFrom(account.getJid()); - Element displayed = packet.addChild("displayed", "urn:xmpp:chat-markers:0"); - displayed.setAttribute("id", id); - if (groupChat && counterpart != null) { - displayed.setAttribute("sender", counterpart.toString()); + final Element displayed = packet.addChild("displayed", "urn:xmpp:chat-markers:0"); + if (groupChat) { + final String stanzaId = message.getServerMsgId(); + if (stanzaId != null) { + displayed.setAttribute("id", stanzaId); + } else { + displayed.setAttribute("sender", to.toString()); + displayed.setAttribute("id", message.getRemoteMsgId()); + } + } else { + displayed.setAttribute("id", message.getRemoteMsgId()); } packet.addChild("store", "urn:xmpp:hints"); return packet; diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 9cb6e7bbd..8d11499d2 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -927,22 +927,29 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece activateGracePeriod(account); } } else if (isTypeGroupChat) { - Conversation conversation = mXmppConnectionService.find(account, counterpart.asBareJid()); - if (conversation != null && id != null && sender != null) { - Message message = conversation.findMessageWithRemoteId(id, sender); - if (message != null) { - final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart); - final Jid trueJid = getTrueCounterpart((query != null && query.safeToExtractTrueCounterpart()) ? mucUserElement : null, fallback); - final boolean trueJidMatchesAccount = account.getJid().asBareJid().equals(trueJid == null ? null : trueJid.asBareJid()); - if (trueJidMatchesAccount || conversation.getMucOptions().isSelf(counterpart)) { - if (!message.isRead() && (query == null || query.isCatchup())) { //checking if message is unread fixes race conditions with reflections - mXmppConnectionService.markRead(conversation); - } - } else if (!counterpart.isBareJid() && trueJid != null) { - final ReadByMarker readByMarker = ReadByMarker.from(counterpart, trueJid); - if (message.addReadByMarker(readByMarker)) { - mXmppConnectionService.updateMessage(message, false); - } + final Conversation conversation = mXmppConnectionService.find(account, counterpart.asBareJid()); + final Message message; + if (conversation != null && id != null) { + if (sender != null) { + message = conversation.findMessageWithRemoteId(id, sender); + } else { + message = conversation.findMessageWithServerMsgId(id); + } + } else { + message = null; + } + if (message != null) { + final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart); + final Jid trueJid = getTrueCounterpart((query != null && query.safeToExtractTrueCounterpart()) ? mucUserElement : null, fallback); + final boolean trueJidMatchesAccount = account.getJid().asBareJid().equals(trueJid == null ? null : trueJid.asBareJid()); + if (trueJidMatchesAccount || conversation.getMucOptions().isSelf(counterpart)) { + if (!message.isRead() && (query == null || query.isCatchup())) { //checking if message is unread fixes race conditions with reflections + mXmppConnectionService.markRead(conversation); + } + } else if (!counterpart.isBareJid() && trueJid != null) { + final ReadByMarker readByMarker = ReadByMarker.from(counterpart, trueJid); + if (message.addReadByMarker(readByMarker)) { + mXmppConnectionService.updateMessage(message, false); } } } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index b9204d9d5..4b1c5c2ff 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -4062,6 +4062,7 @@ public class XmppConnectionService extends Service { } }; mDatabaseWriterExecutor.execute(runnable); + updateConversationUi(); updateUnreadCountBadge(); return readMessages; } else { @@ -4094,11 +4095,9 @@ public class XmppConnectionService extends Service { && (markable.trusted() || isPrivateAndNonAnonymousMuc) && markable.getRemoteMsgId() != null) { Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": sending read marker to " + markable.getCounterpart().toString()); - Account account = conversation.getAccount(); - final Jid to = markable.getCounterpart(); - final boolean groupChat = conversation.getMode() == Conversation.MODE_MULTI; - MessagePacket packet = mMessageGenerator.confirm(account, to, markable.getRemoteMsgId(), markable.getCounterpart(), groupChat); - this.sendMessagePacket(conversation.getAccount(), packet); + final Account account = conversation.getAccount(); + final MessagePacket packet = mMessageGenerator.confirm(markable); + this.sendMessagePacket(account, packet); } } From faf1ff365d09c230003ed4aa14298d871ac06a27 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 28 Apr 2020 11:22:42 +0200 Subject: [PATCH 09/19] modify call connected tone --- .../java/eu/siacs/conversations/xmpp/jingle/ToneManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java index b64e28845..3274452e9 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java @@ -86,7 +86,7 @@ public class ToneManager { private void scheduleConnected() { this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(() -> { - this.toneGenerator.startTone(ToneGenerator.TONE_CDMA_ONE_MIN_BEEP, 145); + this.toneGenerator.startTone(ToneGenerator.TONE_PROP_PROMPT, 200); }, 0, TimeUnit.SECONDS); } From f93bac6d73a72a7047425b4cb87fd3a774d0ceb1 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 28 Apr 2020 20:15:23 +0200 Subject: [PATCH 10/19] catch ISE around peerconnection.dispose() --- .../siacs/conversations/xmpp/jingle/WebRTCWrapper.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index a5fc8429a..ffbd8cf70 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -230,7 +230,7 @@ public class WebRTCWrapper { final AppRTCAudioManager audioManager = this.appRTCAudioManager; final EglBase eglBase = this.eglBase; if (peerConnection != null) { - peerConnection.dispose(); + dispose(peerConnection); this.peerConnection = null; } if (audioManager != null) { @@ -251,6 +251,14 @@ public class WebRTCWrapper { } } + private static void dispose(final PeerConnection peerConnection) { + try { + peerConnection.dispose(); + } catch (final IllegalStateException e) { + Log.e(Config.LOGTAG,"unable to dispose of peer connection", e); + } + } + void verifyClosed() { if (this.peerConnection != null || this.eglBase != null From daf234191b604a21ec8ee47b7a9c165c3630e25b Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 28 Apr 2020 20:15:34 +0200 Subject: [PATCH 11/19] pulled translations from transifex --- src/conversations/res/values-ca/strings.xml | 5 + src/main/res/values-zh-rCN/strings.xml | 103 ++++++++++++++------ 2 files changed, 76 insertions(+), 32 deletions(-) create mode 100644 src/conversations/res/values-ca/strings.xml diff --git a/src/conversations/res/values-ca/strings.xml b/src/conversations/res/values-ca/strings.xml new file mode 100644 index 000000000..71a32bc6b --- /dev/null +++ b/src/conversations/res/values-ca/strings.xml @@ -0,0 +1,5 @@ + + + Triï el seu proveïdor de XMPP + + \ No newline at end of file diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index 0cfeb7929..d0a2e340d 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -7,7 +7,7 @@ 关闭聊天 联系人详情 群聊详情 - 频道详情 + 群聊详情 安全聊天 添加账号 编辑姓名 @@ -31,10 +31,10 @@ 1分钟前 %d分钟前 %d条未读消息 - 正在发送… - 解密信息中. 请稍候… - OpenPGP 加密的信息 - 该名称已存在 + 发送中… + 解密中. 请稍候… + OpenPGP加密的信息 + 该用户名已存在 无效的用户名 管理员 所有者 @@ -47,11 +47,11 @@ 封禁 %s 中的所有联系人? 解封%s 中所有联系人? 联系人已封禁 - 已屏蔽 + 已封禁 从书签中移除 %s ?相关会话消息不会被清除 . 在服务器上注册新账户 在服务器上修改密码 - 分享…… + 分享… 开始会话 邀请联系人 邀请 @@ -62,8 +62,8 @@ 添加 编辑 删除 - 屏蔽 - 解除屏蔽 + 封禁 + 解封 保存 完成 畅聊已崩溃 @@ -73,7 +73,7 @@ 无法连接至账户 无法连接至多个账户 点击此处管理账户 - 附加文件 + 添加文件 该联系人不在您的列表,需要加为联系人吗 ? 添加联系人 传递失败 @@ -109,13 +109,17 @@ 因您的联系人未公布公钥,畅聊未能成功加密您的信息.\n\n请通知联系人设置OpenPGP. 常规 接收文件 - 自动接收小于 … 的文件 + 自动接收小于此大小的文件 附件 通知 震动 收到新消息时震动 LED 灯提示 收到新消息时闪烁通知灯 + 铃声 + 通知铃声 + 新消息通知铃声 + 来电铃声 静默期限 在您的其他设备之一上检测到活动之后,时间通知的长度将被静音。 高级 @@ -134,7 +138,7 @@ 接收在线联系人列表更新 请求在线联系人列表更新 选择图片 - 照相 + 拍摄图片 预先同意订阅请求 您选择的文件不是图像文件 转换图像出错 @@ -188,6 +192,7 @@ XEP-0191:屏蔽指令 XEP-0237:名单版本 XEP-0198:流管理 + XEP-0215:发现外部服务 XEP-0163:PEP(头像/OMEMO) XEP-0363:HTTP文件上传 XEP-0357:推送 @@ -229,23 +234,23 @@ 保存为书签 删除书签 解散群聊 - 解散频道 + 解散群聊 您确定要解散此群聊吗?\n\n警告:此群聊将在服务器上完全删除。 - 您确定要解散此公共频道吗?\n\n警告:该频道将在服务器上完全删除。 + 您确定要解散此公共群聊吗?\n\n警告:该群聊将在服务器上完全删除。 无法解散群聊 - 无法解散频道 + 无法解散群聊 编辑群聊主题 主题 正在加入群聊… 离开 联系人已添加你到联系人列表 反向添加 - %s 已读此句 - %s 已读此句 - %1$s +%2$d 都已读此句 - 全部已读此句 + %s 读到这里了 + %s 读到这里了 + %1$s 和另外%2$d人读到这里了 + 所有人都读到这里了 发布 - 点击头像可从相册中选择头像 + 点击头像以选择图片 正在发布… 服务器拒绝了您的发布请求 转换头像图片出错 @@ -283,7 +288,7 @@ 同步书签 根据书签中的自动加入标记加入并离开群聊。 OMEMO 指纹已拷贝到剪贴板! - 你已经被禁言了 + 你已经被封禁了 这个群组只允许群组成员聊天 资源限制 您已被移出该群组了 @@ -374,10 +379,10 @@ 授予所有者权限 撤销所有者权限 从群聊中移除 - 从频道中移除 + 从群聊中移除 不能修改 %s 的从属关系 屏蔽群聊 - 从频道中屏蔽 + 从群聊中屏蔽 %s将被从公共群聊中移除。只有将此用户封禁才能将他从群聊永远移除。 现在屏蔽 不能修改 %s 的角色 @@ -465,7 +470,7 @@ xmpp.example.com 使用证书添加账户 无法解析证书 - 留空以认证 w/ 证书 + 留空以使用证书认证 压缩设置 服务端压缩设置 正在获取压缩设置。请稍候…… @@ -483,7 +488,7 @@ 所有连接使用 Tor 网络传输,需要 Orbot 主机名 端口 - 服务器 - 或者 .orion 地址 + 服务器 - 或者 .onion 地址 该端口号无效 该主机名无效 %2$d 个中的 %1$d 个账户已连接 @@ -550,7 +555,7 @@ - 广播最后打开该应用的时间 + 广播最后使用应用的时间 让你的所有联系人知道你使用畅聊的时间 隐私 主题 @@ -562,8 +567,8 @@ 接收到的消息使用绿色背景 无法连接到 OpenKeychain 此设备不再使用 - 计算机 - 移动电话 + 电脑 + 手机 平板 浏览器 控制台 @@ -639,6 +644,7 @@ 相应的对话已关闭。 联系人已屏蔽 陌生人也通知 + 提醒来自陌生人的消息与通话 已收到陌生人的信息 屏蔽陌生人 屏蔽整个域名 @@ -737,9 +743,14 @@ 连接问题 此通知类别用于显示一旦帐户连接出现问题的通知。 消息 + 通话 消息 + 来电 + 正在进行的通话 无声消息 此通知组用于显示不应触发任何声音的通知。 例如,当在另一个设备上激活时(宽限期)。 + 消息通知设置 + 来电通知设置 重要性,声音,振动 视频压缩 查看媒体文件 @@ -775,7 +786,7 @@ 确定放弃注册? - 正在验证...... + 正在验证..... 请求短信... 验证码错误。 验证码已失效 @@ -832,7 +843,7 @@ 允许任何成员修改主题 允许任何成员邀请其他人 允许任何成员修改主题 - 拥有者可修改话题 + 拥有者可修改主题 管理员可修改主题 所有者可以邀请其他人 允许任何成员邀请其他人 @@ -847,7 +858,7 @@ 发现群聊 搜索群聊 可能侵犯隐私! - search.jabber.network。

的第三方服务。使用此功能会将您的IP地址和搜索字词传输到该服务。 有关更多信息,请参见其Privacy Policy。]]>
+ search.jabber.network。

的第三方服务。使用此功能会将您的IP地址和搜索字词传输到该服务。 有关更多信息,请参见其Privacy Policy。]]>
我已有账户 添加已有账户 注册新账户 @@ -868,10 +879,38 @@ jabber.network 本地服务器 大多数用户应该选择“ jabber.network”以从整个XMPP生态系统中获得更好的建议。 - 频道发现方法 + 群聊发现方法 备份 关于 请启用一个帐户 + 拨打电话 + 来电 + 视频来电 + 正在连接 + 已连接 + 正在接受通话 + 正在结束通话 + 接电话 + 忽略 + 正在确定设备位置 + 正在响铃 + 忙碌 + 无法接通来电 + 撤销的通话 + 程序错误 + 挂断 + 正在进行的通话 + 正在进行的视频通话 + 禁用Tor以拨打电话 + 来电 + 来电%s + 去电 + 去电%s + 未接电话 + 语音通话 + 视频通话 + 麦克风不可用 + 在同一时间只能打一通电话 查看%1$d成员 From 0d4b1757607157492e4491a127f7700292fe6bc6 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 29 Apr 2020 08:51:38 +0200 Subject: [PATCH 12/19] better failure behaviour after direct init from jitsi --- .../conversations/ui/RtpSessionActivity.java | 73 ++++++++++++++----- .../xmpp/jingle/JingleRtpConnection.java | 24 +++--- 2 files changed, 67 insertions(+), 30 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index acd9d151e..2cdba2d0c 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -21,6 +21,7 @@ import android.view.WindowManager; import android.widget.Toast; import com.google.common.base.Optional; +import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; @@ -29,6 +30,7 @@ import org.webrtc.VideoTrack; import java.lang.ref.WeakReference; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Set; @@ -315,7 +317,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe if (remoteVideo.isPresent()) { remoteVideo.get().removeSink(binding.remoteVideo); } - final Optional localVideo = jingleRtpConnection.geLocalVideoTrack(); + final Optional localVideo = jingleRtpConnection.getLocalVideoTrack(); if (localVideo.isPresent()) { localVideo.get().removeSink(binding.localVideo); } @@ -385,6 +387,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe finish(); return true; } + final Set media = getMedia(); if (currentState == RtpEndUserState.INCOMING_CALL) { getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } @@ -393,16 +396,14 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } binding.with.setText(getWith().getDisplayName()); updateVideoViews(currentState); - updateStateDisplay(currentState); - updateButtonConfiguration(currentState); + updateStateDisplay(currentState, media); + updateButtonConfiguration(currentState, media); updateProfilePicture(currentState); return false; } private void reInitializeActivityWithRunningRapSession(final Account account, Jid with, String sessionId) { - runOnUiThread(() -> { - initializeActivityWithRunningRtpSession(account, with, sessionId); - }); + runOnUiThread(() -> initializeActivityWithRunningRtpSession(account, with, sessionId)); final Intent intent = new Intent(Intent.ACTION_VIEW); intent.putExtra(EXTRA_ACCOUNT, account.getJid().toEscapedString()); intent.putExtra(EXTRA_WITH, with.toEscapedString()); @@ -421,9 +422,14 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } private void updateStateDisplay(final RtpEndUserState state) { + updateStateDisplay(state, Collections.emptySet()); + } + + private void updateStateDisplay(final RtpEndUserState state, final Set media) { switch (state) { case INCOMING_CALL: - if (getMedia().contains(Media.VIDEO)) { + Preconditions.checkArgument(media.size() > 0, "Media must not be empty"); + if (media.contains(Media.VIDEO)) { setTitle(R.string.rtp_state_incoming_video_call); } else { setTitle(R.string.rtp_state_incoming_call); @@ -467,11 +473,19 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } private void updateProfilePicture(final RtpEndUserState state) { + updateProfilePicture(state, null); + } + + private void updateProfilePicture(final RtpEndUserState state, final Contact contact) { if (state == RtpEndUserState.INCOMING_CALL || state == RtpEndUserState.ACCEPTING_CALL) { final boolean show = getResources().getBoolean(R.bool.show_avatar_incoming_call); if (show) { binding.contactPhoto.setVisibility(View.VISIBLE); - AvatarWorkerTask.loadAvatar(getWith(), binding.contactPhoto, R.dimen.publish_avatar_size); + if (contact == null) { + AvatarWorkerTask.loadAvatar(getWith(), binding.contactPhoto, R.dimen.publish_avatar_size); + } else { + AvatarWorkerTask.loadAvatar(contact, binding.contactPhoto, R.dimen.publish_avatar_size); + } } else { binding.contactPhoto.setVisibility(View.GONE); } @@ -484,8 +498,12 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe return requireRtpConnection().getMedia(); } - @SuppressLint("RestrictedApi") private void updateButtonConfiguration(final RtpEndUserState state) { + updateButtonConfiguration(state, Collections.emptySet()); + } + + @SuppressLint("RestrictedApi") + private void updateButtonConfiguration(final RtpEndUserState state, final Set media) { if (state == RtpEndUserState.ENDING_CALL || isPictureInPicture()) { this.binding.rejectCall.setVisibility(View.INVISIBLE); this.binding.endCall.setVisibility(View.INVISIBLE); @@ -519,7 +537,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe this.binding.endCall.setVisibility(View.VISIBLE); this.binding.acceptCall.setVisibility(View.INVISIBLE); } - updateInCallButtonConfiguration(state); + updateInCallButtonConfiguration(state, media); } private boolean isPictureInPicture() { @@ -531,13 +549,14 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } private void updateInCallButtonConfiguration() { - updateInCallButtonConfiguration(requireRtpConnection().getEndUserState()); + updateInCallButtonConfiguration(requireRtpConnection().getEndUserState(), requireRtpConnection().getMedia()); } @SuppressLint("RestrictedApi") - private void updateInCallButtonConfiguration(final RtpEndUserState state) { + private void updateInCallButtonConfiguration(final RtpEndUserState state, final Set media) { if (state == RtpEndUserState.CONNECTED && !isPictureInPicture()) { - if (getMedia().contains(Media.VIDEO)) { + Preconditions.checkArgument(media.size() > 0, "Media must not be empty"); + if (media.contains(Media.VIDEO)) { updateInCallButtonConfigurationVideo(requireRtpConnection().isVideoEnabled()); } else { final AppRTCAudioManager audioManager = requireRtpConnection().getAudioManager(); @@ -655,7 +674,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe binding.pipLocalMicOffIndicator.setVisibility(View.GONE); return; } - final Optional localVideoTrack = requireRtpConnection().geLocalVideoTrack(); + final Optional localVideoTrack = getLocalVideoTrack(); if (localVideoTrack.isPresent() && !isPictureInPicture()) { ensureSurfaceViewRendererIsSetup(binding.localVideo); //paint local view over remote view @@ -665,7 +684,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } else { binding.localVideo.setVisibility(View.GONE); } - final Optional remoteVideoTrack = requireRtpConnection().getRemoteVideoTrack(); + final Optional remoteVideoTrack = getRemoteVideoTrack(); if (remoteVideoTrack.isPresent()) { ensureSurfaceViewRendererIsSetup(binding.remoteVideo); remoteVideoTrack.get().addSink(binding.remoteVideo); @@ -688,6 +707,22 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } } + private Optional getLocalVideoTrack() { + final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; + if (connection == null) { + return Optional.absent(); + } + return connection.getLocalVideoTrack(); + } + + private Optional getRemoteVideoTrack() { + final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; + if (connection == null) { + return Optional.absent(); + } + return connection.getRemoteVideoTrack(); + } + private void disableMicrophone(View view) { JingleRtpConnection rtpConnection = requireRtpConnection(); rtpConnection.setMicrophoneEnabled(false); @@ -762,16 +797,18 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe return; } final AbstractJingleConnection.Id id = requireRtpConnection().getId(); + final Set media = getMedia(); + final Contact contact = getWith(); if (account == id.account && id.with.equals(with) && id.sessionId.equals(sessionId)) { if (state == RtpEndUserState.ENDED) { finish(); return; } runOnUiThread(() -> { - updateStateDisplay(state); - updateButtonConfiguration(state); + updateStateDisplay(state, media); + updateButtonConfiguration(state, media); updateVideoViews(state); - updateProfilePicture(state); + updateProfilePicture(state, contact); }); if (END_CARD.contains(state)) { resetIntent(account, with, state, requireRtpConnection().getMedia()); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 7817c2d2e..4dce736a8 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -6,6 +6,7 @@ import android.util.Log; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.base.Strings; +import com.google.common.base.Throwables; import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -52,7 +53,6 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web public static final List STATES_SHOWING_ONGOING_CALL = Arrays.asList( State.PROCEED, - State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED ); @@ -369,8 +369,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web ); try { this.webRTCWrapper.setRemoteDescription(answer).get(); - } catch (Exception e) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to set remote description after receiving session-accept", e); + } catch (final Exception e) { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to set remote description after receiving session-accept", Throwables.getRootCause(e)); webRTCWrapper.close(); sendSessionTerminate(Reason.FAILED_APPLICATION); } @@ -420,9 +420,10 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription); sendSessionAccept(respondingRtpContentMap); this.webRTCWrapper.setLocalDescription(webRTCSessionDescription); - } catch (Exception e) { - Log.d(Config.LOGTAG, "unable to send session accept", e); - + } catch (final Exception e) { + Log.d(Config.LOGTAG, "unable to send session accept", Throwables.getRootCause(e)); + webRTCWrapper.close(); + sendSessionTerminate(Reason.FAILED_APPLICATION); } } @@ -791,10 +792,11 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } public Set getMedia() { - if (isInState(State.NULL)) { + final State current = getState(); + if (current == State.NULL) { throw new IllegalStateException("RTP connection has not been initialized yet"); } - if (isInState(State.PROPOSED, State.PROCEED)) { + if (Arrays.asList(State.PROPOSED, State.PROCEED).contains(current)) { return Preconditions.checkNotNull(this.proposedMedia, "RTP connection has not been initialized properly"); } final RtpContentMap initiatorContentMap = initiatorRtpContentMap; @@ -1028,9 +1030,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private void updateEndUserState() { final RtpEndUserState endUserState = getEndUserState(); - final RtpContentMap contentMap = initiatorRtpContentMap; - final Set media = contentMap == null ? Collections.emptySet() : contentMap.getMedia(); - jingleConnectionManager.toneManager.transition(isInitiator(), endUserState, media); + jingleConnectionManager.toneManager.transition(isInitiator(), endUserState, getMedia()); xmppConnectionService.notifyJingleRtpConnectionUpdate(id.account, id.with, id.sessionId, endUserState); } @@ -1147,7 +1147,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web return TERMINATED.contains(this.state); } - public Optional geLocalVideoTrack() { + public Optional getLocalVideoTrack() { return webRTCWrapper.getLocalVideoTrack(); } From 333f509e5354408095505b4a1dcd3da104492892 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 29 Apr 2020 09:10:15 +0200 Subject: [PATCH 13/19] =?UTF-8?q?synchronize=20public=20WebRTCWrapper=20me?= =?UTF-8?q?thods=20so=20closes=20don=E2=80=99t=20race?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../xmpp/jingle/WebRTCWrapper.java | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index ffbd8cf70..bb5c1ea24 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -158,7 +158,7 @@ public class WebRTCWrapper { private EglBase eglBase = null; private CapturerChoice capturerChoice; - public WebRTCWrapper(final EventCallback eventCallback) { + WebRTCWrapper(final EventCallback eventCallback) { this.eventCallback = eventCallback; } @@ -175,7 +175,7 @@ public class WebRTCWrapper { }); } - public void initializePeerConnection(final Set media, final List iceServers) throws InitializationException { + synchronized void initializePeerConnection(final Set media, final List iceServers) throws InitializationException { Preconditions.checkState(this.eglBase != null); Preconditions.checkNotNull(media); Preconditions.checkArgument(media.size() > 0, "media can not be empty when initializing peer connection"); @@ -224,7 +224,7 @@ public class WebRTCWrapper { this.peerConnection = peerConnection; } - public void close() { + synchronized void close() { final PeerConnection peerConnection = this.peerConnection; final CapturerChoice capturerChoice = this.capturerChoice; final AppRTCAudioManager audioManager = this.appRTCAudioManager; @@ -259,7 +259,7 @@ public class WebRTCWrapper { } } - void verifyClosed() { + synchronized void verifyClosed() { if (this.peerConnection != null || this.eglBase != null || this.localVideoTrack != null @@ -286,7 +286,7 @@ public class WebRTCWrapper { audioTrack.setEnabled(enabled); } - public boolean isVideoEnabled() { + boolean isVideoEnabled() { final VideoTrack videoTrack = this.localVideoTrack; if (videoTrack == null) { throw new IllegalStateException("Local video track does not exist"); @@ -294,7 +294,7 @@ public class WebRTCWrapper { return videoTrack.enabled(); } - public void setVideoEnabled(final boolean enabled) { + void setVideoEnabled(final boolean enabled) { final VideoTrack videoTrack = this.localVideoTrack; if (videoTrack == null) { throw new IllegalStateException("Local video track does not exist"); @@ -302,7 +302,7 @@ public class WebRTCWrapper { videoTrack.setEnabled(enabled); } - public ListenableFuture createOffer() { + ListenableFuture createOffer() { return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> { final SettableFuture future = SettableFuture.create(); peerConnection.createOffer(new CreateSdpObserver() { @@ -313,7 +313,6 @@ public class WebRTCWrapper { @Override public void onCreateFailure(String s) { - Log.d(Config.LOGTAG, "create failure" + s); future.setException(new IllegalStateException("Unable to create offer: " + s)); } }, new MediaConstraints()); @@ -321,7 +320,7 @@ public class WebRTCWrapper { }, MoreExecutors.directExecutor()); } - public ListenableFuture createAnswer() { + ListenableFuture createAnswer() { return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> { final SettableFuture future = SettableFuture.create(); peerConnection.createAnswer(new CreateSdpObserver() { @@ -339,7 +338,7 @@ public class WebRTCWrapper { }, MoreExecutors.directExecutor()); } - public ListenableFuture setLocalDescription(final SessionDescription sessionDescription) { + ListenableFuture setLocalDescription(final SessionDescription sessionDescription) { Log.d(EXTENDED_LOGGING_TAG, "setting local description:"); for (final String line : sessionDescription.description.split(eu.siacs.conversations.xmpp.jingle.SessionDescription.LINE_DIVIDER)) { Log.d(EXTENDED_LOGGING_TAG, line); @@ -363,7 +362,7 @@ public class WebRTCWrapper { }, MoreExecutors.directExecutor()); } - public ListenableFuture setRemoteDescription(final SessionDescription sessionDescription) { + ListenableFuture setRemoteDescription(final SessionDescription sessionDescription) { Log.d(EXTENDED_LOGGING_TAG, "setting remote description:"); for (final String line : sessionDescription.description.split(eu.siacs.conversations.xmpp.jingle.SessionDescription.LINE_DIVIDER)) { Log.d(EXTENDED_LOGGING_TAG, line); @@ -396,7 +395,7 @@ public class WebRTCWrapper { } } - public void addIceCandidate(IceCandidate iceCandidate) { + void addIceCandidate(IceCandidate iceCandidate) { requirePeerConnection().addIceCandidate(iceCandidate); } @@ -447,11 +446,11 @@ public class WebRTCWrapper { return this.eglBase.getEglBaseContext(); } - public Optional getLocalVideoTrack() { + Optional getLocalVideoTrack() { return Optional.fromNullable(this.localVideoTrack); } - public Optional getRemoteVideoTrack() { + Optional getRemoteVideoTrack() { return Optional.fromNullable(this.remoteVideoTrack); } @@ -471,7 +470,7 @@ public class WebRTCWrapper { return context; } - public AppRTCAudioManager getAudioManager() { + AppRTCAudioManager getAudioManager() { return appRTCAudioManager; } @@ -512,7 +511,7 @@ public class WebRTCWrapper { } } - public static class InitializationException extends Exception { + static class InitializationException extends Exception { private InitializationException(String message) { super(message); @@ -523,12 +522,12 @@ public class WebRTCWrapper { private final CameraVideoCapturer cameraVideoCapturer; private final CameraEnumerationAndroid.CaptureFormat captureFormat; - public CapturerChoice(CameraVideoCapturer cameraVideoCapturer, CameraEnumerationAndroid.CaptureFormat captureFormat) { + CapturerChoice(CameraVideoCapturer cameraVideoCapturer, CameraEnumerationAndroid.CaptureFormat captureFormat) { this.cameraVideoCapturer = cameraVideoCapturer; this.captureFormat = captureFormat; } - public int getFrameRate() { + int getFrameRate() { return Math.max(captureFormat.framerate.min, Math.min(CAPTURING_MAX_FRAME_RATE, captureFormat.framerate.max)); } } From 54ca3fb02022879bf98a17ebd64d1b75ea2bb7a0 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 29 Apr 2020 09:22:49 +0200 Subject: [PATCH 14/19] release video after end card reached. fixes video call retry --- .../java/eu/siacs/conversations/ui/RtpSessionActivity.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 2cdba2d0c..335063b2c 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -645,7 +645,9 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe private void updateVideoViews(final RtpEndUserState state) { if (END_CARD.contains(state) || state == RtpEndUserState.ENDING_CALL) { binding.localVideo.setVisibility(View.GONE); + binding.localVideo.release(); binding.remoteVideo.setVisibility(View.GONE); + binding.remoteVideo.release(); binding.pipLocalMicOffIndicator.setVisibility(View.GONE); if (isPictureInPicture()) { binding.appBarLayout.setVisibility(View.GONE); @@ -811,7 +813,9 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe updateProfilePicture(state, contact); }); if (END_CARD.contains(state)) { - resetIntent(account, with, state, requireRtpConnection().getMedia()); + final JingleRtpConnection rtpConnection = requireRtpConnection(); + resetIntent(account, with, state, rtpConnection.getMedia()); + releaseVideoTracks(rtpConnection); this.rtpConnectionReference = null; } } else { From a49d69c87844bccbdfb40a6f0ba58dee5655852f Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 29 Apr 2020 10:36:54 +0200 Subject: [PATCH 15/19] parse candidates from session-init and session-accept --- .../xmpp/jingle/JingleRtpConnection.java | 63 +++++++++++-------- 1 file changed, 37 insertions(+), 26 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 4dce736a8..275d0e427 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -49,14 +49,12 @@ import rocks.xmpp.addr.Jid; public class JingleRtpConnection extends AbstractJingleConnection implements WebRTCWrapper.EventCallback { - private static final long BUSY_TIME_OUT = 30; - public static final List STATES_SHOWING_ONGOING_CALL = Arrays.asList( State.PROCEED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED ); - + private static final long BUSY_TIME_OUT = 30; private static final List TERMINATED = Arrays.asList( State.TERMINATED_SUCCESS, State.TERMINATED_DECLINED_OR_BUSY, @@ -236,31 +234,40 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web if (identificationTags.size() == 0) { Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": no identification tags found in initial offer. we won't be able to calculate mLineIndices"); } - for (final Map.Entry content : contentMap.contents.entrySet()) { - final String ufrag = content.getValue().transport.getAttribute("ufrag"); - for (final IceUdpTransportInfo.Candidate candidate : content.getValue().transport.getCandidates()) { - final String sdp; - try { - sdp = candidate.toSdpAttribute(ufrag); - } catch (IllegalArgumentException e) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring invalid ICE candidate " + e.getMessage()); - continue; - } - final String sdpMid = content.getKey(); - final int mLineIndex = identificationTags.indexOf(sdpMid); - final IceCandidate iceCandidate = new IceCandidate(sdpMid, mLineIndex, sdp); - if (isInState(State.SESSION_ACCEPTED)) { - Log.d(Config.LOGTAG, "received candidate: " + iceCandidate); - this.webRTCWrapper.addIceCandidate(iceCandidate); - } else { - this.pendingIceCandidates.offer(iceCandidate); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": put ICE candidate on backlog"); - } + receiveCandidates(identificationTags, contentMap.contents.entrySet()); + } else { + if (isTerminated()) { + respondOk(jinglePacket); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring out-of-order transport info; we where already terminated"); + } else { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received transport info while in state=" + this.state); + terminateWithOutOfOrder(jinglePacket); + } + } + } + + private void receiveCandidates(final List identificationTags, final Set> contents) { + for (final Map.Entry content : contents) { + final String ufrag = content.getValue().transport.getAttribute("ufrag"); + for (final IceUdpTransportInfo.Candidate candidate : content.getValue().transport.getCandidates()) { + final String sdp; + try { + sdp = candidate.toSdpAttribute(ufrag); + } catch (IllegalArgumentException e) { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring invalid ICE candidate " + e.getMessage()); + continue; + } + final String sdpMid = content.getKey(); + final int mLineIndex = identificationTags.indexOf(sdpMid); + final IceCandidate iceCandidate = new IceCandidate(sdpMid, mLineIndex, sdp); + if (isInState(State.SESSION_ACCEPTED)) { + Log.d(Config.LOGTAG, "received candidate: " + iceCandidate); + this.webRTCWrapper.addIceCandidate(iceCandidate); + } else { + this.pendingIceCandidates.offer(iceCandidate); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": put ICE candidate on backlog"); } } - } else { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received transport info while in state=" + this.state); - terminateWithOutOfOrder(jinglePacket); } } @@ -302,6 +309,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } if (transition(target, () -> this.initiatorRtpContentMap = contentMap)) { respondOk(jinglePacket); + final List identificationTags = contentMap.group == null ? Collections.emptyList() : contentMap.group.getIdentificationTags(); + receiveCandidates(identificationTags, contentMap.contents.entrySet()); if (target == State.SESSION_INITIALIZED_PRE_APPROVED) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": automatically accepting session-initiate"); sendSessionAccept(); @@ -346,6 +355,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web if (transition(State.SESSION_ACCEPTED)) { respondOk(jinglePacket); receiveSessionAccept(contentMap); + final List identificationTags = contentMap.group == null ? Collections.emptyList() : contentMap.group.getIdentificationTags(); + receiveCandidates(identificationTags, contentMap.contents.entrySet()); } else { Log.d(Config.LOGTAG, String.format("%s: received session-accept while in state %s", id.account.getJid().asBareJid(), state)); respondOk(jinglePacket); From 52d416c6e626291c5caa7b9b691193e24df0dd72 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 29 Apr 2020 14:53:52 +0200 Subject: [PATCH 16/19] version bump to 2.8.1 + changelog --- CHANGELOG.md | 4 ++++ build.gradle | 4 ++-- fastlane/metadata/android/en-US/changelogs/380.txt | 2 ++ 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/380.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d5d92537..1d1e8cc13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +### Version 2.8.1 +* Audible feedback (dialing, call started, call ended) for voice calls. +* Fixed issue with retrying failed video call + ### Version 2.8.0 * Audio/Video calls (Requires server support in form of STUN and TURN servers discoverable via XEP-0215) diff --git a/build.gradle b/build.gradle index 162dc26ee..0b262eecd 100644 --- a/build.gradle +++ b/build.gradle @@ -95,8 +95,8 @@ android { defaultConfig { minSdkVersion 16 targetSdkVersion 28 - versionCode 379 - versionName "2.8.0" + versionCode 380 + versionName "2.8.1" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId diff --git a/fastlane/metadata/android/en-US/changelogs/380.txt b/fastlane/metadata/android/en-US/changelogs/380.txt new file mode 100644 index 000000000..a2df5e828 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/380.txt @@ -0,0 +1,2 @@ +* Audible feedback (dialing, call started, call ended) for voice calls. +* Fixed issue with retrying failed video call From 8a586527c4c4ea7366740bfc8b31928ac83bc23a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 29 Apr 2020 15:32:27 +0200 Subject: [PATCH 17/19] check if setting local description was succesful --- .../siacs/conversations/xmpp/jingle/JingleRtpConnection.java | 4 ++-- .../eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 275d0e427..4359f39c3 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -430,7 +430,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription); sendSessionAccept(respondingRtpContentMap); - this.webRTCWrapper.setLocalDescription(webRTCSessionDescription); + this.webRTCWrapper.setLocalDescription(webRTCSessionDescription).get(); } catch (final Exception e) { Log.d(Config.LOGTAG, "unable to send session accept", Throwables.getRootCause(e)); webRTCWrapper.close(); @@ -647,7 +647,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web sendSessionInitiate(rtpContentMap, targetState); this.webRTCWrapper.setLocalDescription(webRTCSessionDescription).get(); } catch (final Exception e) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to sendSessionInitiate", e); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to sendSessionInitiate", Throwables.getRootCause(e)); webRTCWrapper.close(); if (isInState(targetState)) { sendSessionTerminate(Reason.FAILED_APPLICATION); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index bb5c1ea24..17c1b77fe 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -352,8 +352,7 @@ public class WebRTCWrapper { } @Override - public void onSetFailure(String s) { - Log.d(Config.LOGTAG, "unable to set local " + s); + public void onSetFailure(final String s) { future.setException(new IllegalArgumentException("unable to set local session description: " + s)); } From deae2b109f59426d95847deae1a97b1f78ed7e46 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 29 Apr 2020 15:54:02 +0200 Subject: [PATCH 18/19] do not crash UI after ignoring improperly formatted jingle init --- .../xmpp/jingle/JingleRtpConnection.java | 4 +++- .../conversations/xmpp/jingle/RtpContentMap.java | 13 ++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 4359f39c3..e46ce78ec 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -283,9 +283,9 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web contentMap.requireContentDescriptions(); contentMap.requireDTLSFingerprint(); } catch (final RuntimeException e) { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents", Throwables.getRootCause(e)); respondOk(jinglePacket); sendSessionTerminate(Reason.of(e), e.getMessage()); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents", e); return; } Log.d(Config.LOGTAG, "processing session-init with " + contentMap.contents.size() + " contents"); @@ -813,6 +813,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web final RtpContentMap initiatorContentMap = initiatorRtpContentMap; if (initiatorContentMap != null) { return initiatorContentMap.getMedia(); + } else if (isTerminated()) { + return Collections.emptySet(); //we might fail before we ever got a chance to set media } else { return Preconditions.checkNotNull(this.proposedMedia, "RTP connection has not been initialized properly"); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java index da48d017f..9150d393e 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java @@ -59,7 +59,7 @@ public class RtpContentMap { })); } - public void requireContentDescriptions() { + void requireContentDescriptions() { if (this.contents.size() == 0) { throw new IllegalStateException("No contents available"); } @@ -70,7 +70,7 @@ public class RtpContentMap { } } - public void requireDTLSFingerprint() { + void requireDTLSFingerprint() { if (this.contents.size() == 0) { throw new IllegalStateException("No contents available"); } @@ -80,10 +80,13 @@ public class RtpContentMap { if (fingerprint == null || Strings.isNullOrEmpty(fingerprint.getContent()) || Strings.isNullOrEmpty(fingerprint.getHash())) { throw new SecurityException(String.format("Use of DTLS-SRTP (XEP-0320) is required for content %s", entry.getKey())); } + if (Strings.isNullOrEmpty(fingerprint.getSetup())) { + throw new SecurityException(String.format("Use of DTLS-SRTP (XEP-0320) is required for content %s but missing setup attribute", entry.getKey())); + } } } - public JinglePacket toJinglePacket(final JinglePacket.Action action, final String sessionId) { + JinglePacket toJinglePacket(final JinglePacket.Action action, final String sessionId) { final JinglePacket jinglePacket = new JinglePacket(action, sessionId); if (this.group != null) { jinglePacket.addGroup(this.group); @@ -99,7 +102,7 @@ public class RtpContentMap { return jinglePacket; } - public RtpContentMap transportInfo(final String contentName, final IceUdpTransportInfo.Candidate candidate) { + RtpContentMap transportInfo(final String contentName, final IceUdpTransportInfo.Candidate candidate) { final RtpContentMap.DescriptionTransport descriptionTransport = contents.get(contentName); final IceUdpTransportInfo transportInfo = descriptionTransport == null ? null : descriptionTransport.transport; if (transportInfo == null) { @@ -115,7 +118,7 @@ public class RtpContentMap { public final RtpDescription description; public final IceUdpTransportInfo transport; - public DescriptionTransport(final RtpDescription description, final IceUdpTransportInfo transport) { + DescriptionTransport(final RtpDescription description, final IceUdpTransportInfo transport) { this.description = description; this.transport = transport; } From f106cbccf5f26b14e1b169ec5a64a90b5da04844 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 29 Apr 2020 16:28:31 +0200 Subject: [PATCH 19/19] bump version code to 381 (2.8.1) --- build.gradle | 2 +- fastlane/metadata/android/en-US/changelogs/{380.txt => 381.txt} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename fastlane/metadata/android/en-US/changelogs/{380.txt => 381.txt} (100%) diff --git a/build.gradle b/build.gradle index 0b262eecd..e2d8f11d9 100644 --- a/build.gradle +++ b/build.gradle @@ -95,7 +95,7 @@ android { defaultConfig { minSdkVersion 16 targetSdkVersion 28 - versionCode 380 + versionCode 381 versionName "2.8.1" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" diff --git a/fastlane/metadata/android/en-US/changelogs/380.txt b/fastlane/metadata/android/en-US/changelogs/381.txt similarity index 100% rename from fastlane/metadata/android/en-US/changelogs/380.txt rename to fastlane/metadata/android/en-US/changelogs/381.txt