From d584ffee7d951212ba379b2425571ec8c398c189 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 25 Aug 2022 07:54:01 +0200 Subject: [PATCH 001/101] try to improve 'sync bookmarks' description --- src/main/res/values/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 8b1fd1de38f1d4c4030ae3985b9a28bf09fdfc7e..d4e2c0d57d9f0a896274fc63e31adafe312c9edd 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -292,8 +292,8 @@ Enable quiet hours Notifications will be silenced during quiet hours Other - Synchronize with bookmarks - Join group chats automatically if the bookmark says so + Synchronize bookmarks + Set “autojoin” flag when entering or leaving a MUC and react to modifications made by other clients. OMEMO fingerprint copied to clipboard You are banned from this group chat This group chat is members only From ddd08bfe5fb16125306a5d59f43535af80d96383 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 25 Aug 2022 17:12:39 +0200 Subject: [PATCH 002/101] issue self ping + rejoin on muc status code 333 --- .../conversations/entities/MucOptions.java | 1 + .../conversations/generator/IqGenerator.java | 4 ++-- .../conversations/parser/PresenceParser.java | 20 +++++++++++++++---- .../ui/ConversationFragment.java | 3 +++ src/main/res/values/strings.xml | 1 + 5 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java index 34f437e1c91e3d272f2d171e68405cf258f96bf3..060b1b6f65ceaf9335ec8439bbdd8dcdfaa26ca3 100644 --- a/src/main/java/eu/siacs/conversations/entities/MucOptions.java +++ b/src/main/java/eu/siacs/conversations/entities/MucOptions.java @@ -725,6 +725,7 @@ public class MucOptions { SHUTDOWN, DESTROYED, INVALID_NICK, + TECHNICAL_PROBLEMS, UNKNOWN, NON_ANONYMOUS } diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index 2776aa4f058aef2fa4d0a292b3c6c1f452123abd..6b87cb36ddbe98a4b2415920c4288a9b444f152d 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -136,8 +136,8 @@ public class IqGenerator extends AbstractGenerator { return publish(Namespace.NICK, item); } - public IqPacket deleteNode(String node) { - IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + public IqPacket deleteNode(final String node) { + final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB_OWNER); pubsub.addChild("delete").setAttribute("node", node); return packet; diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java index 341e8d9a0b9d1c8335b4932c9afe8a6ee0911d12..8ad582b17679165be3e6dc903d08492dd1d11a03 100644 --- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java +++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java @@ -56,7 +56,8 @@ public class PresenceParser extends AbstractParser implements } private void processConferencePresence(PresencePacket packet, Conversation conversation) { - MucOptions mucOptions = conversation.getMucOptions(); + final Account account = conversation.getAccount(); + final MucOptions mucOptions = conversation.getMucOptions(); final Jid jid = conversation.getAccount().getJid(); final Jid from = packet.getFrom(); if (!from.isBareJid()) { @@ -93,7 +94,7 @@ public class PresenceParser extends AbstractParser implements axolotlService.fetchDeviceIds(user.getRealJid()); } if (codes.contains(MucOptions.STATUS_CODE_ROOM_CREATED) && mucOptions.autoPushConfiguration()) { - Log.d(Config.LOGTAG,mucOptions.getAccount().getJid().asBareJid() + Log.d(Config.LOGTAG,account.getJid().asBareJid() +": room '" +mucOptions.getConversation().getJid().asBareJid() +"' created. pushing default configuration"); @@ -138,13 +139,24 @@ public class PresenceParser extends AbstractParser implements final Jid alternate = destroy == null ? null : InvalidJid.getNullForInvalid(destroy.getAttributeAsJid("jid")); mucOptions.setError(MucOptions.Error.DESTROYED); if (alternate != null) { - Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": muc destroyed. alternate location " + alternate); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": muc destroyed. alternate location " + alternate); } } else if (codes.contains(MucOptions.STATUS_CODE_SHUTDOWN) && fullJidMatches) { mucOptions.setError(MucOptions.Error.SHUTDOWN); } else if (codes.contains(MucOptions.STATUS_CODE_SELF_PRESENCE)) { if (codes.contains(MucOptions.STATUS_CODE_TECHNICAL_REASONS)) { - mucOptions.setError(MucOptions.Error.UNKNOWN); + final boolean wasOnline = mucOptions.online(); + mucOptions.setError(MucOptions.Error.TECHNICAL_PROBLEMS); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": received status code 333 in room " + + mucOptions.getConversation().getJid().asBareJid() + + " online=" + + wasOnline); + if (wasOnline) { + mXmppConnectionService.mucSelfPingAndRejoin(conversation); + } } else if (codes.contains(MucOptions.STATUS_CODE_KICKED)) { mucOptions.setError(MucOptions.Error.KICKED); } else if (codes.contains(MucOptions.STATUS_CODE_BANNED)) { diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 0471c014f3b570fd8ff6f1bea2bb077d13d4b25e..3b923adce54738d6e3f8603c7b080b1629ef48de 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -2721,6 +2721,9 @@ public class ConversationFragment extends XmppFragment case KICKED: showSnackbar(R.string.conference_kicked, R.string.join, joinMuc); break; + case TECHNICAL_PROBLEMS: + showSnackbar(R.string.conference_technical_problems, R.string.try_again, joinMuc); + break; case UNKNOWN: showSnackbar(R.string.conference_unknown_error, R.string.try_again, joinMuc); break; diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index d4e2c0d57d9f0a896274fc63e31adafe312c9edd..299c57b3367596be7e495af42eb82d3643147189 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -301,6 +301,7 @@ You have been kicked from this group chat The group chat was shut down You are no longer in this group chat + You left this group chat due to technical reasons using account %s hosted on %s Checking %s on HTTP host From e439c223ee07e8ee10394db601cad43320c000e9 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 25 Aug 2022 19:22:40 +0200 Subject: [PATCH 003/101] add overflow menu action to delete own avatar --- .../crypto/axolotl/AxolotlService.java | 8 +-- .../conversations/generator/IqGenerator.java | 17 +++-- .../conversations/parser/MessageParser.java | 2 + .../services/XmppConnectionService.java | 72 +++++++++++++++++-- .../ui/PublishProfilePictureActivity.java | 23 +++++- .../eu/siacs/conversations/xml/Namespace.java | 2 + .../menu/activity_publish_profile_picture.xml | 10 +++ src/main/res/values/strings.xml | 1 + 8 files changed, 121 insertions(+), 14 deletions(-) create mode 100644 src/main/res/menu/activity_publish_profile_picture.xml diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index 4da07af9faf3ea992d318489e9cfb745f5177f0a..faef2e098dfa2ff9d501bc278625759575183740 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -708,11 +708,11 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } public void deleteOmemoIdentity() { - final String node = AxolotlService.PEP_BUNDLES + ":" + getOwnDeviceId(); - final IqPacket deleteBundleNode = mXmppConnectionService.getIqGenerator().deleteNode(node); - mXmppConnectionService.sendIqPacket(account, deleteBundleNode, null); + mXmppConnectionService.deletePepNode( + account, AxolotlService.PEP_BUNDLES + ":" + getOwnDeviceId()); final Set ownDeviceIds = getOwnDeviceIds(); - publishDeviceIdsAndRefineAccessModel(ownDeviceIds == null ? Collections.emptySet() : ownDeviceIds); + publishDeviceIdsAndRefineAccessModel( + ownDeviceIds == null ? Collections.emptySet() : ownDeviceIds); } public List getCryptoTargets(Conversation conversation) { diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index 6b87cb36ddbe98a4b2415920c4288a9b444f152d..52a19eaa4cb26fb8b44db9048433bc940c90a509 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -156,9 +156,9 @@ public class IqGenerator extends AbstractGenerator { public IqPacket publishAvatar(Avatar avatar, Bundle options) { final Element item = new Element("item"); item.setAttribute("id", avatar.sha1sum); - final Element data = item.addChild("data", "urn:xmpp:avatar:data"); + final Element data = item.addChild("data", Namespace.AVATAR_DATA); data.setContent(avatar.image); - return publish("urn:xmpp:avatar:data", item, options); + return publish(Namespace.AVATAR_DATA, item, options); } public IqPacket publishElement(final String namespace, final Element element, String id, final Bundle options) { @@ -172,20 +172,20 @@ public class IqGenerator extends AbstractGenerator { final Element item = new Element("item"); item.setAttribute("id", avatar.sha1sum); final Element metadata = item - .addChild("metadata", "urn:xmpp:avatar:metadata"); + .addChild("metadata", Namespace.AVATAR_METADATA); final Element info = metadata.addChild("info"); info.setAttribute("bytes", avatar.size); info.setAttribute("id", avatar.sha1sum); info.setAttribute("height", avatar.height); info.setAttribute("width", avatar.height); info.setAttribute("type", avatar.type); - return publish("urn:xmpp:avatar:metadata", item, options); + return publish(Namespace.AVATAR_METADATA, item, options); } public IqPacket retrievePepAvatar(final Avatar avatar) { final Element item = new Element("item"); item.setAttribute("id", avatar.sha1sum); - final IqPacket packet = retrieve("urn:xmpp:avatar:data", item); + final IqPacket packet = retrieve(Namespace.AVATAR_DATA, item); packet.setTo(avatar.owner); return packet; } @@ -197,6 +197,13 @@ public class IqGenerator extends AbstractGenerator { return packet; } + public IqPacket retrieveVcardAvatar(final Jid to) { + final IqPacket packet = new IqPacket(IqPacket.TYPE.GET); + packet.setTo(to); + packet.addChild("vCard", "vcard-temp"); + return packet; + } + public IqPacket retrieveAvatarMetaData(final Jid to) { final IqPacket packet = retrieve("urn:xmpp:avatar:metadata", null); if (to != null) { diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 5c66451ce5d1af7024457d6d92fe1df9d0c832af..50743312ca4fa0f03a43b17a97aaf147a63eac6e 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -279,6 +279,8 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece } else if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) { account.setBookmarks(Collections.emptyMap()); Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": deleted bookmarks node"); + } else if (Namespace.AVATAR_METADATA.equals(node) && account.getJid().asBareJid().equals(from)) { + Log.d(Config.LOGTAG,account.getJid().asBareJid()+": deleted avatar metadata node"); } } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 43d0e769f233daf07b8456b04a622294321dae9b..79da6d551ae1f855b7182bbfb38774ee22eb07b4 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -38,7 +38,6 @@ import android.preference.PreferenceManager; import android.provider.ContactsContract; import android.security.KeyChain; import android.telephony.PhoneStateListener; -import android.telephony.TelephonyCallback; import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.DisplayMetrics; @@ -48,6 +47,7 @@ import android.util.Pair; import androidx.annotation.BoolRes; import androidx.annotation.IntegerRes; +import androidx.annotation.NonNull; import androidx.core.app.RemoteInput; import androidx.core.content.ContextCompat; @@ -2792,7 +2792,6 @@ public class XmppConnectionService extends Service { } }); } - public void joinMuc(Conversation conversation) { joinMuc(conversation, null, false); } @@ -3010,6 +3009,71 @@ public class XmppConnectionService extends Service { } } + public void deleteAvatar(final Account account) { + final AtomicBoolean executed = new AtomicBoolean(false); + final Runnable onDeleted = + () -> { + if (executed.compareAndSet(false, true)) { + account.setAvatar(null); + databaseBackend.updateAccount(account); + getAvatarService().clear(account); + updateAccountUi(); + } + }; + deleteVcardAvatar(account, onDeleted); + deletePepNode(account, Namespace.AVATAR_DATA); + deletePepNode(account, Namespace.AVATAR_METADATA, onDeleted); + } + + public void deletePepNode(final Account account, final String node) { + deletePepNode(account, node, null); + } + + private void deletePepNode(final Account account, final String node, final Runnable runnable) { + final IqPacket request = mIqGenerator.deleteNode(node); + sendIqPacket(account, request, (a, packet) -> { + if (packet.getType() == IqPacket.TYPE.RESULT) { + Log.d(Config.LOGTAG,a.getJid().asBareJid()+": successfully deleted pep node "+node); + if (runnable != null) { + runnable.run(); + } + } else { + Log.d(Config.LOGTAG,a.getJid().asBareJid()+": failed to delete "+ packet); + } + }); + } + + private void deleteVcardAvatar(final Account account, @NonNull final Runnable runnable) { + final IqPacket retrieveVcard = mIqGenerator.retrieveVcardAvatar(account.getJid().asBareJid()); + sendIqPacket(account, retrieveVcard, (a, response) -> { + if (response.getType() != IqPacket.TYPE.RESULT) { + Log.d(Config.LOGTAG,a.getJid().asBareJid()+": no vCard set. nothing to do"); + return; + } + final Element vcard = response.findChild("vCard", "vcard-temp"); + if (vcard == null) { + Log.d(Config.LOGTAG,a.getJid().asBareJid()+": no vCard set. nothing to do"); + return; + } + Element photo = vcard.findChild("PHOTO"); + if (photo == null) { + photo = vcard.addChild("PHOTO"); + } + photo.clearChildren(); + IqPacket publication = new IqPacket(IqPacket.TYPE.SET); + publication.setTo(a.getJid().asBareJid()); + publication.addChild(vcard); + sendIqPacket(account, publication, (a1, publicationResponse) -> { + if (publicationResponse.getType() == IqPacket.TYPE.RESULT) { + Log.d(Config.LOGTAG,a1.getJid().asBareJid()+": successfully deleted vcard avatar"); + runnable.run(); + } else { + Log.d(Config.LOGTAG, "failed to publish vcard " + publicationResponse.getErrorCondition()); + } + }); + }); + } + private boolean hasEnabledAccounts() { if (this.accounts == null) { return false; @@ -3598,7 +3662,7 @@ public class XmppConnectionService extends Service { if (result.getType() == IqPacket.TYPE.RESULT) { publishAvatarMetadata(account, avatar, options, true, callback); } else if (retry && PublishOptions.preconditionNotMet(result)) { - pushNodeConfiguration(account, "urn:xmpp:avatar:data", options, new OnConfigurationPushed() { + pushNodeConfiguration(account, Namespace.AVATAR_DATA, options, new OnConfigurationPushed() { @Override public void onPushSucceeded() { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": changed node configuration for avatar node"); @@ -3638,7 +3702,7 @@ public class XmppConnectionService extends Service { callback.onAvatarPublicationSucceeded(); } } else if (retry && PublishOptions.preconditionNotMet(result)) { - pushNodeConfiguration(account, "urn:xmpp:avatar:metadata", options, new OnConfigurationPushed() { + pushNodeConfiguration(account, Namespace.AVATAR_METADATA, options, new OnConfigurationPushed() { @Override public void onPushSucceeded() { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": changed node configuration for avatar meta data node"); diff --git a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java index cb1d0ad31884d8eeaeae8a34c02a3e52566b2a95..0e14fcc8fe86fa5b2513f5c300d3a425a88a3c4a 100644 --- a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java @@ -7,6 +7,8 @@ import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; import android.view.View; import android.view.View.OnLongClickListener; import android.widget.Button; @@ -14,6 +16,7 @@ import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.NonNull; import androidx.annotation.StringRes; import com.theartofdev.edmodo.cropper.CropImage; @@ -120,7 +123,25 @@ public class PublishProfilePictureActivity extends XmppActivity implements XmppC } @Override - public void onSaveInstanceState(Bundle outState) { + public boolean onCreateOptionsMenu(@NonNull final Menu menu) { + getMenuInflater().inflate(R.menu.activity_publish_profile_picture, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(final MenuItem item) { + if (item.getItemId() == R.id.action_delete_avatar) { + if (xmppConnectionService != null && account != null) { + xmppConnectionService.deleteAvatar(account); + } + return true; + } else { + return super.onOptionsItemSelected(item); + } + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { if (this.avatarUri != null) { outState.putParcelable("uri", this.avatarUri); } diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index 09bbda4cdcb4dbbe7a08352f452f931bf7ea4866..72c35a92f6f74dbc9f869610e60eb40d4f29f2ba 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -26,6 +26,8 @@ public final class Namespace { public static final String BOOKMARKS_CONVERSION = "urn:xmpp:bookmarks-conversion:0"; public static final String BOOKMARKS = "storage:bookmarks"; public static final String SYNCHRONIZATION = "im.quicksy.synchronization:0"; + public static final String AVATAR_DATA = "urn:xmpp:avatar:data"; + public static final String AVATAR_METADATA = "urn:xmpp:avatar:metadata"; public static final String AVATAR_CONVERSION = "urn:xmpp:pep-vcard-conversion:0"; public static final String JINGLE = "urn:xmpp:jingle:1"; public static final String JINGLE_ERRORS = "urn:xmpp:jingle:errors:1"; diff --git a/src/main/res/menu/activity_publish_profile_picture.xml b/src/main/res/menu/activity_publish_profile_picture.xml new file mode 100644 index 0000000000000000000000000000000000000000..bcfb99ae83fc100ba426e10187415376d97fff09 --- /dev/null +++ b/src/main/res/menu/activity_publish_profile_picture.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 299c57b3367596be7e495af42eb82d3643147189..53fb4871df2f9348faba349bf306a2aa1852bf7c 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -979,5 +979,6 @@ Account registrations are not supported No XMPP address found Temporary authentication failure + Delete avatar From e2612709af160c9001c1c6000fdcd004aa7fd009 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 25 Aug 2022 19:26:18 +0200 Subject: [PATCH 004/101] pulled translations from transifex --- src/main/res/values-ar/strings.xml | 1 - src/main/res/values-bg/strings.xml | 2 -- src/main/res/values-ca/strings.xml | 2 -- src/main/res/values-cs/strings.xml | 2 -- src/main/res/values-da-rDK/strings.xml | 4 ++-- src/main/res/values-de/strings.xml | 5 +---- src/main/res/values-el/strings.xml | 2 -- src/main/res/values-es/strings.xml | 5 +---- src/main/res/values-eu/strings.xml | 1 - src/main/res/values-fi/strings.xml | 2 -- src/main/res/values-fr/strings.xml | 2 -- src/main/res/values-gl/strings.xml | 5 +---- src/main/res/values-hu/strings.xml | 2 -- src/main/res/values-it/strings.xml | 5 +---- src/main/res/values-ja/strings.xml | 5 +---- src/main/res/values-nl/strings.xml | 1 - src/main/res/values-pl/strings.xml | 5 +---- src/main/res/values-pt-rBR/strings.xml | 5 +---- src/main/res/values-ro-rRO/strings.xml | 5 +---- src/main/res/values-ru/strings.xml | 2 -- src/main/res/values-sk/strings.xml | 2 -- src/main/res/values-sr/strings.xml | 2 -- src/main/res/values-sv/strings.xml | 2 -- src/main/res/values-szl/strings.xml | 2 -- src/main/res/values-tr-rTR/strings.xml | 2 -- src/main/res/values-uk/strings.xml | 2 -- src/main/res/values-vi/strings.xml | 2 -- src/main/res/values-zh-rCN/strings.xml | 5 +---- src/main/res/values-zh-rTW/strings.xml | 1 - 29 files changed, 11 insertions(+), 72 deletions(-) diff --git a/src/main/res/values-ar/strings.xml b/src/main/res/values-ar/strings.xml index 197b23a2657801549a9ca3190332e0c6244cb3b5..6ce1300089a80e92aa3e78bbe7661a89eee459d3 100644 --- a/src/main/res/values-ar/strings.xml +++ b/src/main/res/values-ar/strings.xml @@ -239,7 +239,6 @@ تفعيل ساعات السكون سوف تكتم التنبيهات إبان ساعات السكون أخرى - زامِن مع الفواصل المرجعية حسابك محظور للإلتحاق بمجموعة المحادثة هذه هذه المجموعة متاحة للأعضاء المنتمين إليها فقط تم طردك من مجموعة الدردشة هذه diff --git a/src/main/res/values-bg/strings.xml b/src/main/res/values-bg/strings.xml index d4603cc3ce107ef661e2019745758c0a0bdeb3ec..a5018d6ec20099b9adfed6e32b9829132255b6a4 100644 --- a/src/main/res/values-bg/strings.xml +++ b/src/main/res/values-bg/strings.xml @@ -294,8 +294,6 @@ Включване на тихите часове Известията ще бъдат заглушени по време на тихите часове Други - Синхронизиране с отметките - Автоматично присъединяване към групови разговори, ако такава е настройката на отметката Отпечатъкът OMEMO е копиран Достъпът Ви до този групов разговор е забранен Този групов разговор е само за членове diff --git a/src/main/res/values-ca/strings.xml b/src/main/res/values-ca/strings.xml index 452c1423d5399372264154b29464be5da8bba971..d5421b742799f9c37f43dbdacd5fabe15774b534 100644 --- a/src/main/res/values-ca/strings.xml +++ b/src/main/res/values-ca/strings.xml @@ -284,8 +284,6 @@ Habilitar hores de silenci Les notificacions seràn silenciades a les hores de silenci Altres - Sincronitzar als marcadors - Unir-se als xats de grup automàticament si el marcador l\'indica Empremta digital de OMEMO copiada en el portapapers Estàs prohibit en aquest xat de grup Aquest xat en grup només és de membres diff --git a/src/main/res/values-cs/strings.xml b/src/main/res/values-cs/strings.xml index d0ddfa60471dde0e8ef8d4fa309e95b6aeb5ea6d..d39965222b57b90a03cbd6687f668767c108f8fb 100644 --- a/src/main/res/values-cs/strings.xml +++ b/src/main/res/values-cs/strings.xml @@ -297,8 +297,6 @@ Povolit tichý režim Upozornění budou během tichého režimu ztlumena Další - Synchronizovat se záložkami - Automaticky se připojovat ke skupinovým chatům, pokud jsou nastaveny v záložkách OMEMO otisk zkopírován do schránky Byl(a) jste blokován(a) v této skupině Tento skupinový chat je pouze pro registrované členy diff --git a/src/main/res/values-da-rDK/strings.xml b/src/main/res/values-da-rDK/strings.xml index c07353a6ac76346306fc0d88ee79e1e574168bf4..80e81485c83f7b0100bc3e52475fd612a4be3145 100644 --- a/src/main/res/values-da-rDK/strings.xml +++ b/src/main/res/values-da-rDK/strings.xml @@ -294,8 +294,6 @@ Aktiver stilletid Notifikationer vil være lydløs under stilletid Andre - Synkroniser med bogmærker - Deltag automatisk i gruppechat hvis bogmærket tillader det OMEMO-fingeraftryk kopieret til udklipsholder Du er udelukket fra denne gruppechat Denne gruppechat er kun for medlemmer @@ -417,6 +415,7 @@ video billede vektorgrafik + multimediefil PDF dokument Android App Kontakt @@ -976,4 +975,5 @@ Ren tekstdokument Kontoregistrering er ikke understøttet Ingen XMPP-adresse fundet + Midlertidig godkendelsesfejl diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index 6d85f67d971a12045533fe3b3d664b8bd525abbb..928c8311f4a21d0cd2fe5b0b19d4fddf9af22e24 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -294,8 +294,6 @@ Ruhige Stunden aktivieren Benachrichtigungen sind während der ruhigen Stunden stumm. Sonstiges - Mit Lesezeichen synchronisieren - Gruppenchats automatisch beitreten, wenn das Lesezeichen dies angibt OMEMO-Fingerabdruck in die Zwischenablage kopiert Du wurdest aus diesem Gruppenchat ausgeschlossen Dieser Gruppenchat ist nur für Mitglieder @@ -978,5 +976,4 @@ Kontoregistrierungen werden nicht unterstützt Keine XMPP-Adresse gefunden Temporärer Authentifizierungsfehler - - + diff --git a/src/main/res/values-el/strings.xml b/src/main/res/values-el/strings.xml index 22b188af49c88619c75c2bc94073de7a655e5041..88750efe516cbaf1596561ce47088b65a5f29cdf 100644 --- a/src/main/res/values-el/strings.xml +++ b/src/main/res/values-el/strings.xml @@ -294,8 +294,6 @@ Ενεργοποίηση ωρών ησυχίας Οι ειδοποιήσεις θα σιγαστούν κατά τις ώρες ησυχίας Άλλο - Συγχρονισμός με σελιδοδείκτες - Συμμετοχή σε ομαδικές συζητήσεις αυτόματα αν ο σελιδοδείκτης αναφέρει αυτόματη συμμετοχή Το αποτύπωμα OMEMO αντιγράφηκε στο πρόχειρο Είστε αποκλεισμένοι από αυτή την ομαδική συζήτηση Αυτή η ομαδική συζήτηση είναι μόνο για μέλη diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index 202d70fc7b3dc323a44e05fe98a238cf6caec79f..fce2d4736a35c9dc0c3e417af1defdc3f9e5a4a2 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -297,8 +297,6 @@ Habilitar horario de silencio Las notificaciones serán silenciadas durante el horario de silencio Otros - Sincronizar marcadores - Unirse a conversaciones en grupo automáticamente si el marcador así lo indica Huella digital OMEMO copiada al portapapeles Tu entrada a esta conversación en grupo ha sido prohibida Esta conversación en grupo es solo para miembros @@ -990,5 +988,4 @@ Los registros de cuenta no están soportados Dirección XMPP no encontrada Fallo temporal de autenticación - - + diff --git a/src/main/res/values-eu/strings.xml b/src/main/res/values-eu/strings.xml index 4d2fde7731a89314fabff6aa3e1b878d77392f5d..cfd94fcb314969caf3cf74a71a088649ef9c4b6e 100644 --- a/src/main/res/values-eu/strings.xml +++ b/src/main/res/values-eu/strings.xml @@ -239,7 +239,6 @@ Ordu lasaiak gaitu Jakinarazpenak isilaraziko dira ordu lasaiak iraun bitartean Besteak - Laster-markekin sinkronizatu Talde honetara sartzea debekatuta duzu Talde hau kideentzat da soilik Baliabide murrizketa diff --git a/src/main/res/values-fi/strings.xml b/src/main/res/values-fi/strings.xml index 9a03e5808711665939ad0298859234aae5139765..675e11fd4d2be525dae93b682f934d89f2363ca7 100644 --- a/src/main/res/values-fi/strings.xml +++ b/src/main/res/values-fi/strings.xml @@ -289,8 +289,6 @@ Ota käyttöön hiljaisuus Ilmoitukset vaimennetaan hiljaisuuden aikana Muut - Synkronoi kirjanmerkkien kanssa - Liity ryhmään automaattisesti jos se on kirjanmerkeissäsi OMEMO-sormenjälki kopioitu leikepöydälle Sinut on estetty tästä ryhmäkeskustelusta Tämä ryhmäkeskustelu on vain jäsenille diff --git a/src/main/res/values-fr/strings.xml b/src/main/res/values-fr/strings.xml index 807a45f529ea01ccb17b614820a12e9a3f16a745..6be70fd0c43f3bd060992eb9a8dba096cefd11b4 100644 --- a/src/main/res/values-fr/strings.xml +++ b/src/main/res/values-fr/strings.xml @@ -295,8 +295,6 @@ Activer les heures tranquilles Les notifications seront muettes pendant les heures tranquilles. Autres - Synchroniser avec les signets - Rejoindre automatiquement les groupes marqués en favoris Empreinte OMEMO copiée dans le presse-papier Vous êtes bannis de ce groupe Ce groupe est réservé aux membres diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index 3c7a0606c76ecc8eff70f7d99709fb85757b1bdd..2b52435fe4131145626357b3c4be5962d6dceccc 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -294,8 +294,6 @@ Establecer horario sen notificacións As notificacións serán silenciadas durante estas horas Outro - Sincronizar cos marcadores - Unirte as conversas en grupo automáticamente se o marcador así o indica Copiouse a impresión dixital OMEMO ao portapapeis Non podes acceder a esta conversa en grupo Esta conversa en grupo é so para membros @@ -978,5 +976,4 @@ Non está permitido o rexistro de novas contas Non se atopa un enderezo XMPP Fallo temporal da autenticación - - + diff --git a/src/main/res/values-hu/strings.xml b/src/main/res/values-hu/strings.xml index 162212928314f90fae219975deb5a3112dd316a6..5ad16d2e986ba432c69551c388072430a6f8064b 100644 --- a/src/main/res/values-hu/strings.xml +++ b/src/main/res/values-hu/strings.xml @@ -289,8 +289,6 @@ Csendes órák engedélyezése Az értesítések el lesznek némítva a csendes órák alatt Egyéb - Szinkronizálás a könyvjelzőkkel - Automatikusan csatlakozzon a csoportos csevegésekhez, ha ez szerepel a könyvjelzőben OMEMO ujjlenyomat a vágólapra lett másolva Ki van tiltva ebből a csoportos csevegésből Ez a csoportos csevegés csak tagoknak szól diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml index ffc63ac41d5e35c3b273ae47f03275ce7ee9548f..37ef96f7916d6070e82d069dba2f9b44b7c0704d 100644 --- a/src/main/res/values-it/strings.xml +++ b/src/main/res/values-it/strings.xml @@ -297,8 +297,6 @@ Attiva ore di quiete Le notifiche verranno silenziate durante le ore di quiete Altro - Sincronizza con i segnalibri - Entra nelle chat di gruppo automaticamente se il segnalibro dice così Impronta OMEMO copiata negli appunti Sei stato bandito da questa chat di gruppo Questa chat di gruppo è solo per membri @@ -990,5 +988,4 @@ Le registrazioni di profili non sono supportate Nessun indirizzo XMPP trovato Errore di autenticazione temporaneo - - + diff --git a/src/main/res/values-ja/strings.xml b/src/main/res/values-ja/strings.xml index 7fb4f6ae375908ff02e6c593453b91049fc9162e..94335a35981d270041fb1ffa0485ecefc84ee108 100644 --- a/src/main/res/values-ja/strings.xml +++ b/src/main/res/values-ja/strings.xml @@ -291,8 +291,6 @@ 消音時間を有効化 消音時間の間、通知は無音になります その他 - ブックマークと同期 - ブックマークに従って、グループチャットに自動で参加します。 OMEMO フィンガープリントをクリップボードにコピーしました このグループチャットから出禁にされています このグループチャットはメンバー制です @@ -958,5 +956,4 @@ アカウント登録はサポートされていません XMPPアドレスがみつかりません 一時的な認証失敗 - - + diff --git a/src/main/res/values-nl/strings.xml b/src/main/res/values-nl/strings.xml index 9b7f72a7c52f0ce9a5c67e087fd9f0a08fbc4576..f67b0c76c1054b952dcd69669acf66c5a8163863 100644 --- a/src/main/res/values-nl/strings.xml +++ b/src/main/res/values-nl/strings.xml @@ -276,7 +276,6 @@ Stille uren inschakelen Tijdens stille uren worden meldingen onderdrukt Andere - Synchroniseren met bladwijzers OMEMO-vingerafdruk gekopieerd naar klembord Je bent verbannen uit dit groepsgesprek Dit groepsgesprek is enkel voor leden diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index a25411374ee176162f71ca40ea309f3b132ee503..53158925fab28c75f128534b45cb47a67f5605c1 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -300,8 +300,6 @@ Włącz godziny ciszy Powiadomienia będą wyciszone w wybranym przedziale czasu Inne - Synchronizuj z zakładkami - Dołączaj do rozmów grupowych automatycznie jeśli na to wskazuje zakładka Odcisk klucza OMEMO został skopiowany do schowka Zbanowany Konferencja tylko dla użytkowników @@ -1005,5 +1003,4 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Rejestracja kont nie jest wspierana Nie znaleziono adresu XMPP Tymczasowy błąd uwierzytelniania - - + diff --git a/src/main/res/values-pt-rBR/strings.xml b/src/main/res/values-pt-rBR/strings.xml index 0847137b3edd8f7274e0f0f9e9d5c8d26b425d6c..fd4b29cc257bed3e39aebbc752231a9e127017db 100644 --- a/src/main/res/values-pt-rBR/strings.xml +++ b/src/main/res/values-pt-rBR/strings.xml @@ -297,8 +297,6 @@ Habilitar horário de sossego As notificações serão silenciadas no horário de sossego. Outras - Sincronizar com os favoritos - Entre nas conversas em grupo automaticamente caso isso esteja definido no favorito Impressão digital OMEMO copiada para a área de transferência Você foi banido desta conversa em grupo Somente membros podem entrar nessa conversa em grupo @@ -991,5 +989,4 @@ O registro de contas não está ativo Não foi encontrado nenhum endereço XMPP Falha temporária na autenticação - - + diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index 17459ba0d20703985ed59576ed9cd78a25c0173b..34c5c155fb3d09f7a1ff174a6f6d6211205a0e4a 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -297,8 +297,6 @@ Activează orar de liniște Notificările vor fi reduse la tăcere în timpul orelor de liniște Altele - Sincronizează cu semnele de carte - Alătură-te discuției de grup în mod automat dacă semnul de carte este setat așa Amprentă OMEMO copiată în memorie V-a fost interzis accesul la această discuție de grup Această discuție de grup este rezervată membrilor @@ -991,5 +989,4 @@ Nu este posibilă înregistrarea unui cont Nu a fost găsită o adresă XMPP Eroare temporară de autentificare - - + diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml index 4e4880dd62794b4dd72f8dcd39710da5621ff0e9..ddc23d005e0349ef8c3b8454f8e75c50b00dd471 100644 --- a/src/main/res/values-ru/strings.xml +++ b/src/main/res/values-ru/strings.xml @@ -300,8 +300,6 @@ Включить режим «тихих часов» Уведомления будут отключены во время «тихих часов» Другие - Синхронизировать с закладками - Автоматически заходить в конференции при установленном флаге в настройках закладки OMEMO-отпечаток скопирован в буфер обмена Вы заблокированы в этой конференции Эта конференция — только для участников diff --git a/src/main/res/values-sk/strings.xml b/src/main/res/values-sk/strings.xml index 25e10cd611ca10f39f246df3bcfd7cd4224b7fd5..e63f51f24f7aa60b70f7d592975f312b9aae3224 100644 --- a/src/main/res/values-sk/strings.xml +++ b/src/main/res/values-sk/strings.xml @@ -287,8 +287,6 @@ Povoliť tichý režim Upozornenia budú počas tichého režimu stlmené Ďalší - Synchronizovať so záložkami - Automaticky sa pripojiť k skupinovému rozhovoru, ak to hovorí záložka OMEMO odtlačok skopírovaný do schránky Ste zakázaný na tomto skupinovom rozhovore Skupinový rozhovor len pre členov diff --git a/src/main/res/values-sr/strings.xml b/src/main/res/values-sr/strings.xml index cd39b936972887c555397175ae352c4961472d9d..df624e9d8da75762a6e4400832a79f4f794a1947 100644 --- a/src/main/res/values-sr/strings.xml +++ b/src/main/res/values-sr/strings.xml @@ -293,8 +293,6 @@ Укључи тихе сате Обавештења ће бити ућуткана за време тихих сати Остало - Синхронизуј са обележивачима - Аутоматски се придружите групним ћаскањима по поставци обележивача ОМЕМО отисак копиран на клипборд Забрањен вам је приступ овом групном ћаскању Ово групно ћаскање је само за чланове diff --git a/src/main/res/values-sv/strings.xml b/src/main/res/values-sv/strings.xml index 14e91099b90c778fbfbc53a95eb70d5f83271a2c..0257dca182c83b7feb26e462c718509c22913fbc 100644 --- a/src/main/res/values-sv/strings.xml +++ b/src/main/res/values-sv/strings.xml @@ -294,8 +294,6 @@ Aktivera tysta timmar Notifieringar kommer vara tysta under tysta timmar Annat - Synkronisera med bokmärken - Gå med i gruppchattar automatiskt om bokmärket säger det OMEMO-fingeravtryck kopierat till urklipp Du är avstängd från denna gruppchatt Denna gruppchatt är endast för medlemmar diff --git a/src/main/res/values-szl/strings.xml b/src/main/res/values-szl/strings.xml index 0ed32a5337f5e0cfbdb89479f1fa3f4a9b806c1b..b6004932c7b8f4b299fb521f5ca688c0f88d7872 100644 --- a/src/main/res/values-szl/strings.xml +++ b/src/main/res/values-szl/strings.xml @@ -316,8 +316,6 @@ Włōncz godziny cisze Powiadōmiynia bydōm wyciszōne we ôbranych godzinach Inksze - Synchrōnizuj ze zokłodkami - Przistympuj do godek grupowych autōmatycznie, jeźli tak pado zokłodka Ôdcisk klucza OMEMO bōł skopiowany do skrytki Ôd tyj grupy mosz wykluczynie Kōnferyncyjo ino dlo czōnkōw diff --git a/src/main/res/values-tr-rTR/strings.xml b/src/main/res/values-tr-rTR/strings.xml index 283fee4d3330fc2e0865461d318dbeaaa86fbf7e..683901d9fd6af7939f5e0961f67af01b8de761c5 100644 --- a/src/main/res/values-tr-rTR/strings.xml +++ b/src/main/res/values-tr-rTR/strings.xml @@ -294,8 +294,6 @@ Sessiz saatleri etkinleştir Bildirimler sessiz saatler boyunca sessize alınacaktır Diğer - Yer imleri ile senkronize et. - Yer imleri öyle belirtmişse grup konuşmalarına otomatik olarak katıl. OMEMO parmak izi panoya kopyalandı Bu grup konuşmasından menedildiniz Bu grup konuşması yalnızca üyeleri içindir diff --git a/src/main/res/values-uk/strings.xml b/src/main/res/values-uk/strings.xml index 4a96a344e51fbf21294f4d4d5e8353fbf66f5e09..b28336c59df38d5fde06017b47b57619b54e36e1 100644 --- a/src/main/res/values-uk/strings.xml +++ b/src/main/res/values-uk/strings.xml @@ -276,8 +276,6 @@ Увімкнути години тиші Сповіщення не звучатимуть під час годин тиші Інше - Синхронізовувати з закладками - Приєднуватися до груп і полишати їх відповідно до опції автоматичного приєднання, вибраної в закладках. Цифровий підпис OMEMO скопійовано Вам заборонили доступ до цієї групи Ця група лише для учасників diff --git a/src/main/res/values-vi/strings.xml b/src/main/res/values-vi/strings.xml index 76eb33c371973e3a2ee283327baea1c6cc210888..afb4b729642dad4e2fa31d487e7413bfd9f4df3e 100644 --- a/src/main/res/values-vi/strings.xml +++ b/src/main/res/values-vi/strings.xml @@ -291,8 +291,6 @@ Bật giờ yên lặng Thông báo sẽ được tắt trong giờ yên lặng Khác - Đồng bộ hoá bằng dấu trang - Tự động tham gia các cuộc trò chuyện nhóm nếu dấu trang bảo thế Đã sao chép mã vân tay OMEMO vào bộ nhớ tạm Bạn bị cấm khỏi cuộc trò chuyện nhóm này Cuộc trò chuyện nhóm này chỉ dành cho thành viên diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index 17d1851058d8628138ad476663a768d3b410b058..ad19591cfb4a87c518e2d338e9850f4bbfe4aab9 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -291,8 +291,6 @@ 启用静默时间段 在静默时间段内通知将保持静音 其他 - 与书签同步 - 根据书签标记自动加入群聊。 OMEMO指纹已拷贝到剪贴板 您被封禁了 这个群聊只允许成员聊天 @@ -965,5 +963,4 @@ 不支持注册账户 未找到 XMPP 地址 临时认证失败 - - + diff --git a/src/main/res/values-zh-rTW/strings.xml b/src/main/res/values-zh-rTW/strings.xml index 0179f100896697f02578bb02c8fb0938b8946fa1..edcacf37469d4aeae5d92a928d03cc86e734dd15 100644 --- a/src/main/res/values-zh-rTW/strings.xml +++ b/src/main/res/values-zh-rTW/strings.xml @@ -248,7 +248,6 @@ 啟用靜默時間段 在靜默時間段內通知將保持靜音 其他 - 同步處理書籤 用帳戶 %s 正在 HTTP 伺服器中檢查 %s 你沒有連接。請稍後重試 From a6b88ba9e95deaeeeff6a4433d1e79a28d74cd25 Mon Sep 17 00:00:00 2001 From: Dmitry Markin Date: Mon, 29 Aug 2022 13:41:35 +0300 Subject: [PATCH 005/101] Add missed call notifications Co-authored-by: Daniel Gultsch --- art/ic_missed_call_notification.svg | 344 ++++++++++++++++++ art/render.rb | 3 +- .../conversations/entities/Conversation.java | 4 +- .../services/NotificationService.java | 241 +++++++++++- .../services/XmppConnectionService.java | 31 +- .../xmpp/jingle/JingleRtpConnection.java | 2 + .../ic_missed_call_notification.png | Bin 0 -> 810 bytes .../ic_missed_call_notification.png | Bin 0 -> 589 bytes .../ic_missed_call_notification.png | Bin 0 -> 1151 bytes .../ic_missed_call_notification.png | Bin 0 -> 1680 bytes .../ic_missed_call_notification.png | Bin 0 -> 2179 bytes src/main/res/values/strings.xml | 5 + 12 files changed, 609 insertions(+), 21 deletions(-) create mode 100644 art/ic_missed_call_notification.svg create mode 100644 src/main/res/drawable-hdpi/ic_missed_call_notification.png create mode 100644 src/main/res/drawable-mdpi/ic_missed_call_notification.png create mode 100644 src/main/res/drawable-xhdpi/ic_missed_call_notification.png create mode 100644 src/main/res/drawable-xxhdpi/ic_missed_call_notification.png create mode 100644 src/main/res/drawable-xxxhdpi/ic_missed_call_notification.png diff --git a/art/ic_missed_call_notification.svg b/art/ic_missed_call_notification.svg new file mode 100644 index 0000000000000000000000000000000000000000..78f0acead8673ea1623d4e72e64a72b0bf012142 --- /dev/null +++ b/art/ic_missed_call_notification.svg @@ -0,0 +1,344 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/art/render.rb b/art/render.rb index 7fb46d138ca0bc7dac38262b8c11681e94cb7963..7ae4ac8ae18d8d6c3e8cb2c6a49ed7c661a476f7 100755 --- a/art/render.rb +++ b/art/render.rb @@ -28,6 +28,7 @@ images = { 'conversations_mono.svg' => ['conversations/ic_notification', 24], 'quicksy_mono.svg' => ['quicksy/ic_notification', 24], 'flip_camera_android-black-24dp.svg' => ['ic_flip_camera_android_black_24dp', 24], + 'ic_missed_call_notification.svg' => ['ic_missed_call_notification', 24], 'ic_send_text_offline.svg' => ['ic_send_text_offline', 36], 'ic_send_text_offline_white.svg' => ['ic_send_text_offline_white', 36], 'ic_send_text_online.svg' => ['ic_send_text_online', 36], @@ -119,7 +120,7 @@ images.each do |source_filename, settings| else path = "../src/#{output_parts[0]}/res/drawable-#{resolution}/#{output_parts[1]}.png" end - execute_cmd "#{inkscape} #{source_filename} -C -w #{width} -h #{height} -o #{path}" + execute_cmd "#{inkscape} #{source_filename} -C -w #{width} -h #{height} -e #{path}" top = [] right = [] diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index 4a825fbb38b5b5cfe08507dafdd068f636b0d985..8bb65cc0f57ab3f0992f80c8f3ce41d0baba12c8 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -241,11 +241,11 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl } } - public void findUnreadMessages(OnMessageFound onMessageFound) { + public void findUnreadMessagesAndCalls(OnMessageFound onMessageFound) { final ArrayList results = new ArrayList<>(); synchronized (this.messages) { for (final Message message : this.messages) { - if (message.isRead() || message.getType() == Message.TYPE_RTP_SESSION) { + if (message.isRead()) { continue; } results.add(message); diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index c9b932415586fd37a8dd9744eeefc15cd90f6c1e..b6916020daff0d2e17d18d8e09a832251bdfebf5 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -90,17 +90,20 @@ public class NotificationService { private static final long[] CALL_PATTERN = {0, 500, 300, 600}; - private static final String CONVERSATIONS_GROUP = "eu.siacs.conversations"; + private static final String MESSAGES_GROUP = "eu.siacs.conversations.messages"; + private static final String MISSED_CALLS_GROUP = "eu.siacs.conversations.missed_calls"; private static final int NOTIFICATION_ID_MULTIPLIER = 1024 * 1024; static final int FOREGROUND_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 4; private static final int NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 2; private static final int ERROR_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 6; private static final int INCOMING_CALL_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 8; public static final int ONGOING_CALL_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 10; - private static final int DELIVERY_FAILED_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 12; + public static final int MISSED_CALL_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 12; + private static final int DELIVERY_FAILED_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 13; private final XmppConnectionService mXmppConnectionService; private final LinkedHashMap> notifications = new LinkedHashMap<>(); private final HashMap mBacklogMessageCounter = new HashMap<>(); + private final LinkedHashMap mMissedCalls = new LinkedHashMap<>(); private Conversation mOpenConversation; private boolean mIsInForeground; private long mLastNotification; @@ -221,6 +224,16 @@ public class NotificationService { ongoingCallsChannel.setGroup("calls"); notificationManager.createNotificationChannel(ongoingCallsChannel); + final NotificationChannel missedCallsChannel = new NotificationChannel("missed_calls", + c.getString(R.string.missed_calls_channel_name), + NotificationManager.IMPORTANCE_HIGH); + missedCallsChannel.setShowBadge(true); + missedCallsChannel.setSound(null, null); + missedCallsChannel.setLightColor(LED_COLOR); + missedCallsChannel.enableLights(true); + missedCallsChannel.setGroup("calls"); + notificationManager.createNotificationChannel(missedCallsChannel); + final NotificationChannel messagesChannel = new NotificationChannel( "messages", @@ -284,12 +297,18 @@ public class NotificationService { notificationManager.createNotificationChannel(deliveryFailedChannel); } - private boolean notify(final Message message) { + private boolean notifyMessage(final Message message) { final Conversation conversation = (Conversation) message.getConversation(); return message.getStatus() == Message.STATUS_RECEIVED && !conversation.isMuted() && (conversation.alwaysNotify() || wasHighlightedOrPrivate(message)) - && (!conversation.isWithStranger() || notificationsFromStrangers()); + && (!conversation.isWithStranger() || notificationsFromStrangers()) + && message.getType() != Message.TYPE_RTP_SESSION; + } + + private boolean notifyMissedCall(final Message message) { + return message.getType() == Message.TYPE_RTP_SESSION + && message.getStatus() == Message.STATUS_RECEIVED; } public boolean notificationsFromStrangers() { @@ -320,12 +339,16 @@ public class NotificationService { } public void pushFromBacklog(final Message message) { - if (notify(message)) { + if (notifyMessage(message)) { synchronized (notifications) { getBacklogMessageCounter((Conversation) message.getConversation()) .incrementAndGet(); pushToStack(message); } + } else if (notifyMissedCall(message)) { + synchronized (mMissedCalls) { + pushMissedCall(message); + } } } @@ -360,6 +383,9 @@ public class NotificationService { updateNotification(count > 0, conversations); } } + synchronized (mMissedCalls) { + updateMissedCallNotifications(mMissedCalls.keySet()); + } } private List getBacklogConversations(Account account) { @@ -666,7 +692,7 @@ public class NotificationService { private void pushNow(final Message message) { mXmppConnectionService.updateUnreadCountBadge(); - if (!notify(message)) { + if (!notifyMessage(message)) { Log.d( Config.LOGTAG, message.getConversation().getAccount().getJid().asBareJid() @@ -695,7 +721,29 @@ public class NotificationService { } } - public void clear() { + private void pushMissedCall(final Message message) { + final Conversational conversation = message.getConversation(); + final MissedCallsInfo info = mMissedCalls.get(conversation); + if (info == null) { + mMissedCalls.put(conversation, new MissedCallsInfo(message.getTimeSent())); + } else { + info.newMissedCall(message.getTimeSent()); + } + } + + public void pushMissedCallNow(final Message message) { + synchronized (mMissedCalls) { + pushMissedCall(message); + updateMissedCallNotifications(Collections.singleton(message.getConversation())); + } + } + + public void clear(final Conversation conversation) { + clearMessages(conversation); + clearMissedCalls(conversation); + } + + public void clearMessages() { synchronized (notifications) { for (ArrayList messages : notifications.values()) { markAsReadIfHasDirectReply(messages); @@ -705,7 +753,7 @@ public class NotificationService { } } - public void clear(final Conversation conversation) { + public void clearMessages(final Conversation conversation) { synchronized (this.mBacklogMessageCounter) { this.mBacklogMessageCounter.remove(conversation); } @@ -718,6 +766,25 @@ public class NotificationService { } } + public void clearMissedCalls() { + synchronized (mMissedCalls) { + for (final Conversational conversation : mMissedCalls.keySet()) { + cancel(conversation.getUuid(), MISSED_CALL_NOTIFICATION_ID); + } + mMissedCalls.clear(); + updateMissedCallNotifications(null); + } + } + + public void clearMissedCalls(final Conversation conversation) { + synchronized (mMissedCalls) { + if (mMissedCalls.remove(conversation) != null) { + cancel(conversation.getUuid(), MISSED_CALL_NOTIFICATION_ID); + updateMissedCallNotifications(null); + } + } + } + private void markAsReadIfHasDirectReply(final Conversation conversation) { markAsReadIfHasDirectReply(notifications.get(conversation.getUuid())); } @@ -797,7 +864,7 @@ public class NotificationService { } modifyForSoundVibrationAndLight( singleBuilder, notifyThis, quiteHours, preferences); - singleBuilder.setGroup(CONVERSATIONS_GROUP); + singleBuilder.setGroup(MESSAGES_GROUP); setNotificationColor(singleBuilder); notify(entry.getKey(), NOTIFICATION_ID, singleBuilder.build()); } @@ -807,6 +874,31 @@ public class NotificationService { } } + private void updateMissedCallNotifications(final Set update) { + if (mMissedCalls.isEmpty()) { + cancel(MISSED_CALL_NOTIFICATION_ID); + return; + } + if (mMissedCalls.size() == 1 && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + final Conversational conversation = mMissedCalls.keySet().iterator().next(); + final MissedCallsInfo info = mMissedCalls.values().iterator().next(); + final Notification notification = missedCall(conversation, info); + notify(MISSED_CALL_NOTIFICATION_ID, notification); + } else { + final Notification summary = missedCallsSummary(); + notify(MISSED_CALL_NOTIFICATION_ID, summary); + if (update != null) { + for (final Conversational conversation : update) { + final MissedCallsInfo info = mMissedCalls.get(conversation); + if (info != null) { + final Notification notification = missedCall(conversation, info); + notify(conversation.getUuid(), MISSED_CALL_NOTIFICATION_ID, notification); + } + } + } + } + } + private void modifyForSoundVibrationAndLight( Builder mBuilder, boolean notify, boolean quietHours, SharedPreferences preferences) { final Resources resources = mXmppConnectionService.getResources(); @@ -867,6 +959,101 @@ public class NotificationService { } } + private Notification missedCallsSummary() { + final Builder publicBuilder = buildMissedCallsSummary(true); + final Builder builder = buildMissedCallsSummary(false); + builder.setPublicVersion(publicBuilder.build()); + return builder.build(); + } + + private Builder buildMissedCallsSummary(boolean publicVersion) { + final Builder builder = new NotificationCompat.Builder(mXmppConnectionService, "missed_calls"); + int totalCalls = 0; + final StringBuilder names = new StringBuilder(); + long lastTime = 0; + for (Map.Entry entry : mMissedCalls.entrySet()) { + final Conversational conversation = entry.getKey(); + final MissedCallsInfo missedCallsInfo = entry.getValue(); + names.append(conversation.getContact().getDisplayName()); + names.append(", "); + totalCalls += missedCallsInfo.getNumberOfCalls(); + lastTime = Math.max(lastTime, missedCallsInfo.getLastTime()); + } + if (names.length() >= 2) { + names.delete(names.length() - 2, names.length()); + } + final String title = (totalCalls == 1) ? mXmppConnectionService.getString(R.string.missed_call) : + (mMissedCalls.size() == 1) ? mXmppConnectionService.getString(R.string.n_missed_calls, totalCalls) : + mXmppConnectionService.getString(R.string.n_missed_calls_from_m_contacts, totalCalls, mMissedCalls.size()); + builder.setContentTitle(title); + builder.setTicker(title); + if (!publicVersion) { + builder.setContentText(names.toString()); + } + builder.setSmallIcon(R.drawable.ic_missed_call_notification); + builder.setGroupSummary(true); + builder.setGroup(MISSED_CALLS_GROUP); + builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN); + builder.setCategory(NotificationCompat.CATEGORY_CALL); + builder.setWhen(lastTime); + if (!mMissedCalls.isEmpty()) { + final Conversational firstConversation = mMissedCalls.keySet().iterator().next(); + builder.setContentIntent(createContentIntent(firstConversation)); + } + builder.setDeleteIntent(createMissedCallsDeleteIntent(null)); + modifyMissedCall(builder); + return builder; + } + + private Notification missedCall(final Conversational conversation, final MissedCallsInfo info) { + final Builder publicBuilder = buildMissedCall(conversation, info, true); + final Builder builder = buildMissedCall(conversation, info, false); + builder.setPublicVersion(publicBuilder.build()); + return builder.build(); + } + + private Builder buildMissedCall(final Conversational conversation, final MissedCallsInfo info, boolean publicVersion) { + final Builder builder = new NotificationCompat.Builder(mXmppConnectionService, "missed_calls"); + final String title = (info.getNumberOfCalls() == 1) ? mXmppConnectionService.getString(R.string.missed_call) : + mXmppConnectionService.getString(R.string.n_missed_calls, info.getNumberOfCalls()); + builder.setContentTitle(title); + final String name = conversation.getContact().getDisplayName(); + if (publicVersion) { + builder.setTicker(title); + } else { + if (info.getNumberOfCalls() == 1) { + builder.setTicker(mXmppConnectionService.getString(R.string.missed_call_from_x, name)); + } else { + builder.setTicker(mXmppConnectionService.getString(R.string.n_missed_calls_from_x, info.getNumberOfCalls(), name)); + } + builder.setContentText(name); + } + builder.setSmallIcon(R.drawable.ic_missed_call_notification); + builder.setGroup(MISSED_CALLS_GROUP); + builder.setCategory(NotificationCompat.CATEGORY_CALL); + builder.setWhen(info.getLastTime()); + builder.setContentIntent(createContentIntent(conversation)); + builder.setDeleteIntent(createMissedCallsDeleteIntent(conversation)); + if (!publicVersion && conversation instanceof Conversation) { + builder.setLargeIcon(mXmppConnectionService.getAvatarService() + .get((Conversation) conversation, AvatarService.getSystemUiAvatarSize(mXmppConnectionService))); + } + modifyMissedCall(builder); + return builder; + } + + private void modifyMissedCall(final Builder builder) { + final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService); + final Resources resources = mXmppConnectionService.getResources(); + final boolean led = preferences.getBoolean("led", resources.getBoolean(R.bool.led)); + if (led) { + builder.setLights(LED_COLOR, 2000, 3000); + } + builder.setPriority(NotificationCompat.PRIORITY_HIGH); + builder.setSound(null); + setNotificationColor(builder); + } + private Builder buildMultipleConversation(final boolean notify, final boolean quietHours) { final Builder mBuilder = new NotificationCompat.Builder( @@ -932,7 +1119,7 @@ public class NotificationService { mBuilder.setContentIntent(createContentIntent(conversation)); } mBuilder.setGroupSummary(true); - mBuilder.setGroup(CONVERSATIONS_GROUP); + mBuilder.setGroup(MESSAGES_GROUP); mBuilder.setDeleteIntent(createDeleteIntent(null)); mBuilder.setSmallIcon(R.drawable.ic_notification); return mBuilder; @@ -1336,7 +1523,7 @@ public class NotificationService { private PendingIntent createDeleteIntent(Conversation conversation) { final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class); - intent.setAction(XmppConnectionService.ACTION_CLEAR_NOTIFICATION); + intent.setAction(XmppConnectionService.ACTION_CLEAR_MESSAGE_NOTIFICATION); if (conversation != null) { intent.putExtra("uuid", conversation.getUuid()); return PendingIntent.getService( @@ -1356,6 +1543,16 @@ public class NotificationService { : PendingIntent.FLAG_UPDATE_CURRENT); } + private PendingIntent createMissedCallsDeleteIntent(final Conversational conversation) { + final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class); + intent.setAction(XmppConnectionService.ACTION_CLEAR_MISSED_CALL_NOTIFICATION); + if (conversation != null) { + intent.putExtra("uuid", conversation.getUuid()); + return PendingIntent.getService(mXmppConnectionService, generateRequestCode(conversation, 21), intent, 0); + } + return PendingIntent.getService(mXmppConnectionService, 1, intent, 0); + } + private PendingIntent createReplyIntent( final Conversation conversation, final String lastMessageUuid, @@ -1677,6 +1874,28 @@ public class NotificationService { } } + private static class MissedCallsInfo { + private int numberOfCalls; + private long lastTime; + + MissedCallsInfo(final long time) { + numberOfCalls = 1; + lastTime = time; + } + + public void newMissedCall(final long time) { + ++numberOfCalls; + lastTime = time; + } + + public int getNumberOfCalls() { + return numberOfCalls; + } + + public long getLastTime() { + return lastTime; + } + } private class VibrationRunnable implements Runnable { @Override diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 79da6d551ae1f855b7182bbfb38774ee22eb07b4..24545424743c444d0f6c4601fa45d3fdc895e58f 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -175,7 +175,8 @@ public class XmppConnectionService extends Service { public static final String ACTION_REPLY_TO_CONVERSATION = "reply_to_conversations"; public static final String ACTION_MARK_AS_READ = "mark_as_read"; public static final String ACTION_SNOOZE = "snooze"; - public static final String ACTION_CLEAR_NOTIFICATION = "clear_notification"; + public static final String ACTION_CLEAR_MESSAGE_NOTIFICATION = "clear_message_notification"; + public static final String ACTION_CLEAR_MISSED_CALL_NOTIFICATION = "clear_missed_call_notification"; public static final String ACTION_DISMISS_ERROR_NOTIFICATIONS = "dismiss_error"; public static final String ACTION_TRY_AGAIN = "try_again"; public static final String ACTION_IDLE_PING = "idle_ping"; @@ -670,19 +671,35 @@ public class XmppConnectionService extends Service { case Intent.ACTION_SHUTDOWN: logoutAndSave(true); return START_NOT_STICKY; - case ACTION_CLEAR_NOTIFICATION: + case ACTION_CLEAR_MESSAGE_NOTIFICATION: mNotificationExecutor.execute(() -> { try { final Conversation c = findConversationByUuid(uuid); if (c != null) { - mNotificationService.clear(c); + mNotificationService.clearMessages(c); } else { - mNotificationService.clear(); + mNotificationService.clearMessages(); } restoredFromDatabaseLatch.await(); } catch (InterruptedException e) { - Log.d(Config.LOGTAG, "unable to process clear notification"); + Log.d(Config.LOGTAG, "unable to process clear message notification"); + } + }); + break; + case ACTION_CLEAR_MISSED_CALL_NOTIFICATION: + mNotificationExecutor.execute(() -> { + try { + final Conversation c = findConversationByUuid(uuid); + if (c != null) { + mNotificationService.clearMissedCalls(c); + } else { + mNotificationService.clearMissedCalls(); + } + restoredFromDatabaseLatch.await(); + + } catch (InterruptedException e) { + Log.d(Config.LOGTAG, "unable to process clear missed call notification"); } }); break; @@ -769,7 +786,7 @@ public class XmppConnectionService extends Service { return; } c.setMutedTill(System.currentTimeMillis() + 30 * 60 * 1000); - mNotificationService.clear(c); + mNotificationService.clearMessages(c); updateConversation(c); }); case AudioManager.RINGER_MODE_CHANGED_ACTION: @@ -1954,7 +1971,7 @@ public class XmppConnectionService extends Service { private void restoreMessages(Conversation conversation) { conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE)); conversation.findUnsentTextMessages(message -> markMessage(message, Message.STATUS_WAITING)); - conversation.findUnreadMessages(mNotificationService::pushFromBacklog); + conversation.findUnreadMessagesAndCalls(mNotificationService::pushFromBacklog); } public void loadPhoneContacts() { 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 353851c37f0cdf63568740b94967c0bf9a67ff64..c69fc6b023eaec40c762569d6ae6de91bb01ca5d 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -1110,6 +1110,7 @@ public class JingleRtpConnection extends AbstractJingleConnection rejectCallFromSessionInitiate(); break; } + xmppConnectionService.getNotificationService().pushMissedCallNow(message); } private void cancelRingingTimeout() { @@ -1187,6 +1188,7 @@ public class JingleRtpConnection extends AbstractJingleConnection this.state == State.PROCEED ? State.RETRACTED_RACED : State.RETRACTED; if (transition(target)) { xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); + xmppConnectionService.getNotificationService().pushMissedCallNow(message); Log.d( Config.LOGTAG, id.account.getJid().asBareJid() diff --git a/src/main/res/drawable-hdpi/ic_missed_call_notification.png b/src/main/res/drawable-hdpi/ic_missed_call_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..3608ebd92478ee32c52ad4feba3b261218ebc50d GIT binary patch literal 810 zcmV+_1J(SAP)oxe((33 z_x;c1Kk%Os%1%%NuoBn+tN`W%GXR12KtJ#lxF16JP(RhNBJJui^_9i=soqhy)?1`S zJ+6K$D3Zkkb$vZ#7OKxCAd+TSJ*Q5uD$}LDtCLKcyXwrcGOL|fi}JXyPAe+YsSZt| zOv2S7GR^9f2FN7r%@aDg=>bQ%a@8{Iq-9HsW>?#QYd}Zpa8@fu$f!-gWnfiC+{pFSJ>!)7 zOdQi>^y-1STzy@nO{uw8#)Pg?w6pvuX$-yaYA_Z%R~V_c*zmNni15Pk{=x z#rwTEl`2!2gzuwUwzh=Ocv4klR0l>2wHo!Ys8E*FePfy!C(PnPEVC$M?2l9` zPgxn&(^*1?OD0LU)$V&>2*xt5men4R43~u;iKlQXi?eanvk(!Uty+Z`#3n@)3yp#bDn2DYi`|9EFzXxg-f5P-bMHC# z+}W886hhqui~@gvk0FG=rM3SfJ+9tY-@6(s>YRE*omdO;LG`I+_^sYmx0aI6sNV|7 zV=Sl#`jJo99jz4~)Uh`5ed^Z%$Q!&+*X0Cehd|!oLc<%HO$FXG??b>6us?+0`Z{nn zAzuU@WeRr#=QHs+U;@}yzwZT(0j~g1yNSiexwh1!Ei>P)o=)P|02&8-+Ptb$z$~yC z*pMv01WwlRo5|XEO!6t`Ks{AIPpCgyI^Sh=22wl#7^yXuTMS%MJ0XPUzzpy+(|H$| z%GCaf1(w=I1YAu-x&`C_@8S_%PVCLA7uDBMtP|z8jU~pJSYS-;^(j#86q0{ex2*D` z-YyVO{Zf!~ui+huG_LpAiv~Liaz?XJ^>A%(P%NwYYezkzE)GHDP&y(2LJ0Fz_sAWf zS1^t?O!v)D$JHz9Gpqk{&+0_KRi9XW-jnJ%tNUyva`13~=2{z?RBx%D)rFx8L_!!cl58m?00000NkvXXu0mjfUpoA) literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xhdpi/ic_missed_call_notification.png b/src/main/res/drawable-xhdpi/ic_missed_call_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..80cd15819fcca113e91cd6a57468a3062926bfed GIT binary patch literal 1151 zcmV-_1c3XAP)dAf*T*3p6S`T|i9B zAdVfPHxWXzpo}6asy`rl&@Mz52ASp^8>>}brSqec?BB4RTxkz&Ie0o@W+3fAU zthM*r``8CItToL1=KGzQH8Xn#{^vy10o6-^<-h`9F>o4~0geJkB4W16`g%5?UI9D+ z+zH$YT+$}{EN~Fm1AGR29T8`G)leEW)b;9@>KVJ|nEJAM*&xN=t{&(?bTejZMlSBB z@DcSbb*^IZDM!@X`XPRWdZ>@WQ)bm?suaIU{c}L#Q{GUAdli3!dU77b=e(zOH&d$1 z)D!b4KIf%w#9yd>GvCT<%&8k&X&q{j{ycC?Y3KbRfH&18g>swCIDgM?qc`QXR_+=N zdho5a3B*X^GzxCB#hu_W<8@$hS6qxgdESD^SU)h&WkPk$S3N)+u0J zMC?`f0=t0KE%M%xzBKm0d~jAgKtxOeZ@0-mYO*GzJ+Pvi+RZjxuK?FItE@xuI`Or5 z4+5`uD7PH=zEQ}*0*lEbElgal{#bHhnpW2|#2Gi+PIqE`UM3wvBOP2fie6Tmxrm4p zz}>*l1&dDsYa?P$5^nJLQQF$P%^y+&2f7H9(yAx+QC-VJ){}qaFKK|U0O{?t zZfi~8qY+_599LIIgn5RV_-k@iDNY--^0mO=CiOESpyF^;`A0_1lL@^*42w`l`CgCU?W?kh)I&$o6}u zrzD71VTUS@8SJlk)??!*n{0mB(A6Sh= RVebF{002ovPDHLkV1lNL8CC!Q literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxhdpi/ic_missed_call_notification.png b/src/main/res/drawable-xxhdpi/ic_missed_call_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..0072d2ef0d9253358efbc88d9ed7882b77e27be1 GIT binary patch literal 1680 zcmV;B25K~#90?VMe#9aR;_f2)*Iga|Dp7*dgvSQQ@l?1O;P!h?iB zga_ck05%vQKmq}R7-Fo65QO+Ze9Qry|-H(hO$9jHp+cATISWIz=Oc4qjWwYHUsBG z#PdxW*A_@!4m<*Uwn>K_u?09kBG%V!SW6)FNMHl7ylzKRz&7CQi1|NqO(&1})1$i6Z=QilzKGK(mQTY<^ zg%<2=08X#j<~-mBLu;uoHQ1OMX;W&T@1KmkMW%Q3Y^|-?Zm0UmtWOIv4jus<*T&6u z#BC9A6F@|)1-@6K?Ltxn??!eGI*|^)wvNbc5pm1lg`P;a*06C>_Ci*mHpYJ!+?f>z zKwShJU&GGlvKO+G^op8}RYE-rxTr$aIU!zwIg!)rb20FH;OrWsPQ2=3 z_2Ch17biogFE%uq73diC=Z;6Daf+j9ldw>rryCm06G(N&c10S;uo~ne%Qn!|M;jV0 z5=h$n2*V+9I4VMX|<-_e473`0DSoImYirw1K@Gyb2xk!@|NA=ZV0$tkB za20{H{&1Vbuo|RrZ@S;{%7%uk3Z&H|X_ArC=gw?@*){(;U@I`^wZBEy0HIzU5wrb^ z5izT-0R-MDdAIrv^KC>O&2buM}dCojGRK>X*qAuzG zfI}*@Sp)nmE689}^|9HDXI=ga{C#LG^+Mj1C*<59Gp6N! zQp2jxJw;lSB`;hCtgEbQd#nRK#ds&=K?F{Th-dQ(%LH0T$_*;thTQY{LquFqR9Lur zM8q!O3X;O}nK#M`OB1Lf;wj+XR^6FCN^>uVegiXQ)jvZz88{!=1uTt-_wovdCUHi@ zyTAp&o`1qGpM}6_MTM2pfP3d0u5HVsDP&bqLDf8IBI2jOciQk~3i)zTL6gHQKLoBH z$(`wACutsfKU+{ag8D?n4ZwBGujPM#3XXQw76>3B?gB0cc8uWEw2`~qHxdXSB4$Xc zzuDBrl#wgY$hknKtgIgam$YH8Lv{d5B4XR%#l{iTCnB~+#AT$V^2Jf@b;u&1(w1u@ zdaorX1K(iC`^^-yDtnoTX(yee{nI45OTz+l?R zcHn*BKfphMSAdPA?{?2e#77nTJB_qub0M&}qMK(%#1j?kP7c#n>k`NC-xAcN>UHWX z6{2Wu+x_oRsAs6Z>$5^1lsM)#-99O*9xYLvel%6zE2mO8{5Y| an(;C5E_<216#l;e0000X6P)W!p2+^2O#IMAcNF>G&N{Z0Lchpd60SQo$(n^|e5fFSsG}7AkQY^H+ ze?QDQk$XG4JG(P`cJDd+`?xzh^PJ~7^UUir105Y59gSg43s8illYu3`Qs5L|40ttg z9B?e~0&oDB0`>#{03HLLFtgoF$Xte289>qkUH*gp5doz0} zPRb!e(wL;nCH+>?e^oCBB>hU#XC=L=>EGTcl2%Ikk)#)DR(cjwl5Uc8ZqvFL7?S2m zx=7N44Jkc~hb3JmXk@R@8dM;#_q>nYNtE5O;Drs}WiY?0)NoO>zyC@_r zkaWGIL(S^4G6yAHC+QV+>Zz7)sHA1Ut-#yr$bZ0W2QD$QC+o;Od7~rgGMv!EpnQO{ zfE|)9tt0bf29Pvg(ha~5aii2>Vhq?Q>03(NQ%d53B)t;23HVfs{94XNU~Q@qb+iE_ zodDbhoE;NqBqhlsV3)>r+wbQ>UP0@wn~uUhC3 zatN3(v-`__SjqsB76UtRD^nda`5W*KGkdPgM`c~m_vnPyi6mK zj%Um)a}1TWm~a;H%AW*28kt8CzXaY~Q{LNwYk*sD+u^Hns-$IR_L%QwmI2O>D6r3q zX7*Hqd@_;rVoh1^1Xh{Z9!VGAmR)0va6T?(;V81&fD{IptLy|mXl8o=%xoKQ0WSEX zDw+B`$^gCP#&<^)Hka8BoYz-=p9DsDZ`KQ64LFVA+gKICcHC65cmAelgfCajd$A;M zq>s-4b9+~CHTph3bMj2ma^UL`Wi0hQ^fkiXgmu!JbNUkH*O^aEP<-w`A3)LyU^8(I z@iO3n*$;gNctb=Xl`ym2k|u#4FxbWZK`9?HW)*xcsFA^!nQa6v1r86W$WXWQ8{(Bu zRvtbBj3p?n5@xmuSVR3rn9qReLitdl2GrgKm9!ym zPi4;tR{%#67wzrm&ysBgZsToA47i3|4eY2cOuBcyH<2JQ1_dXZ&-yGTC|Qx^*0`NMKQ*(=ZhfW!dad@)Y7r@p+=f}rH@LI;N#4t% z2$}Z*_a+r59h-m|ZGzh4^ZWb>SW>A50GQc*xE@g(ncVrIL5kC3t7r2`0Ph*O*$y$bj_uJ7A169J3O?B7`lqKEHhwi$QlSE=I~ zM+Bq54JDaBF|!AH1^qQeenUoXL!4{^%giQm0m<7^>}V@xa}H)h{!DB2yW-v zI^c8UY$J?d9&NnjNYY7?)=RoiZVUIIcuCTolGaIjjYqCCBz;rTo`hw3r}0~Jqqd~e zft9#X^#pK~Vj`Hv?NHu|n@YbYaPj+3-j)h?wTwa!PqNIFx}jgs~U8sNI7Wgbc-jY+yv(vAWH#2k6n5rU+5 zO8UN}=VvK@N^U^fF$g3rlMessages Incoming calls Ongoing calls + Missed calls Silent messages This notification group is used to display notifications that should not trigger any sound. For example when being active on another device (Grace Period). Failed deliveries @@ -934,6 +935,10 @@ Outgoing call Outgoing call · %s Missed call + Missed call from %s + %1$d missed calls from %2$s + %d missed calls + %1$d missed calls from %2$d contacts Audio call Video call Help From f8b9e15634e2ddb21899a009770a9252d4979290 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 29 Aug 2022 13:01:20 +0200 Subject: [PATCH 006/101] fixups for missed call notifications --- .../services/NotificationService.java | 81 ++++++++++++++----- .../services/XmppConnectionService.java | 2 +- 2 files changed, 60 insertions(+), 23 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index b6916020daff0d2e17d18d8e09a832251bdfebf5..92c777fb40caa8b07c4885d3fb792b4f21ebae63 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -103,7 +103,8 @@ public class NotificationService { private final XmppConnectionService mXmppConnectionService; private final LinkedHashMap> notifications = new LinkedHashMap<>(); private final HashMap mBacklogMessageCounter = new HashMap<>(); - private final LinkedHashMap mMissedCalls = new LinkedHashMap<>(); + private final LinkedHashMap mMissedCalls = + new LinkedHashMap<>(); private Conversation mOpenConversation; private boolean mIsInForeground; private long mLastNotification; @@ -224,9 +225,11 @@ public class NotificationService { ongoingCallsChannel.setGroup("calls"); notificationManager.createNotificationChannel(ongoingCallsChannel); - final NotificationChannel missedCallsChannel = new NotificationChannel("missed_calls", - c.getString(R.string.missed_calls_channel_name), - NotificationManager.IMPORTANCE_HIGH); + final NotificationChannel missedCallsChannel = + new NotificationChannel( + "missed_calls", + c.getString(R.string.missed_calls_channel_name), + NotificationManager.IMPORTANCE_HIGH); missedCallsChannel.setShowBadge(true); missedCallsChannel.setSound(null, null); missedCallsChannel.setLightColor(LED_COLOR); @@ -413,8 +416,8 @@ public class NotificationService { return count; } - void finishBacklog(boolean notify) { - finishBacklog(notify, null); + void finishBacklog() { + finishBacklog(false, null); } private void pushToStack(final Message message) { @@ -967,7 +970,8 @@ public class NotificationService { } private Builder buildMissedCallsSummary(boolean publicVersion) { - final Builder builder = new NotificationCompat.Builder(mXmppConnectionService, "missed_calls"); + final Builder builder = + new NotificationCompat.Builder(mXmppConnectionService, "missed_calls"); int totalCalls = 0; final StringBuilder names = new StringBuilder(); long lastTime = 0; @@ -982,9 +986,16 @@ public class NotificationService { if (names.length() >= 2) { names.delete(names.length() - 2, names.length()); } - final String title = (totalCalls == 1) ? mXmppConnectionService.getString(R.string.missed_call) : - (mMissedCalls.size() == 1) ? mXmppConnectionService.getString(R.string.n_missed_calls, totalCalls) : - mXmppConnectionService.getString(R.string.n_missed_calls_from_m_contacts, totalCalls, mMissedCalls.size()); + final String title = + (totalCalls == 1) + ? mXmppConnectionService.getString(R.string.missed_call) + : (mMissedCalls.size() == 1) + ? mXmppConnectionService.getString( + R.string.n_missed_calls, totalCalls) + : mXmppConnectionService.getString( + R.string.n_missed_calls_from_m_contacts, + totalCalls, + mMissedCalls.size()); builder.setContentTitle(title); builder.setTicker(title); if (!publicVersion) { @@ -1012,19 +1023,27 @@ public class NotificationService { return builder.build(); } - private Builder buildMissedCall(final Conversational conversation, final MissedCallsInfo info, boolean publicVersion) { - final Builder builder = new NotificationCompat.Builder(mXmppConnectionService, "missed_calls"); - final String title = (info.getNumberOfCalls() == 1) ? mXmppConnectionService.getString(R.string.missed_call) : - mXmppConnectionService.getString(R.string.n_missed_calls, info.getNumberOfCalls()); + private Builder buildMissedCall( + final Conversational conversation, final MissedCallsInfo info, boolean publicVersion) { + final Builder builder = + new NotificationCompat.Builder(mXmppConnectionService, "missed_calls"); + final String title = + (info.getNumberOfCalls() == 1) + ? mXmppConnectionService.getString(R.string.missed_call) + : mXmppConnectionService.getString( + R.string.n_missed_calls, info.getNumberOfCalls()); builder.setContentTitle(title); final String name = conversation.getContact().getDisplayName(); if (publicVersion) { builder.setTicker(title); } else { if (info.getNumberOfCalls() == 1) { - builder.setTicker(mXmppConnectionService.getString(R.string.missed_call_from_x, name)); + builder.setTicker( + mXmppConnectionService.getString(R.string.missed_call_from_x, name)); } else { - builder.setTicker(mXmppConnectionService.getString(R.string.n_missed_calls_from_x, info.getNumberOfCalls(), name)); + builder.setTicker( + mXmppConnectionService.getString( + R.string.n_missed_calls_from_x, info.getNumberOfCalls(), name)); } builder.setContentText(name); } @@ -1035,15 +1054,20 @@ public class NotificationService { builder.setContentIntent(createContentIntent(conversation)); builder.setDeleteIntent(createMissedCallsDeleteIntent(conversation)); if (!publicVersion && conversation instanceof Conversation) { - builder.setLargeIcon(mXmppConnectionService.getAvatarService() - .get((Conversation) conversation, AvatarService.getSystemUiAvatarSize(mXmppConnectionService))); + builder.setLargeIcon( + mXmppConnectionService + .getAvatarService() + .get( + (Conversation) conversation, + AvatarService.getSystemUiAvatarSize(mXmppConnectionService))); } modifyMissedCall(builder); return builder; } private void modifyMissedCall(final Builder builder) { - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService); + final SharedPreferences preferences = + PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService); final Resources resources = mXmppConnectionService.getResources(); final boolean led = preferences.getBoolean("led", resources.getBoolean(R.bool.led)); if (led) { @@ -1521,7 +1545,7 @@ public class NotificationService { return createContentIntent(conversation.getUuid(), null); } - private PendingIntent createDeleteIntent(Conversation conversation) { + private PendingIntent createDeleteIntent(final Conversation conversation) { final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class); intent.setAction(XmppConnectionService.ACTION_CLEAR_MESSAGE_NOTIFICATION); if (conversation != null) { @@ -1548,9 +1572,21 @@ public class NotificationService { intent.setAction(XmppConnectionService.ACTION_CLEAR_MISSED_CALL_NOTIFICATION); if (conversation != null) { intent.putExtra("uuid", conversation.getUuid()); - return PendingIntent.getService(mXmppConnectionService, generateRequestCode(conversation, 21), intent, 0); + return PendingIntent.getService( + mXmppConnectionService, + generateRequestCode(conversation, 21), + intent, + s() + ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT); } - return PendingIntent.getService(mXmppConnectionService, 1, intent, 0); + return PendingIntent.getService( + mXmppConnectionService, + 1, + intent, + s() + ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT); } private PendingIntent createReplyIntent( @@ -1896,6 +1932,7 @@ public class NotificationService { return lastTime; } } + private class VibrationRunnable implements Runnable { @Override diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 24545424743c444d0f6c4601fa45d3fdc895e58f..517a63a6a93cef4326d10d9c9b5d1d1f87c2e050 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -1958,7 +1958,7 @@ public class XmppConnectionService extends Service { restoreMessages(conversation); } } - mNotificationService.finishBacklog(false); + mNotificationService.finishBacklog(); restoredFromDatabaseLatch.countDown(); final long diffMessageRestore = SystemClock.elapsedRealtime() - startMessageRestore; Log.d(Config.LOGTAG, "finished restoring messages in " + diffMessageRestore + "ms"); From b792563fad694286ee79ea7205853653502c7c7a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 29 Aug 2022 14:04:33 +0200 Subject: [PATCH 007/101] use non-custom missed called --- art/ic_missed_call_notification.svg | 344 ------------------ .../services/NotificationService.java | 82 ++--- .../ic_missed_call_notification.png | Bin 810 -> 0 bytes .../ic_missed_call_notification.png | Bin 589 -> 0 bytes .../ic_missed_call_notification.png | Bin 1151 -> 0 bytes .../ic_missed_call_notification.png | Bin 1680 -> 0 bytes .../ic_missed_call_notification.png | Bin 2179 -> 0 bytes .../drawable/ic_call_missed_white_24db.xml | 5 + 8 files changed, 43 insertions(+), 388 deletions(-) delete mode 100644 art/ic_missed_call_notification.svg delete mode 100644 src/main/res/drawable-hdpi/ic_missed_call_notification.png delete mode 100644 src/main/res/drawable-mdpi/ic_missed_call_notification.png delete mode 100644 src/main/res/drawable-xhdpi/ic_missed_call_notification.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_missed_call_notification.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_missed_call_notification.png create mode 100644 src/main/res/drawable/ic_call_missed_white_24db.xml diff --git a/art/ic_missed_call_notification.svg b/art/ic_missed_call_notification.svg deleted file mode 100644 index 78f0acead8673ea1623d4e72e64a72b0bf012142..0000000000000000000000000000000000000000 --- a/art/ic_missed_call_notification.svg +++ /dev/null @@ -1,344 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index 92c777fb40caa8b07c4885d3fb792b4f21ebae63..55e220f62262495a0088098e10cc001f3613d89b 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -36,6 +36,7 @@ import androidx.core.app.RemoteInput; import androidx.core.content.ContextCompat; import androidx.core.graphics.drawable.IconCompat; +import com.google.common.base.Joiner; import com.google.common.base.Strings; import com.google.common.collect.Iterables; @@ -973,19 +974,15 @@ public class NotificationService { final Builder builder = new NotificationCompat.Builder(mXmppConnectionService, "missed_calls"); int totalCalls = 0; - final StringBuilder names = new StringBuilder(); + final List names = new ArrayList<>(); long lastTime = 0; - for (Map.Entry entry : mMissedCalls.entrySet()) { + for (final Map.Entry entry : mMissedCalls.entrySet()) { final Conversational conversation = entry.getKey(); final MissedCallsInfo missedCallsInfo = entry.getValue(); - names.append(conversation.getContact().getDisplayName()); - names.append(", "); + names.add(conversation.getContact().getDisplayName()); totalCalls += missedCallsInfo.getNumberOfCalls(); lastTime = Math.max(lastTime, missedCallsInfo.getLastTime()); } - if (names.length() >= 2) { - names.delete(names.length() - 2, names.length()); - } final String title = (totalCalls == 1) ? mXmppConnectionService.getString(R.string.missed_call) @@ -999,9 +996,9 @@ public class NotificationService { builder.setContentTitle(title); builder.setTicker(title); if (!publicVersion) { - builder.setContentText(names.toString()); + builder.setContentText(Joiner.on(", ").join(names)); } - builder.setSmallIcon(R.drawable.ic_missed_call_notification); + builder.setSmallIcon(R.drawable.ic_call_missed_white_24db); builder.setGroupSummary(true); builder.setGroup(MISSED_CALLS_GROUP); builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN); @@ -1047,7 +1044,7 @@ public class NotificationService { } builder.setContentText(name); } - builder.setSmallIcon(R.drawable.ic_missed_call_notification); + builder.setSmallIcon(R.drawable.ic_call_missed_white_24db); builder.setGroup(MISSED_CALLS_GROUP); builder.setCategory(NotificationCompat.CATEGORY_CALL); builder.setWhen(info.getLastTime()); @@ -1091,42 +1088,39 @@ public class NotificationService { R.plurals.x_unread_conversations, notifications.size(), notifications.size())); - final StringBuilder names = new StringBuilder(); + final List names = new ArrayList<>(); Conversation conversation = null; for (final ArrayList messages : notifications.values()) { - if (messages.size() > 0) { - conversation = (Conversation) messages.get(0).getConversation(); - final String name = conversation.getName().toString(); - SpannableString styledString; - if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) { - int count = messages.size(); - styledString = - new SpannableString( - name - + ": " - + mXmppConnectionService - .getResources() - .getQuantityString( - R.plurals.x_messages, count, count)); - styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0); - style.addLine(styledString); - } else { - styledString = - new SpannableString( - name - + ": " - + UIHelper.getMessagePreview( - mXmppConnectionService, messages.get(0)) - .first); - styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0); - style.addLine(styledString); - } - names.append(name); - names.append(", "); + if (messages.isEmpty()) { + continue; } - } - if (names.length() >= 2) { - names.delete(names.length() - 2, names.length()); + conversation = (Conversation) messages.get(0).getConversation(); + final String name = conversation.getName().toString(); + SpannableString styledString; + if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) { + int count = messages.size(); + styledString = + new SpannableString( + name + + ": " + + mXmppConnectionService + .getResources() + .getQuantityString( + R.plurals.x_messages, count, count)); + styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0); + style.addLine(styledString); + } else { + styledString = + new SpannableString( + name + + ": " + + UIHelper.getMessagePreview( + mXmppConnectionService, messages.get(0)) + .first); + styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0); + style.addLine(styledString); + } + names.add(name); } final String contentTitle = mXmppConnectionService @@ -1137,7 +1131,7 @@ public class NotificationService { notifications.size()); mBuilder.setContentTitle(contentTitle); mBuilder.setTicker(contentTitle); - mBuilder.setContentText(names.toString()); + mBuilder.setContentText(Joiner.on(", ").join(names)); mBuilder.setStyle(style); if (conversation != null) { mBuilder.setContentIntent(createContentIntent(conversation)); diff --git a/src/main/res/drawable-hdpi/ic_missed_call_notification.png b/src/main/res/drawable-hdpi/ic_missed_call_notification.png deleted file mode 100644 index 3608ebd92478ee32c52ad4feba3b261218ebc50d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 810 zcmV+_1J(SAP)oxe((33 z_x;c1Kk%Os%1%%NuoBn+tN`W%GXR12KtJ#lxF16JP(RhNBJJui^_9i=soqhy)?1`S zJ+6K$D3Zkkb$vZ#7OKxCAd+TSJ*Q5uD$}LDtCLKcyXwrcGOL|fi}JXyPAe+YsSZt| zOv2S7GR^9f2FN7r%@aDg=>bQ%a@8{Iq-9HsW>?#QYd}Zpa8@fu$f!-gWnfiC+{pFSJ>!)7 zOdQi>^y-1STzy@nO{uw8#)Pg?w6pvuX$-yaYA_Z%R~V_c*zmNni15Pk{=x z#rwTEl`2!2gzuwUwzh=Ocv4klR0l>2wHo!Ys8E*FePfy!C(PnPEVC$M?2l9` zPgxn&(^*1?OD0LU)$V&>2*xt5men4R43~u;iKlQXi?eanvk(!Uty+Z`#3n@)3yp#bDn2DYi`|9EFzXxg-f5P-bMHC# z+}W886hhqui~@gvk0FG=rM3SfJ+9tY-@6(s>YRE*omdO;LG`I+_^sYmx0aI6sNV|7 zV=Sl#`jJo99jz4~)Uh`5ed^Z%$Q!&+*X0Cehd|!oLc<%HO$FXG??b>6us?+0`Z{nn zAzuU@WeRr#=QHs+U;@}yzwZT(0j~g1yNSiexwh1!Ei>P)o=)P|02&8-+Ptb$z$~yC z*pMv01WwlRo5|XEO!6t`Ks{AIPpCgyI^Sh=22wl#7^yXuTMS%MJ0XPUzzpy+(|H$| z%GCaf1(w=I1YAu-x&`C_@8S_%PVCLA7uDBMtP|z8jU~pJSYS-;^(j#86q0{ex2*D` z-YyVO{Zf!~ui+huG_LpAiv~Liaz?XJ^>A%(P%NwYYezkzE)GHDP&y(2LJ0Fz_sAWf zS1^t?O!v)D$JHz9Gpqk{&+0_KRi9XW-jnJ%tNUyva`13~=2{z?RBx%D)rFx8L_!!cl58m?00000NkvXXu0mjfUpoA) diff --git a/src/main/res/drawable-xhdpi/ic_missed_call_notification.png b/src/main/res/drawable-xhdpi/ic_missed_call_notification.png deleted file mode 100644 index 80cd15819fcca113e91cd6a57468a3062926bfed..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1151 zcmV-_1c3XAP)dAf*T*3p6S`T|i9B zAdVfPHxWXzpo}6asy`rl&@Mz52ASp^8>>}brSqec?BB4RTxkz&Ie0o@W+3fAU zthM*r``8CItToL1=KGzQH8Xn#{^vy10o6-^<-h`9F>o4~0geJkB4W16`g%5?UI9D+ z+zH$YT+$}{EN~Fm1AGR29T8`G)leEW)b;9@>KVJ|nEJAM*&xN=t{&(?bTejZMlSBB z@DcSbb*^IZDM!@X`XPRWdZ>@WQ)bm?suaIU{c}L#Q{GUAdli3!dU77b=e(zOH&d$1 z)D!b4KIf%w#9yd>GvCT<%&8k&X&q{j{ycC?Y3KbRfH&18g>swCIDgM?qc`QXR_+=N zdho5a3B*X^GzxCB#hu_W<8@$hS6qxgdESD^SU)h&WkPk$S3N)+u0J zMC?`f0=t0KE%M%xzBKm0d~jAgKtxOeZ@0-mYO*GzJ+Pvi+RZjxuK?FItE@xuI`Or5 z4+5`uD7PH=zEQ}*0*lEbElgal{#bHhnpW2|#2Gi+PIqE`UM3wvBOP2fie6Tmxrm4p zz}>*l1&dDsYa?P$5^nJLQQF$P%^y+&2f7H9(yAx+QC-VJ){}qaFKK|U0O{?t zZfi~8qY+_599LIIgn5RV_-k@iDNY--^0mO=CiOESpyF^;`A0_1lL@^*42w`l`CgCU?W?kh)I&$o6}u zrzD71VTUS@8SJlk)??!*n{0mB(A6Sh= RVebF{002ovPDHLkV1lNL8CC!Q diff --git a/src/main/res/drawable-xxhdpi/ic_missed_call_notification.png b/src/main/res/drawable-xxhdpi/ic_missed_call_notification.png deleted file mode 100644 index 0072d2ef0d9253358efbc88d9ed7882b77e27be1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1680 zcmV;B25K~#90?VMe#9aR;_f2)*Iga|Dp7*dgvSQQ@l?1O;P!h?iB zga_ck05%vQKmq}R7-Fo65QO+Ze9Qry|-H(hO$9jHp+cATISWIz=Oc4qjWwYHUsBG z#PdxW*A_@!4m<*Uwn>K_u?09kBG%V!SW6)FNMHl7ylzKRz&7CQi1|NqO(&1})1$i6Z=QilzKGK(mQTY<^ zg%<2=08X#j<~-mBLu;uoHQ1OMX;W&T@1KmkMW%Q3Y^|-?Zm0UmtWOIv4jus<*T&6u z#BC9A6F@|)1-@6K?Ltxn??!eGI*|^)wvNbc5pm1lg`P;a*06C>_Ci*mHpYJ!+?f>z zKwShJU&GGlvKO+G^op8}RYE-rxTr$aIU!zwIg!)rb20FH;OrWsPQ2=3 z_2Ch17biogFE%uq73diC=Z;6Daf+j9ldw>rryCm06G(N&c10S;uo~ne%Qn!|M;jV0 z5=h$n2*V+9I4VMX|<-_e473`0DSoImYirw1K@Gyb2xk!@|NA=ZV0$tkB za20{H{&1Vbuo|RrZ@S;{%7%uk3Z&H|X_ArC=gw?@*){(;U@I`^wZBEy0HIzU5wrb^ z5izT-0R-MDdAIrv^KC>O&2buM}dCojGRK>X*qAuzG zfI}*@Sp)nmE689}^|9HDXI=ga{C#LG^+Mj1C*<59Gp6N! zQp2jxJw;lSB`;hCtgEbQd#nRK#ds&=K?F{Th-dQ(%LH0T$_*;thTQY{LquFqR9Lur zM8q!O3X;O}nK#M`OB1Lf;wj+XR^6FCN^>uVegiXQ)jvZz88{!=1uTt-_wovdCUHi@ zyTAp&o`1qGpM}6_MTM2pfP3d0u5HVsDP&bqLDf8IBI2jOciQk~3i)zTL6gHQKLoBH z$(`wACutsfKU+{ag8D?n4ZwBGujPM#3XXQw76>3B?gB0cc8uWEw2`~qHxdXSB4$Xc zzuDBrl#wgY$hknKtgIgam$YH8Lv{d5B4XR%#l{iTCnB~+#AT$V^2Jf@b;u&1(w1u@ zdaorX1K(iC`^^-yDtnoTX(yee{nI45OTz+l?R zcHn*BKfphMSAdPA?{?2e#77nTJB_qub0M&}qMK(%#1j?kP7c#n>k`NC-xAcN>UHWX z6{2Wu+x_oRsAs6Z>$5^1lsM)#-99O*9xYLvel%6zE2mO8{5Y| an(;C5E_<216#l;e0000X6P)W!p2+^2O#IMAcNF>G&N{Z0Lchpd60SQo$(n^|e5fFSsG}7AkQY^H+ ze?QDQk$XG4JG(P`cJDd+`?xzh^PJ~7^UUir105Y59gSg43s8illYu3`Qs5L|40ttg z9B?e~0&oDB0`>#{03HLLFtgoF$Xte289>qkUH*gp5doz0} zPRb!e(wL;nCH+>?e^oCBB>hU#XC=L=>EGTcl2%Ikk)#)DR(cjwl5Uc8ZqvFL7?S2m zx=7N44Jkc~hb3JmXk@R@8dM;#_q>nYNtE5O;Drs}WiY?0)NoO>zyC@_r zkaWGIL(S^4G6yAHC+QV+>Zz7)sHA1Ut-#yr$bZ0W2QD$QC+o;Od7~rgGMv!EpnQO{ zfE|)9tt0bf29Pvg(ha~5aii2>Vhq?Q>03(NQ%d53B)t;23HVfs{94XNU~Q@qb+iE_ zodDbhoE;NqBqhlsV3)>r+wbQ>UP0@wn~uUhC3 zatN3(v-`__SjqsB76UtRD^nda`5W*KGkdPgM`c~m_vnPyi6mK zj%Um)a}1TWm~a;H%AW*28kt8CzXaY~Q{LNwYk*sD+u^Hns-$IR_L%QwmI2O>D6r3q zX7*Hqd@_;rVoh1^1Xh{Z9!VGAmR)0va6T?(;V81&fD{IptLy|mXl8o=%xoKQ0WSEX zDw+B`$^gCP#&<^)Hka8BoYz-=p9DsDZ`KQ64LFVA+gKICcHC65cmAelgfCajd$A;M zq>s-4b9+~CHTph3bMj2ma^UL`Wi0hQ^fkiXgmu!JbNUkH*O^aEP<-w`A3)LyU^8(I z@iO3n*$;gNctb=Xl`ym2k|u#4FxbWZK`9?HW)*xcsFA^!nQa6v1r86W$WXWQ8{(Bu zRvtbBj3p?n5@xmuSVR3rn9qReLitdl2GrgKm9!ym zPi4;tR{%#67wzrm&ysBgZsToA47i3|4eY2cOuBcyH<2JQ1_dXZ&-yGTC|Qx^*0`NMKQ*(=ZhfW!dad@)Y7r@p+=f}rH@LI;N#4t% z2$}Z*_a+r59h-m|ZGzh4^ZWb>SW>A50GQc*xE@g(ncVrIL5kC3t7r2`0Ph*O*$y$bj_uJ7A169J3O?B7`lqKEHhwi$QlSE=I~ zM+Bq54JDaBF|!AH1^qQeenUoXL!4{^%giQm0m<7^>}V@xa}H)h{!DB2yW-v zI^c8UY$J?d9&NnjNYY7?)=RoiZVUIIcuCTolGaIjjYqCCBz;rTo`hw3r}0~Jqqd~e zft9#X^#pK~Vj`Hv?NHu|n@YbYaPj+3-j)h?wTwa!PqNIFx}jgs~U8sNI7Wgbc-jY+yv(vAWH#2k6n5rU+5 zO8UN}=VvK@N^U^fF$g3rl + + From a717917b3de98b8d88bed4145dae15fd968f5f00 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 29 Aug 2022 15:09:53 +0200 Subject: [PATCH 008/101] explicitly search for namespaces when processing stream features --- .../crypto/sasl/SaslMechanism.java | 4 ++ .../eu/siacs/conversations/xml/Namespace.java | 2 + .../conversations/xmpp/XmppConnection.java | 48 ++++++++++++++----- 3 files changed, 42 insertions(+), 12 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java index 86fd6524e4f1ecbbfaca32b39415c04621fd4fb5..f6024210a2a9fbd9af703908804ba9ac76dac20e 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java @@ -66,4 +66,8 @@ public abstract class SaslMechanism { public String getResponse(final String challenge) throws AuthenticationException { return ""; } + + public enum Version { + SASL, SASL_2 + } } diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index 72c35a92f6f74dbc9f869610e60eb40d4f29f2ba..c2a7af60779d47feadab960b6103aa998cd8e9ed 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -7,6 +7,7 @@ public final class Namespace { public static final String BLOCKING = "urn:xmpp:blocking"; public static final String ROSTER = "jabber:iq:roster"; public static final String REGISTER = "jabber:iq:register"; + public static final String REGISTER_STREAM_FEATURE = "http://jabber.org/features/iq-register"; public static final String BYTE_STREAMS = "http://jabber.org/protocol/bytestreams"; public static final String HTTP_UPLOAD = "urn:xmpp:http:upload:0"; public static final String HTTP_UPLOAD_LEGACY = "urn:xmpp:http:upload"; @@ -15,6 +16,7 @@ public final class Namespace { public static final String DATA = "jabber:x:data"; public static final String OOB = "jabber:x:oob"; public static final String SASL = "urn:ietf:params:xml:ns:xmpp-sasl"; + public static final String SASL_2 = "urn:xmpp:sasl:1"; public static final String TLS = "urn:ietf:params:xml:ns:xmpp-tls"; public static final String PUBSUB = "http://jabber.org/protocol/pubsub"; public static final String PUBSUB_PUBLISH_OPTIONS = PUBSUB + "#publish-options"; diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 06195aaed1647e471d27df15f92b1bfe536630dc..2222da3e294157e1f6d8389321e1ef578a765301 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -848,40 +848,64 @@ public class XmppConnection implements Runnable { private void processStreamFeatures(final Tag currentTag) throws IOException { this.streamFeatures = tagReader.readElement(currentTag); - final boolean isSecure = features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS || account.isOnion(); + Log.d(Config.LOGTAG, this.streamFeatures.toString()); + final boolean isSecure = + features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS || account.isOnion(); final boolean needsBinding = !isBound && !account.isOptionSet(Account.OPTION_REGISTER); - if (this.streamFeatures.hasChild("starttls") && !features.encryptionEnabled) { + if (this.streamFeatures.hasChild("starttls", Namespace.TLS) + && !features.encryptionEnabled) { sendStartTLS(); - } else if (this.streamFeatures.hasChild("register") && account.isOptionSet(Account.OPTION_REGISTER)) { + } else if (this.streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE) + && account.isOptionSet(Account.OPTION_REGISTER)) { if (isSecure) { register(); } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to find STARTTLS for registration process " + XmlHelper.printElementNames(this.streamFeatures)); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": unable to find STARTTLS for registration process " + + XmlHelper.printElementNames(this.streamFeatures)); throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } - } else if (!this.streamFeatures.hasChild("register") && account.isOptionSet(Account.OPTION_REGISTER)) { + } else if (!this.streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE) + && account.isOptionSet(Account.OPTION_REGISTER)) { throw new StateChangingException(Account.State.REGISTRATION_NOT_SUPPORTED); - } else if (this.streamFeatures.hasChild("mechanisms") && shouldAuthenticate && isSecure) { - authenticate(); - } else if (this.streamFeatures.hasChild("sm", "urn:xmpp:sm:" + smVersion) && streamId != null) { + } else if (this.streamFeatures.hasChild("mechanisms", Namespace.SASL_2) + && shouldAuthenticate + && isSecure) { + authenticate(SaslMechanism.Version.SASL_2); + } else if (this.streamFeatures.hasChild("mechanisms", Namespace.SASL) + && shouldAuthenticate + && isSecure) { + authenticate(SaslMechanism.Version.SASL); + } else if (this.streamFeatures.hasChild("sm", "urn:xmpp:sm:" + smVersion) + && streamId != null) { if (Config.EXTENDED_SM_LOGGING) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": resuming after stanza #" + stanzasReceived); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": resuming after stanza #" + + stanzasReceived); } final ResumePacket resume = new ResumePacket(this.streamId, stanzasReceived, smVersion); this.mSmCatchupMessageCounter.set(0); this.mWaitingForSmCatchup.set(true); this.tagWriter.writeStanzaAsync(resume); } else if (needsBinding) { - if (this.streamFeatures.hasChild("bind") && isSecure) { + if (this.streamFeatures.hasChild("bind", Namespace.BIND) && isSecure) { sendBindRequest(); } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to find bind feature " + XmlHelper.printElementNames(this.streamFeatures)); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": unable to find bind feature " + + XmlHelper.printElementNames(this.streamFeatures)); throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } } } - private void authenticate() throws IOException { + private void authenticate(final SaslMechanism.Version version) throws IOException { final List mechanisms = extractMechanisms(streamFeatures.findChild("mechanisms")); final Element auth = new Element("auth", Namespace.SASL); if (mechanisms.contains(External.MECHANISM) && account.getPrivateKeyAlias() != null) { From 5fc8ff899aa2f81b29a2e19bef52f17ad1f898ef Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 29 Aug 2022 17:09:52 +0200 Subject: [PATCH 009/101] support logging in via SASL 2 --- .../crypto/sasl/SaslMechanism.java | 17 +- .../conversations/xmpp/XmppConnection.java | 160 ++++++++++++------ 2 files changed, 121 insertions(+), 56 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java index f6024210a2a9fbd9af703908804ba9ac76dac20e..b255b6f42e745c598ea2d86c6692bb09f9b0de74 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java @@ -1,8 +1,12 @@ package eu.siacs.conversations.crypto.sasl; +import com.google.common.base.Strings; + import java.security.SecureRandom; import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xml.TagWriter; public abstract class SaslMechanism { @@ -68,6 +72,17 @@ public abstract class SaslMechanism { } public enum Version { - SASL, SASL_2 + SASL, SASL_2; + + public static Version of(final Element element) { + switch ( Strings.nullToEmpty(element.getNamespace())) { + case Namespace.SASL: + return SASL; + case Namespace.SASL_2: + return SASL_2; + default: + throw new IllegalArgumentException("Unrecognized SASL namespace"); + } + } } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 2222da3e294157e1f6d8389321e1ef578a765301..bc77246e8d512601b81e94f86267c89c916df28c 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -469,63 +469,102 @@ public class XmppConnection implements Runnable { } else if (nextTag.isStart("proceed")) { switchOverToTls(); } else if (nextTag.isStart("success")) { - final String challenge = tagReader.readElement(nextTag).getContent(); + final Element success = tagReader.readElement(nextTag); + final SaslMechanism.Version version; + try { + version = SaslMechanism.Version.of(success); + } catch (final IllegalArgumentException e) { + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); + } + final String challenge; + if (version == SaslMechanism.Version.SASL) { + challenge = success.getContent(); + } else if (version == SaslMechanism.Version.SASL_2) { + challenge = success.findChildContent("additional-data"); + } else { + throw new AssertionError("Missing implementation for " + version); + } try { saslMechanism.getResponse(challenge); } catch (final SaslMechanism.AuthenticationException e) { Log.e(Config.LOGTAG, String.valueOf(e)); throw new StateChangingException(Account.State.UNAUTHORIZED); } - Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": logged in"); - account.setKey(Account.PINNED_MECHANISM_KEY, - String.valueOf(saslMechanism.getPriority())); - tagReader.reset(); - sendStartStream(); - final Tag tag = tagReader.readTag(); - if (tag != null && tag.isStart("stream")) { - processStream(); - } else { - throw new StateChangingException(Account.State.STREAM_OPENING_ERROR); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid().toString() + + ": logged in (using " + + version + + ")"); + account.setKey( + Account.PINNED_MECHANISM_KEY, String.valueOf(saslMechanism.getPriority())); + if (version == SaslMechanism.Version.SASL) { + tagReader.reset(); + sendStartStream(); + final Tag tag = tagReader.readTag(); + if (tag != null && tag.isStart("stream")) { + processStream(); + } else { + throw new StateChangingException(Account.State.STREAM_OPENING_ERROR); + } + break; } - break; } else if (nextTag.isStart("failure")) { final Element failure = tagReader.readElement(nextTag); - if (Namespace.SASL.equals(failure.getNamespace())) { - if (failure.hasChild("temporary-auth-failure")) { - throw new StateChangingException(Account.State.TEMPORARY_AUTH_FAILURE); - } else if (failure.hasChild("account-disabled")) { - final String text = failure.findChildContent("text"); - if ( Strings.isNullOrEmpty(text)) { + if (Namespace.TLS.equals(failure.getNamespace())) { + throw new StateChangingException(Account.State.TLS_ERROR); + } + final SaslMechanism.Version version; + try { + version = SaslMechanism.Version.of(failure); + } catch (final IllegalArgumentException e) { + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); + } + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": login failure " + version); + if (failure.hasChild("temporary-auth-failure")) { + throw new StateChangingException(Account.State.TEMPORARY_AUTH_FAILURE); + } else if (failure.hasChild("account-disabled")) { + final String text = failure.findChildContent("text"); + if (Strings.isNullOrEmpty(text)) { + throw new StateChangingException(Account.State.UNAUTHORIZED); + } + final Matcher matcher = Patterns.AUTOLINK_WEB_URL.matcher(text); + if (matcher.find()) { + final HttpUrl url; + try { + url = HttpUrl.get(text.substring(matcher.start(), matcher.end())); + } catch (final IllegalArgumentException e) { throw new StateChangingException(Account.State.UNAUTHORIZED); } - final Matcher matcher = Patterns.AUTOLINK_WEB_URL.matcher(text); - if (matcher.find()) { - final HttpUrl url; - try { - url = HttpUrl.get(text.substring(matcher.start(), matcher.end())); - } catch (final IllegalArgumentException e) { - throw new StateChangingException(Account.State.UNAUTHORIZED); - } - if (url.isHttps()) { - this.redirectionUrl = url; - throw new StateChangingException(Account.State.PAYMENT_REQUIRED); - } + if (url.isHttps()) { + this.redirectionUrl = url; + throw new StateChangingException(Account.State.PAYMENT_REQUIRED); } } - throw new StateChangingException(Account.State.UNAUTHORIZED); - } else if (Namespace.TLS.equals(failure.getNamespace())) { - throw new StateChangingException(Account.State.TLS_ERROR); - } else { - throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } + throw new StateChangingException(Account.State.UNAUTHORIZED); } else if (nextTag.isStart("challenge")) { - final String challenge = tagReader.readElement(nextTag).getContent(); - final Element response = new Element("response", Namespace.SASL); + final Element challenge = tagReader.readElement(nextTag); + final SaslMechanism.Version version; + try { + version = SaslMechanism.Version.of(challenge); + } catch (final IllegalArgumentException e) { + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); + } + final Element response; + if (version == SaslMechanism.Version.SASL) { + response = new Element("response", Namespace.SASL); + } else if (version == SaslMechanism.Version.SASL_2) { + response = new Element("response", Namespace.SASL_2); + } else { + throw new AssertionError("Missing implementation for " + version); + } try { - response.setContent(saslMechanism.getResponse(challenge)); + response.setContent(saslMechanism.getResponse(challenge.getContent())); } catch (final SaslMechanism.AuthenticationException e) { // TODO: Send auth abort tag. Log.e(Config.LOGTAG, e.toString()); + throw new StateChangingException(Account.State.UNAUTHORIZED); } tagWriter.writeElement(response); } else if (nextTag.isStart("enabled")) { @@ -848,7 +887,6 @@ public class XmppConnection implements Runnable { private void processStreamFeatures(final Tag currentTag) throws IOException { this.streamFeatures = tagReader.readElement(currentTag); - Log.d(Config.LOGTAG, this.streamFeatures.toString()); final boolean isSecure = features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS || account.isOnion(); final boolean needsBinding = !isBound && !account.isOptionSet(Account.OPTION_REGISTER); @@ -907,7 +945,6 @@ public class XmppConnection implements Runnable { private void authenticate(final SaslMechanism.Version version) throws IOException { final List mechanisms = extractMechanisms(streamFeatures.findChild("mechanisms")); - final Element auth = new Element("auth", Namespace.SASL); if (mechanisms.contains(External.MECHANISM) && account.getPrivateKeyAlias() != null) { saslMechanism = new External(tagWriter, account, mXmppConnectionService.getRNG()); } else if (mechanisms.contains(ScramSha512.MECHANISM)) { @@ -923,25 +960,38 @@ public class XmppConnection implements Runnable { } else if (mechanisms.contains(Anonymous.MECHANISM)) { saslMechanism = new Anonymous(tagWriter, account, mXmppConnectionService.getRNG()); } - if (saslMechanism != null) { - final int pinnedMechanism = account.getKeyAsInt(Account.PINNED_MECHANISM_KEY, -1); - if (pinnedMechanism > saslMechanism.getPriority()) { - Log.e(Config.LOGTAG, "Auth failed. Authentication mechanism " + saslMechanism.getMechanism() + - " has lower priority (" + saslMechanism.getPriority() + - ") than pinned priority (" + pinnedMechanism + - "). Possible downgrade attack?"); - throw new StateChangingException(Account.State.DOWNGRADE_ATTACK); + if (saslMechanism == null) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to find supported SASL mechanism in " + mechanisms); + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); + } + final int pinnedMechanism = account.getKeyAsInt(Account.PINNED_MECHANISM_KEY, -1); + if (pinnedMechanism > saslMechanism.getPriority()) { + Log.e(Config.LOGTAG, "Auth failed. Authentication mechanism " + saslMechanism.getMechanism() + + " has lower priority (" + saslMechanism.getPriority() + + ") than pinned priority (" + pinnedMechanism + + "). Possible downgrade attack?"); + throw new StateChangingException(Account.State.DOWNGRADE_ATTACK); + } + final String firstMessage = saslMechanism.getClientFirstMessage(); + final Element authenticate; + if (version == SaslMechanism.Version.SASL) { + authenticate = new Element("auth", Namespace.SASL); + if (!Strings.isNullOrEmpty(firstMessage)) { + authenticate.setContent(firstMessage); } - Log.d(Config.LOGTAG, account.getJid().toString() + ": Authenticating with " + saslMechanism.getMechanism()); - auth.setAttribute("mechanism", saslMechanism.getMechanism()); - if (!saslMechanism.getClientFirstMessage().isEmpty()) { - auth.setContent(saslMechanism.getClientFirstMessage()); + } else if (version == SaslMechanism.Version.SASL_2) { + authenticate = new Element("authenticate", Namespace.SASL_2); + if (!Strings.isNullOrEmpty(firstMessage)) { + authenticate.addChild("initial-response").setContent(firstMessage); } - tagWriter.writeElement(auth); + // TODO place to add extensions } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to find supported SASL mechanism in " + mechanisms); - throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); + throw new AssertionError("Missing implementation for " + version); } + + Log.d(Config.LOGTAG, account.getJid().toString() + ": Authenticating with "+version+ "/" + saslMechanism.getMechanism()); + authenticate.setAttribute("mechanism", saslMechanism.getMechanism()); + tagWriter.writeElement(authenticate); } private List extractMechanisms(final Element stream) { From 6202cbe26b086f9febdc5a9aa3515ccf643b2301 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 29 Aug 2022 18:40:49 +0200 Subject: [PATCH 010/101] minor code clean up for tag and element --- .../eu/siacs/conversations/xml/Element.java | 405 +++++++++--------- .../java/eu/siacs/conversations/xml/Tag.java | 185 ++++---- .../conversations/xmpp/XmppConnection.java | 14 +- 3 files changed, 302 insertions(+), 302 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xml/Element.java b/src/main/java/eu/siacs/conversations/xml/Element.java index 4d53a17b723f3b179bf8446d82829b7ebc0b3348..9c570df931f3723fee13aa0dca14f87a05a2fe57 100644 --- a/src/main/java/eu/siacs/conversations/xml/Element.java +++ b/src/main/java/eu/siacs/conversations/xml/Element.java @@ -12,207 +12,206 @@ import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; public class Element { - private final String name; - private Hashtable attributes = new Hashtable<>(); - private String content; - protected List children = new ArrayList<>(); - - public Element(String name) { - this.name = name; - } - - public Element(String name, String xmlns) { - this.name = name; - this.setAttribute("xmlns", xmlns); - } - - public Element addChild(Element child) { - this.content = null; - children.add(child); - return child; - } - - public Element addChild(String name) { - this.content = null; - Element child = new Element(name); - children.add(child); - return child; - } - - public Element addChild(String name, String xmlns) { - this.content = null; - Element child = new Element(name); - child.setAttribute("xmlns", xmlns); - children.add(child); - return child; - } - - public Element setContent(String content) { - this.content = content; - this.children.clear(); - return this; - } - - public Element findChild(String name) { - for (Element child : this.children) { - if (child.getName().equals(name)) { - return child; - } - } - return null; - } - - public String findChildContent(String name) { - Element element = findChild(name); - return element == null ? null : element.getContent(); - } - - public LocalizedContent findInternationalizedChildContentInDefaultNamespace(String name) { - return LocalizedContent.get(this, name); - } - - public Element findChild(String name, String xmlns) { - for (Element child : this.children) { - if (name.equals(child.getName()) && xmlns.equals(child.getAttribute("xmlns"))) { - return child; - } - } - return null; - } - - public Element findChildEnsureSingle(String name, String xmlns) { - final List results = new ArrayList<>(); - for (Element child : this.children) { - if (name.equals(child.getName()) && xmlns.equals(child.getAttribute("xmlns"))) { - results.add(child); - } - } - if (results.size() == 1) { - return results.get(0); - } - return null; - } - - public String findChildContent(String name, String xmlns) { - Element element = findChild(name,xmlns); - return element == null ? null : element.getContent(); - } - - public boolean hasChild(final String name) { - return findChild(name) != null; - } - - public boolean hasChild(final String name, final String xmlns) { - return findChild(name, xmlns) != null; - } - - public List getChildren() { - return this.children; - } - - public Element setChildren(List children) { - this.children = children; - return this; - } - - public final String getContent() { - return content; - } - - public Element setAttribute(String name, String value) { - if (name != null && value != null) { - this.attributes.put(name, value); - } - return this; - } - - public Element setAttribute(String name, Jid value) { - if (name != null && value != null) { - this.attributes.put(name, value.toEscapedString()); - } - return this; - } - - public Element removeAttribute(String name) { - this.attributes.remove(name); - return this; - } - - public Element setAttributes(Hashtable attributes) { - this.attributes = attributes; - return this; - } - - public String getAttribute(String name) { - if (this.attributes.containsKey(name)) { - return this.attributes.get(name); - } else { - return null; - } - } - - public Jid getAttributeAsJid(String name) { - final String jid = this.getAttribute(name); - if (jid != null && !jid.isEmpty()) { - try { - return Jid.ofEscaped(jid); - } catch (final IllegalArgumentException e) { - return InvalidJid.of(jid, this instanceof MessagePacket); - } - } - return null; - } - - public Hashtable getAttributes() { - return this.attributes; - } - - @NotNull - public String toString() { - final StringBuilder elementOutput = new StringBuilder(); - if ((content == null) && (children.size() == 0)) { - Tag emptyTag = Tag.empty(name); - emptyTag.setAtttributes(this.attributes); - elementOutput.append(emptyTag.toString()); - } else { - Tag startTag = Tag.start(name); - startTag.setAtttributes(this.attributes); - elementOutput.append(startTag); - if (content != null) { - elementOutput.append(XmlHelper.encodeEntities(content)); - } else { - for (Element child : children) { - elementOutput.append(child.toString()); - } - } - Tag endTag = Tag.end(name); - elementOutput.append(endTag); - } - return elementOutput.toString(); - } - - public final String getName() { - return name; - } - - public void clearChildren() { - this.children.clear(); - } - - public void setAttribute(String name, long value) { - this.setAttribute(name, Long.toString(value)); - } - - public void setAttribute(String name, int value) { - this.setAttribute(name, Integer.toString(value)); - } - - public boolean getAttributeAsBoolean(String name) { - String attr = getAttribute(name); - return (attr != null && (attr.equalsIgnoreCase("true") || attr.equalsIgnoreCase("1"))); - } - - public String getNamespace() { - return getAttribute("xmlns"); - } + private final String name; + private Hashtable attributes = new Hashtable<>(); + private String content; + protected List children = new ArrayList<>(); + + public Element(String name) { + this.name = name; + } + + public Element(String name, String xmlns) { + this.name = name; + this.setAttribute("xmlns", xmlns); + } + + public Element addChild(Element child) { + this.content = null; + children.add(child); + return child; + } + + public Element addChild(String name) { + this.content = null; + Element child = new Element(name); + children.add(child); + return child; + } + + public Element addChild(String name, String xmlns) { + this.content = null; + Element child = new Element(name); + child.setAttribute("xmlns", xmlns); + children.add(child); + return child; + } + + public Element setContent(String content) { + this.content = content; + this.children.clear(); + return this; + } + + public Element findChild(String name) { + for (Element child : this.children) { + if (child.getName().equals(name)) { + return child; + } + } + return null; + } + + public String findChildContent(String name) { + Element element = findChild(name); + return element == null ? null : element.getContent(); + } + + public LocalizedContent findInternationalizedChildContentInDefaultNamespace(String name) { + return LocalizedContent.get(this, name); + } + + public Element findChild(String name, String xmlns) { + for (Element child : this.children) { + if (name.equals(child.getName()) && xmlns.equals(child.getAttribute("xmlns"))) { + return child; + } + } + return null; + } + + public Element findChildEnsureSingle(String name, String xmlns) { + final List results = new ArrayList<>(); + for (Element child : this.children) { + if (name.equals(child.getName()) && xmlns.equals(child.getAttribute("xmlns"))) { + results.add(child); + } + } + if (results.size() == 1) { + return results.get(0); + } + return null; + } + + public String findChildContent(String name, String xmlns) { + Element element = findChild(name, xmlns); + return element == null ? null : element.getContent(); + } + + public boolean hasChild(final String name) { + return findChild(name) != null; + } + + public boolean hasChild(final String name, final String xmlns) { + return findChild(name, xmlns) != null; + } + + public List getChildren() { + return this.children; + } + + public Element setChildren(List children) { + this.children = children; + return this; + } + + public final String getContent() { + return content; + } + + public Element setAttribute(String name, String value) { + if (name != null && value != null) { + this.attributes.put(name, value); + } + return this; + } + + public Element setAttribute(String name, Jid value) { + if (name != null && value != null) { + this.attributes.put(name, value.toEscapedString()); + } + return this; + } + + public void removeAttribute(final String name) { + this.attributes.remove(name); + } + + public Element setAttributes(Hashtable attributes) { + this.attributes = attributes; + return this; + } + + public String getAttribute(String name) { + if (this.attributes.containsKey(name)) { + return this.attributes.get(name); + } else { + return null; + } + } + + public Jid getAttributeAsJid(String name) { + final String jid = this.getAttribute(name); + if (jid != null && !jid.isEmpty()) { + try { + return Jid.ofEscaped(jid); + } catch (final IllegalArgumentException e) { + return InvalidJid.of(jid, this instanceof MessagePacket); + } + } + return null; + } + + public Hashtable getAttributes() { + return this.attributes; + } + + @NotNull + public String toString() { + final StringBuilder elementOutput = new StringBuilder(); + if ((content == null) && (children.size() == 0)) { + final Tag emptyTag = Tag.empty(name); + emptyTag.setAttributes(this.attributes); + elementOutput.append(emptyTag); + } else { + final Tag startTag = Tag.start(name); + startTag.setAttributes(this.attributes); + elementOutput.append(startTag); + if (content != null) { + elementOutput.append(XmlHelper.encodeEntities(content)); + } else { + for (final Element child : children) { + elementOutput.append(child.toString()); + } + } + final Tag endTag = Tag.end(name); + elementOutput.append(endTag); + } + return elementOutput.toString(); + } + + public final String getName() { + return name; + } + + public void clearChildren() { + this.children.clear(); + } + + public void setAttribute(String name, long value) { + this.setAttribute(name, Long.toString(value)); + } + + public void setAttribute(String name, int value) { + this.setAttribute(name, Integer.toString(value)); + } + + public boolean getAttributeAsBoolean(String name) { + String attr = getAttribute(name); + return (attr != null && (attr.equalsIgnoreCase("true") || attr.equalsIgnoreCase("1"))); + } + + public String getNamespace() { + return getAttribute("xmlns"); + } } diff --git a/src/main/java/eu/siacs/conversations/xml/Tag.java b/src/main/java/eu/siacs/conversations/xml/Tag.java index b9ef979ffe63c7092481d7a02c06de59c1ccefa2..2e609852213eabd19487e7387a07678b6cf2cee7 100644 --- a/src/main/java/eu/siacs/conversations/xml/Tag.java +++ b/src/main/java/eu/siacs/conversations/xml/Tag.java @@ -1,104 +1,101 @@ package eu.siacs.conversations.xml; +import org.jetbrains.annotations.NotNull; + import java.util.Hashtable; -import java.util.Iterator; import java.util.Map.Entry; import java.util.Set; import eu.siacs.conversations.utils.XmlHelper; public class Tag { - public static final int NO = -1; - public static final int START = 0; - public static final int END = 1; - public static final int EMPTY = 2; - - protected int type; - protected String name; - protected Hashtable attributes = new Hashtable(); - - protected Tag(int type, String name) { - this.type = type; - this.name = name; - } - - public static Tag no(String text) { - return new Tag(NO, text); - } - - public static Tag start(String name) { - return new Tag(START, name); - } - - public static Tag end(String name) { - return new Tag(END, name); - } - - public static Tag empty(String name) { - return new Tag(EMPTY, name); - } - - public String getName() { - return name; - } - - public String getAttribute(String attrName) { - return this.attributes.get(attrName); - } - - public Tag setAttribute(String attrName, String attrValue) { - this.attributes.put(attrName, attrValue); - return this; - } - - public Tag setAtttributes(Hashtable attributes) { - this.attributes = attributes; - return this; - } - - public boolean isStart(String needle) { - if (needle == null) - return false; - return (this.type == START) && (needle.equals(this.name)); - } - - public boolean isEnd(String needle) { - if (needle == null) - return false; - return (this.type == END) && (needle.equals(this.name)); - } - - public boolean isNo() { - return (this.type == NO); - } - - public String toString() { - StringBuilder tagOutput = new StringBuilder(); - tagOutput.append('<'); - if (type == END) { - tagOutput.append('/'); - } - tagOutput.append(name); - if (type != END) { - Set> attributeSet = attributes.entrySet(); - Iterator> it = attributeSet.iterator(); - while (it.hasNext()) { - Entry entry = it.next(); - tagOutput.append(' '); - tagOutput.append(entry.getKey()); - tagOutput.append("=\""); - tagOutput.append(XmlHelper.encodeEntities(entry.getValue())); - tagOutput.append('"'); - } - } - if (type == EMPTY) { - tagOutput.append('/'); - } - tagOutput.append('>'); - return tagOutput.toString(); - } - - public Hashtable getAttributes() { - return this.attributes; - } + public static final int NO = -1; + public static final int START = 0; + public static final int END = 1; + public static final int EMPTY = 2; + + protected int type; + protected String name; + protected Hashtable attributes = new Hashtable(); + + protected Tag(int type, String name) { + this.type = type; + this.name = name; + } + + public static Tag no(String text) { + return new Tag(NO, text); + } + + public static Tag start(String name) { + return new Tag(START, name); + } + + public static Tag end(String name) { + return new Tag(END, name); + } + + public static Tag empty(String name) { + return new Tag(EMPTY, name); + } + + public String getName() { + return name; + } + + public String getAttribute(final String attrName) { + return this.attributes.get(attrName); + } + + public Tag setAttribute(final String attrName, final String attrValue) { + this.attributes.put(attrName, attrValue); + return this; + } + + public void setAttributes(final Hashtable attributes) { + this.attributes = attributes; + } + + public boolean isStart(String needle) { + if (needle == null) return false; + return (this.type == START) && (needle.equals(this.name)); + } + + public boolean isEnd(String needle) { + if (needle == null) return false; + return (this.type == END) && (needle.equals(this.name)); + } + + public boolean isNo() { + return (this.type == NO); + } + + @NotNull + public String toString() { + final StringBuilder tagOutput = new StringBuilder(); + tagOutput.append('<'); + if (type == END) { + tagOutput.append('/'); + } + tagOutput.append(name); + if (type != END) { + final Set> attributeSet = attributes.entrySet(); + for (final Entry entry : attributeSet) { + tagOutput.append(' '); + tagOutput.append(entry.getKey()); + tagOutput.append("=\""); + tagOutput.append(XmlHelper.encodeEntities(entry.getValue())); + tagOutput.append('"'); + } + } + if (type == EMPTY) { + tagOutput.append('/'); + } + tagOutput.append('>'); + return tagOutput.toString(); + } + + public Hashtable getAttributes() { + return this.attributes; + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index bc77246e8d512601b81e94f86267c89c916df28c..bd11fcbe223f137c5b032cee064d1db91b9ad336 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -498,6 +498,10 @@ public class XmppConnection implements Runnable { + ")"); account.setKey( Account.PINNED_MECHANISM_KEY, String.valueOf(saslMechanism.getPriority())); + if (version == SaslMechanism.Version.SASL_2) { + final String authorizationIdentifier = success.findChildContent("authorization-identifier"); + Log.d(Config.LOGTAG,account.getJid().asBareJid()+": SASL 2.0 authorization identifier was "+authorizationIdentifier); + } if (version == SaslMechanism.Version.SASL) { tagReader.reset(); sendStartStream(); @@ -1179,7 +1183,7 @@ public class XmppConnection implements Runnable { Log.d(Config.LOGTAG, account.getJid() + ": disconnecting because of bind failure. (no jid)"); } } else { - Log.d(Config.LOGTAG, account.getJid() + ": disconnecting because of bind failure (" + packet.toString()); + Log.d(Config.LOGTAG, account.getJid() + ": disconnecting because of bind failure (" + packet); } final Element error = packet.findChild("error"); if (packet.getType() == IqPacket.TYPE.ERROR && error != null && error.hasChild("conflict")) { @@ -1449,7 +1453,7 @@ public class XmppConnection implements Runnable { features.carbonsEnabled = true; } else { Log.d(Config.LOGTAG, account.getJid().asBareJid() - + ": error enableing carbons " + packet.toString()); + + ": could not enable carbons " + packet); } }); } @@ -1472,7 +1476,7 @@ public class XmppConnection implements Runnable { failPendingMessages(text); throw new StateChangingException(Account.State.POLICY_VIOLATION); } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": stream error " + streamError.toString()); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": stream error " + streamError); throw new StateChangingException(Account.State.STREAM_ERROR); } } @@ -1839,8 +1843,8 @@ public class XmppConnection implements Runnable { Log.d(Config.LOGTAG, "getting certificate chain"); try { return KeyChain.getCertificateChain(mXmppConnectionService, alias); - } catch (Exception e) { - Log.d(Config.LOGTAG, e.getMessage()); + } catch (final Exception e) { + Log.d(Config.LOGTAG, "could not get certificate chain", e); return new X509Certificate[0]; } } From 928a16d31d215a4eda2ba4e03d84e8651301a2e3 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 29 Aug 2022 18:53:34 +0200 Subject: [PATCH 011/101] abort on 'continue' - no client support --- .../siacs/conversations/entities/Account.java | 3 + .../java/eu/siacs/conversations/xml/Tag.java | 10 ++- .../conversations/xmpp/XmppConnection.java | 84 ++++++++++++++----- src/main/res/values/strings.xml | 1 + 4 files changed, 75 insertions(+), 23 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index dc354adc4a90e19639fe72a5fbbe069bf6a3967c..9220cc192fd054e53e3ea908692182d60c6c3e41 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -640,6 +640,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable TLS_ERROR, TLS_ERROR_DOMAIN, INCOMPATIBLE_SERVER, + INCOMPATIBLE_CLIENT, TOR_NOT_AVAILABLE, DOWNGRADE_ATTACK, SESSION_FAILURE, @@ -709,6 +710,8 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable return R.string.account_status_tls_error_domain; case INCOMPATIBLE_SERVER: return R.string.account_status_incompatible_server; + case INCOMPATIBLE_CLIENT: + return R.string.account_status_incompatible_client; case TOR_NOT_AVAILABLE: return R.string.account_status_tor_unavailable; case BIND_FAILURE: diff --git a/src/main/java/eu/siacs/conversations/xml/Tag.java b/src/main/java/eu/siacs/conversations/xml/Tag.java index 2e609852213eabd19487e7387a07678b6cf2cee7..db2b111727227823eb46a0e662598844fdffc688 100644 --- a/src/main/java/eu/siacs/conversations/xml/Tag.java +++ b/src/main/java/eu/siacs/conversations/xml/Tag.java @@ -56,11 +56,17 @@ public class Tag { this.attributes = attributes; } - public boolean isStart(String needle) { - if (needle == null) return false; + public boolean isStart(final String needle) { + if (needle == null) { + return false; + } return (this.type == START) && (needle.equals(this.name)); } + public boolean isStart(final String name, final String namespace) { + return isStart(name) && namespace != null && namespace.equals(this.getAttribute("xmlns")); + } + public boolean isEnd(String needle) { if (needle == null) return false; return (this.type == END) && (needle.equals(this.name)); diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index bd11fcbe223f137c5b032cee064d1db91b9ad336..0fbb85768ba1f66b07f9de595c48b2beb380ed81 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -466,7 +466,7 @@ public class XmppConnection implements Runnable { processStreamError(nextTag); } else if (nextTag.isStart("features")) { processStreamFeatures(nextTag); - } else if (nextTag.isStart("proceed")) { + } else if (nextTag.isStart("proceed", Namespace.TLS)) { switchOverToTls(); } else if (nextTag.isStart("success")) { final Element success = tagReader.readElement(nextTag); @@ -499,8 +499,13 @@ public class XmppConnection implements Runnable { account.setKey( Account.PINNED_MECHANISM_KEY, String.valueOf(saslMechanism.getPriority())); if (version == SaslMechanism.Version.SASL_2) { - final String authorizationIdentifier = success.findChildContent("authorization-identifier"); - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": SASL 2.0 authorization identifier was "+authorizationIdentifier); + final String authorizationIdentifier = + success.findChildContent("authorization-identifier"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": SASL 2.0 authorization identifier was " + + authorizationIdentifier); } if (version == SaslMechanism.Version.SASL) { tagReader.reset(); @@ -513,11 +518,10 @@ public class XmppConnection implements Runnable { } break; } + } else if (nextTag.isStart("failure", Namespace.TLS)) { + throw new StateChangingException(Account.State.TLS_ERROR); } else if (nextTag.isStart("failure")) { final Element failure = tagReader.readElement(nextTag); - if (Namespace.TLS.equals(failure.getNamespace())) { - throw new StateChangingException(Account.State.TLS_ERROR); - } final SaslMechanism.Version version; try { version = SaslMechanism.Version.of(failure); @@ -547,6 +551,8 @@ public class XmppConnection implements Runnable { } } throw new StateChangingException(Account.State.UNAUTHORIZED); + } else if (nextTag.isStart("continue", Namespace.SASL_2)) { + throw new StateChangingException(Account.State.INCOMPATIBLE_CLIENT); } else if (nextTag.isStart("challenge")) { final Element challenge = tagReader.readElement(nextTag); final SaslMechanism.Version version; @@ -575,12 +581,19 @@ public class XmppConnection implements Runnable { final Element enabled = tagReader.readElement(nextTag); if ("true".equals(enabled.getAttribute("resume"))) { this.streamId = enabled.getAttribute("id"); - Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() - + ": stream management(" + smVersion - + ") enabled (resumable)"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid().toString() + + ": stream management(" + + smVersion + + ") enabled (resumable)"); } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() - + ": stream management(" + smVersion + ") enabled"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid().toString() + + ": stream management(" + + smVersion + + ") enabled"); } this.stanzasReceived = 0; this.inSmacksSession = true; @@ -599,11 +612,15 @@ public class XmppConnection implements Runnable { synchronized (this.mStanzaQueue) { final int serverCount = Integer.parseInt(h); if (serverCount < stanzasSent) { - Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() - + ": session resumed with lost packages"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid().toString() + + ": session resumed with lost packages"); stanzasSent = serverCount; } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": session resumed"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid().toString() + ": session resumed"); } acknowledgedMessages = acknowledgeStanzaUpTo(serverCount); for (int i = 0; i < this.mStanzaQueue.size(); ++i) { @@ -618,7 +635,8 @@ public class XmppConnection implements Runnable { for (AbstractAcknowledgeableStanza packet : failedStanzas) { if (packet instanceof MessagePacket) { MessagePacket message = (MessagePacket) packet; - mXmppConnectionService.markMessage(account, + mXmppConnectionService.markMessage( + account, message.getTo().asBareJid(), message.getId(), Message.STATUS_UNSEND); @@ -627,12 +645,20 @@ public class XmppConnection implements Runnable { } } catch (final NumberFormatException ignored) { } - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": online with resource " + account.getResource()); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": online with resource " + + account.getResource()); changeStatus(Account.State.ONLINE); } else if (nextTag.isStart("r")) { tagReader.readElement(nextTag); if (Config.EXTENDED_SM_LOGGING) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": acknowledging stanza #" + this.stanzasReceived); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": acknowledging stanza #" + + this.stanzasReceived); } final AckPacket ack = new AckPacket(this.stanzasReceived, smVersion); tagWriter.writeStanzaAsync(ack); @@ -642,10 +668,19 @@ public class XmppConnection implements Runnable { if (mWaitingForSmCatchup.compareAndSet(true, false)) { final int messageCount = mSmCatchupMessageCounter.get(); final int pendingIQs = packetCallbacks.size(); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": SM catchup complete (messages=" + messageCount + ", pending IQs=" + pendingIQs + ")"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": SM catchup complete (messages=" + + messageCount + + ", pending IQs=" + + pendingIQs + + ")"); accountUiNeedsRefresh = true; if (messageCount > 0) { - mXmppConnectionService.getNotificationService().finishBacklog(true, account); + mXmppConnectionService + .getNotificationService() + .finishBacklog(true, account); } } } @@ -664,13 +699,20 @@ public class XmppConnection implements Runnable { mXmppConnectionService.updateConversationUi(); } } catch (NumberFormatException | NullPointerException e) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server send ack without sequence number"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": server send ack without sequence number"); } } else if (nextTag.isStart("failed")) { Element failed = tagReader.readElement(nextTag); try { final int serverCount = Integer.parseInt(failed.getAttribute("h")); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": resumption failed but server acknowledged stanza #" + serverCount); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": resumption failed but server acknowledged stanza #" + + serverCount); final boolean acknowledgedMessages; synchronized (this.mStanzaQueue) { acknowledgedMessages = acknowledgeStanzaUpTo(serverCount); diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index c51be907e353bda294d8db6ddd797ab84a3f39ea..cd4412588e1444dd20e7bec99c457a889cb4e4d4 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -168,6 +168,7 @@ Domain not verifiable Policy violation Incompatible server + Incompatible client Stream error Stream opening error Unencrypted From f6ab3dd068771394a48e11f25a09e30663adcfe3 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 29 Aug 2022 19:22:25 +0200 Subject: [PATCH 012/101] support resume via sasl 2.0 --- .../conversations/xmpp/XmppConnection.java | 125 ++++++++++-------- 1 file changed, 73 insertions(+), 52 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 0fbb85768ba1f66b07f9de595c48b2beb380ed81..bddcb197a50be4e69f2e1adde4327ca80971ca67 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -506,6 +506,10 @@ public class XmppConnection implements Runnable { account.getJid().asBareJid() + ": SASL 2.0 authorization identifier was " + authorizationIdentifier); + final Element resumed = success.findChild("resumed", "urn:xmpp:sm:3"); + if (resumed != null && streamId != null) { + processResumed(resumed); + } } if (version == SaslMechanism.Version.SASL) { tagReader.reset(); @@ -579,7 +583,7 @@ public class XmppConnection implements Runnable { tagWriter.writeElement(response); } else if (nextTag.isStart("enabled")) { final Element enabled = tagReader.readElement(nextTag); - if ("true".equals(enabled.getAttribute("resume"))) { + if (enabled.getAttributeAsBoolean("resume")) { this.streamId = enabled.getAttribute("id"); Log.d( Config.LOGTAG, @@ -600,57 +604,8 @@ public class XmppConnection implements Runnable { final RequestPacket r = new RequestPacket(smVersion); tagWriter.writeStanzaAsync(r); } else if (nextTag.isStart("resumed")) { - this.inSmacksSession = true; - this.isBound = true; - this.tagWriter.writeStanzaAsync(new RequestPacket(smVersion)); - lastPacketReceived = SystemClock.elapsedRealtime(); final Element resumed = tagReader.readElement(nextTag); - final String h = resumed.getAttribute("h"); - try { - ArrayList failedStanzas = new ArrayList<>(); - final boolean acknowledgedMessages; - synchronized (this.mStanzaQueue) { - final int serverCount = Integer.parseInt(h); - if (serverCount < stanzasSent) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid().toString() - + ": session resumed with lost packages"); - stanzasSent = serverCount; - } else { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid().toString() + ": session resumed"); - } - acknowledgedMessages = acknowledgeStanzaUpTo(serverCount); - for (int i = 0; i < this.mStanzaQueue.size(); ++i) { - failedStanzas.add(mStanzaQueue.valueAt(i)); - } - mStanzaQueue.clear(); - } - if (acknowledgedMessages) { - mXmppConnectionService.updateConversationUi(); - } - Log.d(Config.LOGTAG, "resending " + failedStanzas.size() + " stanzas"); - for (AbstractAcknowledgeableStanza packet : failedStanzas) { - if (packet instanceof MessagePacket) { - MessagePacket message = (MessagePacket) packet; - mXmppConnectionService.markMessage( - account, - message.getTo().asBareJid(), - message.getId(), - Message.STATUS_UNSEND); - } - sendPacket(packet); - } - } catch (final NumberFormatException ignored) { - } - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": online with resource " - + account.getResource()); - changeStatus(Account.State.ONLINE); + processResumed(resumed); } else if (nextTag.isStart("r")) { tagReader.readElement(nextTag); if (Config.EXTENDED_SM_LOGGING) { @@ -739,6 +694,59 @@ public class XmppConnection implements Runnable { } } + private void processResumed(final Element resumed) { + this.inSmacksSession = true; + this.isBound = true; + this.tagWriter.writeStanzaAsync(new RequestPacket(smVersion)); + lastPacketReceived = SystemClock.elapsedRealtime(); + final String h = resumed.getAttribute("h"); + try { + ArrayList failedStanzas = new ArrayList<>(); + final boolean acknowledgedMessages; + synchronized (this.mStanzaQueue) { + final int serverCount = Integer.parseInt(h); + if (serverCount < stanzasSent) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": session resumed with lost packages"); + stanzasSent = serverCount; + } else { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": session resumed"); + } + acknowledgedMessages = acknowledgeStanzaUpTo(serverCount); + for (int i = 0; i < this.mStanzaQueue.size(); ++i) { + failedStanzas.add(mStanzaQueue.valueAt(i)); + } + mStanzaQueue.clear(); + } + if (acknowledgedMessages) { + mXmppConnectionService.updateConversationUi(); + } + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": resending " + + failedStanzas.size() + + " stanzas"); + for (AbstractAcknowledgeableStanza packet : failedStanzas) { + if (packet instanceof MessagePacket) { + MessagePacket message = (MessagePacket) packet; + mXmppConnectionService.markMessage( + account, + message.getTo().asBareJid(), + message.getId(), + Message.STATUS_UNSEND); + } + sendPacket(packet); + } + } catch (final NumberFormatException ignored) { + } + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": online with resource " + account.getResource()); + changeStatus(Account.State.ONLINE); + } + private boolean acknowledgeStanzaUpTo(int serverCount) { if (serverCount > stanzasSent) { Log.e(Config.LOGTAG, "server acknowledged more stanzas than we sent. serverCount=" + serverCount + ", ourCount=" + stanzasSent); @@ -986,6 +994,12 @@ public class XmppConnection implements Runnable { + XmlHelper.printElementNames(this.streamFeatures)); throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } + } else { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": received NOP stream features" + + this.streamFeatures); } } @@ -1030,7 +1044,14 @@ public class XmppConnection implements Runnable { if (!Strings.isNullOrEmpty(firstMessage)) { authenticate.addChild("initial-response").setContent(firstMessage); } - // TODO place to add extensions + final Element inline = this.streamFeatures.findChild("inline", Namespace.SASL_2); + final boolean inlineStreamManagement = inline != null && inline.hasChild("sm", "urn:xmpp:sm:3"); + if (inlineStreamManagement && streamId != null) { + final ResumePacket resume = new ResumePacket(this.streamId, stanzasReceived, smVersion); + this.mSmCatchupMessageCounter.set(0); + this.mWaitingForSmCatchup.set(true); + authenticate.addChild(resume); + } } else { throw new AssertionError("Missing implementation for " + version); } From 7ea4f64ce4b157989266840411bf9aa703d57f0e Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 29 Aug 2022 19:30:03 +0200 Subject: [PATCH 013/101] code clean up for resumed processing --- .../conversations/xmpp/XmppConnection.java | 80 ++++++++++--------- 1 file changed, 42 insertions(+), 38 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index bddcb197a50be4e69f2e1adde4327ca80971ca67..37d02884c1839b212e6147a4348c48c57d88736d 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -694,52 +694,56 @@ public class XmppConnection implements Runnable { } } - private void processResumed(final Element resumed) { + private void processResumed(final Element resumed) throws StateChangingException { this.inSmacksSession = true; this.isBound = true; this.tagWriter.writeStanzaAsync(new RequestPacket(smVersion)); lastPacketReceived = SystemClock.elapsedRealtime(); final String h = resumed.getAttribute("h"); + if (h == null) { + resetStreamId(); + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); + } + final int serverCount; try { - ArrayList failedStanzas = new ArrayList<>(); - final boolean acknowledgedMessages; - synchronized (this.mStanzaQueue) { - final int serverCount = Integer.parseInt(h); - if (serverCount < stanzasSent) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() + ": session resumed with lost packages"); - stanzasSent = serverCount; - } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": session resumed"); - } - acknowledgedMessages = acknowledgeStanzaUpTo(serverCount); - for (int i = 0; i < this.mStanzaQueue.size(); ++i) { - failedStanzas.add(mStanzaQueue.valueAt(i)); - } - mStanzaQueue.clear(); + serverCount = Integer.parseInt(h); + } catch (final NumberFormatException e) { + resetStreamId(); + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); + } + final ArrayList failedStanzas = new ArrayList<>(); + final boolean acknowledgedMessages; + synchronized (this.mStanzaQueue) { + if (serverCount < stanzasSent) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": session resumed with lost packages"); + stanzasSent = serverCount; + } else { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": session resumed"); } - if (acknowledgedMessages) { - mXmppConnectionService.updateConversationUi(); + acknowledgedMessages = acknowledgeStanzaUpTo(serverCount); + for (int i = 0; i < this.mStanzaQueue.size(); ++i) { + failedStanzas.add(mStanzaQueue.valueAt(i)); } - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": resending " - + failedStanzas.size() - + " stanzas"); - for (AbstractAcknowledgeableStanza packet : failedStanzas) { - if (packet instanceof MessagePacket) { - MessagePacket message = (MessagePacket) packet; - mXmppConnectionService.markMessage( - account, - message.getTo().asBareJid(), - message.getId(), - Message.STATUS_UNSEND); - } - sendPacket(packet); + mStanzaQueue.clear(); + } + if (acknowledgedMessages) { + mXmppConnectionService.updateConversationUi(); + } + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": resending " + failedStanzas.size() + " stanzas"); + for (final AbstractAcknowledgeableStanza packet : failedStanzas) { + if (packet instanceof MessagePacket) { + MessagePacket message = (MessagePacket) packet; + mXmppConnectionService.markMessage( + account, + message.getTo().asBareJid(), + message.getId(), + Message.STATUS_UNSEND); } - } catch (final NumberFormatException ignored) { + sendPacket(packet); } Log.d( Config.LOGTAG, @@ -998,7 +1002,7 @@ public class XmppConnection implements Runnable { Log.d( Config.LOGTAG, account.getJid().asBareJid() - + ": received NOP stream features" + + ": received NOP stream features " + this.streamFeatures); } } From 8f76084a439f6d03a417013f9fc25afbb145275d Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 29 Aug 2022 19:44:39 +0200 Subject: [PATCH 014/101] add sm-failed processing --- .../conversations/xmpp/XmppConnection.java | 55 ++++++++++++------- 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 37d02884c1839b212e6147a4348c48c57d88736d..cf18316edf072bb218f6bc80dd9a55a3781220b2 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -507,8 +507,11 @@ public class XmppConnection implements Runnable { + ": SASL 2.0 authorization identifier was " + authorizationIdentifier); final Element resumed = success.findChild("resumed", "urn:xmpp:sm:3"); + final Element failed = success.findChild("failed", "urn:xmpp:sm:3"); if (resumed != null && streamId != null) { processResumed(resumed); + } else if (failed != null) { + processFailed(failed, false); // wait for new stream features } } if (version == SaslMechanism.Version.SASL) { @@ -660,26 +663,8 @@ public class XmppConnection implements Runnable { + ": server send ack without sequence number"); } } else if (nextTag.isStart("failed")) { - Element failed = tagReader.readElement(nextTag); - try { - final int serverCount = Integer.parseInt(failed.getAttribute("h")); - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": resumption failed but server acknowledged stanza #" - + serverCount); - final boolean acknowledgedMessages; - synchronized (this.mStanzaQueue) { - acknowledgedMessages = acknowledgeStanzaUpTo(serverCount); - } - if (acknowledgedMessages) { - mXmppConnectionService.updateConversationUi(); - } - } catch (NumberFormatException | NullPointerException e) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": resumption failed"); - } - resetStreamId(); - sendBindRequest(); + final Element failed = tagReader.readElement(nextTag); + processFailed(failed, true); } else if (nextTag.isStart("iq")) { processIq(nextTag); } else if (nextTag.isStart("message")) { @@ -751,6 +736,36 @@ public class XmppConnection implements Runnable { changeStatus(Account.State.ONLINE); } + private void processFailed(final Element failed, final boolean sendBindRequest) { + final int serverCount; + try { + serverCount = Integer.parseInt(failed.getAttribute("h")); + } catch (final NumberFormatException | NullPointerException e) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": resumption failed"); + resetStreamId(); + if (sendBindRequest) { + sendBindRequest(); + } + return; + } + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": resumption failed but server acknowledged stanza #" + + serverCount); + final boolean acknowledgedMessages; + synchronized (this.mStanzaQueue) { + acknowledgedMessages = acknowledgeStanzaUpTo(serverCount); + } + if (acknowledgedMessages) { + mXmppConnectionService.updateConversationUi(); + } + resetStreamId(); + if (sendBindRequest) { + sendBindRequest(); + } + } + private boolean acknowledgeStanzaUpTo(int serverCount) { if (serverCount > stanzasSent) { Log.e(Config.LOGTAG, "server acknowledged more stanzas than we sent. serverCount=" + serverCount + ", ourCount=" + stanzasSent); From 3fac7d4992a4ebff8f3cabddcbf40920a331e62c Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 30 Aug 2022 08:21:32 +0200 Subject: [PATCH 015/101] fix very rare NPE (race condition) --- .../ui/PublishProfilePictureActivity.java | 31 ++++++++++++------- .../conversations/xmpp/XmppConnection.java | 2 +- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java index 0e14fcc8fe86fa5b2513f5c300d3a425a88a3c4a..16607b81ea4af5b93bb2b907d0e1713ac4052330 100644 --- a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java @@ -102,18 +102,25 @@ public class PublishProfilePictureActivity extends XmppActivity implements XmppC xmppConnectionService.publishAvatar(account, avatarUri, this); } }); - this.cancelButton.setOnClickListener(v -> { - if (mInitialAccountSetup) { - final Intent intent = new Intent(getApplicationContext(), StartConversationActivity.class); - if (xmppConnectionService != null && xmppConnectionService.getAccounts().size() == 1) { - intent.putExtra("init", true); - } - StartConversationActivity.addInviteUri(intent, getIntent()); - intent.putExtra(EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString()); - startActivity(intent); - } - finish(); - }); + this.cancelButton.setOnClickListener( + v -> { + if (mInitialAccountSetup) { + final Intent intent = + new Intent( + getApplicationContext(), StartConversationActivity.class); + if (xmppConnectionService != null + && xmppConnectionService.getAccounts().size() == 1) { + intent.putExtra("init", true); + } + StartConversationActivity.addInviteUri(intent, getIntent()); + if (account != null) { + intent.putExtra( + EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString()); + } + startActivity(intent); + } + finish(); + }); this.avatar.setOnClickListener(v -> chooseAvatar(this)); this.defaultUri = PhoneHelper.getProfilePictureUri(getApplicationContext()); if (savedInstanceState != null) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index cf18316edf072bb218f6bc80dd9a55a3781220b2..c49cb989cd0aba34a835bffa67dac778f7ad4c32 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -1080,7 +1080,7 @@ public class XmppConnection implements Runnable { tagWriter.writeElement(authenticate); } - private List extractMechanisms(final Element stream) { + private static List extractMechanisms(final Element stream) { final ArrayList mechanisms = new ArrayList<>(stream .getChildren().size()); for (final Element child : stream.getChildren()) { From 4f92ba880bcd6fd94798b2370abd42e341a33181 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 30 Aug 2022 09:31:06 +0200 Subject: [PATCH 016/101] process authorization id in case full jid changes --- .../java/eu/siacs/conversations/xmpp/Jid.java | 3 +- .../conversations/xmpp/XmppConnection.java | 133 +++++++++++------- 2 files changed, 81 insertions(+), 55 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/Jid.java b/src/main/java/eu/siacs/conversations/xmpp/Jid.java index 622a132fbab1a0fc968276b4a12b4751b96fd048..299c872b3c665bdcf20aa06fc5be1107a7a46999 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/Jid.java +++ b/src/main/java/eu/siacs/conversations/xmpp/Jid.java @@ -118,8 +118,7 @@ public interface Jid extends Comparable, Serializable, CharSequence { static Jid ofEscaped(CharSequence jid) { try { return new WrappedJid(JidCreate.from(jid)); - } catch (XmppStringprepException e) { - e.printStackTrace(); + } catch (final XmppStringprepException e) { throw new IllegalArgumentException(e); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index c49cb989cd0aba34a835bffa67dac778f7ad4c32..48a9f02815e7ec158f00ff0a984d455f1b2d825f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -470,61 +470,10 @@ public class XmppConnection implements Runnable { switchOverToTls(); } else if (nextTag.isStart("success")) { final Element success = tagReader.readElement(nextTag); - final SaslMechanism.Version version; - try { - version = SaslMechanism.Version.of(success); - } catch (final IllegalArgumentException e) { - throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); - } - final String challenge; - if (version == SaslMechanism.Version.SASL) { - challenge = success.getContent(); - } else if (version == SaslMechanism.Version.SASL_2) { - challenge = success.findChildContent("additional-data"); - } else { - throw new AssertionError("Missing implementation for " + version); - } - try { - saslMechanism.getResponse(challenge); - } catch (final SaslMechanism.AuthenticationException e) { - Log.e(Config.LOGTAG, String.valueOf(e)); - throw new StateChangingException(Account.State.UNAUTHORIZED); - } - Log.d( - Config.LOGTAG, - account.getJid().asBareJid().toString() - + ": logged in (using " - + version - + ")"); - account.setKey( - Account.PINNED_MECHANISM_KEY, String.valueOf(saslMechanism.getPriority())); - if (version == SaslMechanism.Version.SASL_2) { - final String authorizationIdentifier = - success.findChildContent("authorization-identifier"); - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": SASL 2.0 authorization identifier was " - + authorizationIdentifier); - final Element resumed = success.findChild("resumed", "urn:xmpp:sm:3"); - final Element failed = success.findChild("failed", "urn:xmpp:sm:3"); - if (resumed != null && streamId != null) { - processResumed(resumed); - } else if (failed != null) { - processFailed(failed, false); // wait for new stream features - } - } - if (version == SaslMechanism.Version.SASL) { - tagReader.reset(); - sendStartStream(); - final Tag tag = tagReader.readTag(); - if (tag != null && tag.isStart("stream")) { - processStream(); - } else { - throw new StateChangingException(Account.State.STREAM_OPENING_ERROR); - } + if (processSuccess(success)) { break; } + } else if (nextTag.isStart("failure", Namespace.TLS)) { throw new StateChangingException(Account.State.TLS_ERROR); } else if (nextTag.isStart("failure")) { @@ -679,6 +628,84 @@ public class XmppConnection implements Runnable { } } + private boolean processSuccess(final Element success) throws IOException, XmlPullParserException { + final SaslMechanism.Version version; + try { + version = SaslMechanism.Version.of(success); + } catch (final IllegalArgumentException e) { + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); + } + final String challenge; + if (version == SaslMechanism.Version.SASL) { + challenge = success.getContent(); + } else if (version == SaslMechanism.Version.SASL_2) { + challenge = success.findChildContent("additional-data"); + } else { + throw new AssertionError("Missing implementation for " + version); + } + try { + saslMechanism.getResponse(challenge); + } catch (final SaslMechanism.AuthenticationException e) { + Log.e(Config.LOGTAG, String.valueOf(e)); + throw new StateChangingException(Account.State.UNAUTHORIZED); + } + Log.d( + Config.LOGTAG, + account.getJid().asBareJid().toString() + + ": logged in (using " + + version + + ")"); + account.setKey( + Account.PINNED_MECHANISM_KEY, String.valueOf(saslMechanism.getPriority())); + if (version == SaslMechanism.Version.SASL_2) { + final String authorizationIdentifier = + success.findChildContent("authorization-identifier"); + final Jid authorizationJid; + try { + authorizationJid = Strings.isNullOrEmpty(authorizationIdentifier) ? null : Jid.ofEscaped(authorizationIdentifier); + } catch (final IllegalArgumentException e) { + Log.d(Config.LOGTAG,account.getJid().asBareJid()+": SASL 2.0 authorization identifier was not a valid jid"); + throw new StateChangingException(Account.State.BIND_FAILURE); + } + if (authorizationJid == null) { + throw new StateChangingException(Account.State.BIND_FAILURE); + } + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": SASL 2.0 authorization identifier was " + + authorizationJid); + if (!account.getJid().getDomain().equals(authorizationJid.getDomain())) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server tried to re-assign domain to " + authorizationJid.getDomain()); + throw new StateChangingError(Account.State.BIND_FAILURE); + } + if (authorizationJid.isFullJid() && account.setJid(authorizationJid)) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": jid changed during SASL 2.0. updating database"); + mXmppConnectionService.databaseBackend.updateAccount(account); + } + final Element resumed = success.findChild("resumed", "urn:xmpp:sm:3"); + final Element failed = success.findChild("failed", "urn:xmpp:sm:3"); + if (resumed != null && streamId != null) { + processResumed(resumed); + } else if (failed != null) { + processFailed(failed, false); // wait for new stream features + } + } + if (version == SaslMechanism.Version.SASL) { + tagReader.reset(); + sendStartStream(); + final Tag tag = tagReader.readTag(); + if (tag != null && tag.isStart("stream")) { + processStream(); + return true; + } else { + throw new StateChangingException(Account.State.STREAM_OPENING_ERROR); + } + } else { + return false; + } + } + private void processResumed(final Element resumed) throws StateChangingException { this.inSmacksSession = true; this.isBound = true; From cb1d7c69a19aafb5ea9d0e02b8bc0949dfb592fb Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 3 Sep 2022 11:05:27 +0200 Subject: [PATCH 017/101] remove comment --- .../eu/siacs/conversations/services/XmppConnectionService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 517a63a6a93cef4326d10d9c9b5d1d1f87c2e050..af68db19ca1589ef8749b660e9f795cc22c2e345 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -4502,7 +4502,6 @@ public class XmppConnectionService extends Service { for (Account account : getAccounts()) { if (account.isOnlineAndConnected() && mPushManagementService.available(account)) { mPushManagementService.registerPushTokenOnServer(account); - //TODO renew mucs } } } From 00dd9a8058d0cff4964ebc5919ccb670191f509c Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 3 Sep 2022 12:16:06 +0200 Subject: [PATCH 018/101] remove support for sm:2 --- .../eu/siacs/conversations/xml/Namespace.java | 3 + .../conversations/xmpp/XmppConnection.java | 1119 +++++++++++------ .../xmpp/stanzas/csi/ActivePacket.java | 3 +- .../xmpp/stanzas/csi/InactivePacket.java | 3 +- .../xmpp/stanzas/streammgmt/AckPacket.java | 5 +- .../xmpp/stanzas/streammgmt/EnablePacket.java | 5 +- .../stanzas/streammgmt/RequestPacket.java | 5 +- .../xmpp/stanzas/streammgmt/ResumePacket.java | 5 +- 8 files changed, 741 insertions(+), 407 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index c2a7af60779d47feadab960b6103aa998cd8e9ed..e28e69add7008fefb453bfcb7e3de486f7edcc52 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -25,6 +25,9 @@ public final class Namespace { public static final String NICK = "http://jabber.org/protocol/nick"; public static final String FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL = "http://jabber.org/protocol/offline"; public static final String BIND = "urn:ietf:params:xml:ns:xmpp-bind"; + public static final String BIND2 = "urn:xmpp:bind2:0"; + public static final String STREAM_MANAGEMENT = "urn:xmpp:sm:3"; + public static final String CSI = "urn:xmpp:csi:0"; public static final String BOOKMARKS_CONVERSION = "urn:xmpp:bookmarks-conversion:0"; public static final String BOOKMARKS = "storage:bookmarks"; public static final String SYNCHRONIZATION = "im.quicksy.synchronization:0"; diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 48a9f02815e7ec158f00ff0a984d455f1b2d825f..6efbfbf152fdd121da5e617aa6c142766a71c28f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -109,38 +109,45 @@ public class XmppConnection implements Runnable { private static final int PACKET_IQ = 0; private static final int PACKET_MESSAGE = 1; private static final int PACKET_PRESENCE = 2; - public final OnIqPacketReceived registrationResponseListener = (account, packet) -> { - if (packet.getType() == IqPacket.TYPE.RESULT) { - account.setOption(Account.OPTION_REGISTER, false); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": successfully registered new account on server"); - throw new StateChangingError(Account.State.REGISTRATION_SUCCESSFUL); - } else { - final List PASSWORD_TOO_WEAK_MSGS = Arrays.asList( - "The password is too weak", - "Please use a longer password."); - Element error = packet.findChild("error"); - Account.State state = Account.State.REGISTRATION_FAILED; - if (error != null) { - if (error.hasChild("conflict")) { - state = Account.State.REGISTRATION_CONFLICT; - } else if (error.hasChild("resource-constraint") - && "wait".equals(error.getAttribute("type"))) { - state = Account.State.REGISTRATION_PLEASE_WAIT; - } else if (error.hasChild("not-acceptable") - && PASSWORD_TOO_WEAK_MSGS.contains(error.findChildContent("text"))) { - state = Account.State.REGISTRATION_PASSWORD_TOO_WEAK; + public final OnIqPacketReceived registrationResponseListener = + (account, packet) -> { + if (packet.getType() == IqPacket.TYPE.RESULT) { + account.setOption(Account.OPTION_REGISTER, false); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": successfully registered new account on server"); + throw new StateChangingError(Account.State.REGISTRATION_SUCCESSFUL); + } else { + final List PASSWORD_TOO_WEAK_MSGS = + Arrays.asList( + "The password is too weak", "Please use a longer password."); + Element error = packet.findChild("error"); + Account.State state = Account.State.REGISTRATION_FAILED; + if (error != null) { + if (error.hasChild("conflict")) { + state = Account.State.REGISTRATION_CONFLICT; + } else if (error.hasChild("resource-constraint") + && "wait".equals(error.getAttribute("type"))) { + state = Account.State.REGISTRATION_PLEASE_WAIT; + } else if (error.hasChild("not-acceptable") + && PASSWORD_TOO_WEAK_MSGS.contains( + error.findChildContent("text"))) { + state = Account.State.REGISTRATION_PASSWORD_TOO_WEAK; + } + } + throw new StateChangingError(state); } - } - throw new StateChangingError(state); - } - }; + }; protected final Account account; private final Features features = new Features(this); private final HashMap disco = new HashMap<>(); private final HashMap commands = new HashMap<>(); private final SparseArray mStanzaQueue = new SparseArray<>(); - private final Hashtable> packetCallbacks = new Hashtable<>(); - private final Set advancedStreamFeaturesLoadedListeners = new HashSet<>(); + private final Hashtable> packetCallbacks = + new Hashtable<>(); + private final Set advancedStreamFeaturesLoadedListeners = + new HashSet<>(); private final XmppConnectionService mXmppConnectionService; private Socket socket; private XmlReader tagReader; @@ -150,7 +157,6 @@ public class XmppConnection implements Runnable { private boolean isBound = false; private Element streamFeatures; private String streamId = null; - private int smVersion = 3; private int stanzasReceived = 0; private int stanzasSent = 0; private long lastPacketReceived = 0; @@ -178,7 +184,6 @@ public class XmppConnection implements Runnable { private volatile Thread mThread; private CountDownLatch mStreamCountDownLatch; - public XmppConnection(final Account account, final XmppConnectionService service) { this.account = account; this.mXmppConnectionService = service; @@ -186,10 +191,12 @@ public class XmppConnection implements Runnable { private static void fixResource(Context context, Account account) { String resource = account.getResource(); - int fixedPartLength = context.getString(R.string.app_name).length() + 1; //include the trailing dot + int fixedPartLength = + context.getString(R.string.app_name).length() + 1; // include the trailing dot int randomPartLength = 4; // 3 bytes if (resource != null && resource.length() > fixedPartLength + randomPartLength) { - if (validBase64(resource.substring(fixedPartLength, fixedPartLength + randomPartLength))) { + if (validBase64( + resource.substring(fixedPartLength, fixedPartLength + randomPartLength))) { account.setResource(resource.substring(0, fixedPartLength + randomPartLength)); } } @@ -206,7 +213,12 @@ public class XmppConnection implements Runnable { private void changeStatus(final Account.State nextStatus) { synchronized (this) { if (Thread.currentThread().isInterrupted()) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": not changing status to " + nextStatus + " because thread was interrupted"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": not changing status to " + + nextStatus + + " because thread was interrupted"); return; } if (account.getStatus() != nextStatus) { @@ -260,7 +272,9 @@ public class XmppConnection implements Runnable { inSmacksSession = false; isBound = false; this.attempt++; - this.verifiedHostname = null; //will be set if user entered hostname is being used or hostname was verified with dnssec + this.verifiedHostname = + null; // will be set if user entered hostname is being used or hostname was verified + // with dnssec try { Socket localSocket; shouldAuthenticate = !account.isOptionSet(Account.OPTION_REGISTER); @@ -279,7 +293,13 @@ public class XmppConnection implements Runnable { final int port = account.getPort(); final boolean directTls = Resolver.useDirectTls(port); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": connect to " + destination + " via Tor. directTls=" + directTls); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": connect to " + + destination + + " via Tor. directTls=" + + directTls); localSocket = SocksSocketFactory.createSocketOverTor(destination, port); if (directTls) { @@ -290,7 +310,10 @@ public class XmppConnection implements Runnable { try { startXmpp(localSocket); } catch (InterruptedException e) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": thread was interrupted before beginning stream"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": thread was interrupted before beginning stream"); return; } catch (Exception e) { throw new IOException(e.getMessage()); @@ -309,41 +332,70 @@ public class XmppConnection implements Runnable { return; } if (results.size() == 0) { - Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": Resolver results were empty"); + Log.e( + Config.LOGTAG, + account.getJid().asBareJid() + ": Resolver results were empty"); return; } final Resolver.Result storedBackupResult; if (hardcoded) { storedBackupResult = null; } else { - storedBackupResult = mXmppConnectionService.databaseBackend.findResolverResult(domain); + storedBackupResult = + mXmppConnectionService.databaseBackend.findResolverResult(domain); if (storedBackupResult != null && !results.contains(storedBackupResult)) { results.add(storedBackupResult); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": loaded backup resolver result from db: " + storedBackupResult); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": loaded backup resolver result from db: " + + storedBackupResult); } } - for (Iterator iterator = results.iterator(); iterator.hasNext(); ) { + for (Iterator iterator = results.iterator(); + iterator.hasNext(); ) { final Resolver.Result result = iterator.next(); if (Thread.currentThread().isInterrupted()) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": Thread was interrupted"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": Thread was interrupted"); return; } try { // if tls is true, encryption is implied and must not be started features.encryptionEnabled = result.isDirectTls(); - verifiedHostname = result.isAuthenticated() ? result.getHostname().toString() : null; + verifiedHostname = + result.isAuthenticated() ? result.getHostname().toString() : null; Log.d(Config.LOGTAG, "verified hostname " + verifiedHostname); final InetSocketAddress addr; if (result.getIp() != null) { addr = new InetSocketAddress(result.getIp(), result.getPort()); - Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() - + ": using values from resolver " + (result.getHostname() == null ? "" : result.getHostname().toString() - + "/") + result.getIp().getHostAddress() + ":" + result.getPort() + " tls: " + features.encryptionEnabled); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid().toString() + + ": using values from resolver " + + (result.getHostname() == null + ? "" + : result.getHostname().toString() + "/") + + result.getIp().getHostAddress() + + ":" + + result.getPort() + + " tls: " + + features.encryptionEnabled); } else { - addr = new InetSocketAddress(IDN.toASCII(result.getHostname().toString()), result.getPort()); - Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() - + ": using values from resolver " - + result.getHostname().toString() + ":" + result.getPort() + " tls: " + features.encryptionEnabled); + addr = + new InetSocketAddress( + IDN.toASCII(result.getHostname().toString()), + result.getPort()); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid().toString() + + ": using values from resolver " + + result.getHostname().toString() + + ":" + + result.getPort() + + " tls: " + + features.encryptionEnabled); } localSocket = new Socket(); @@ -355,9 +407,12 @@ public class XmppConnection implements Runnable { localSocket.setSoTimeout(Config.SOCKET_TIMEOUT * 1000); if (startXmpp(localSocket)) { - localSocket.setSoTimeout(0); //reset to 0; once the connection is established we don’t want this + localSocket.setSoTimeout( + 0); // reset to 0; once the connection is established we don’t + // want this if (!hardcoded && !result.equals(storedBackupResult)) { - mXmppConnectionService.databaseBackend.saveResolverResult(domain, result); + mXmppConnectionService.databaseBackend.saveResolverResult( + domain, result); } break; // successfully connected to server that speaks xmpp } else { @@ -369,10 +424,20 @@ public class XmppConnection implements Runnable { throw e; } } catch (InterruptedException e) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": thread was interrupted before beginning stream"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": thread was interrupted before beginning stream"); return; } catch (final Throwable e) { - Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": " + e.getMessage() + "(" + e.getClass().getName() + ")"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid().toString() + + ": " + + e.getMessage() + + "(" + + e.getClass().getName() + + ")"); if (!iterator.hasNext()) { throw new UnknownHostException(); } @@ -384,7 +449,9 @@ public class XmppConnection implements Runnable { this.changeStatus(Account.State.MISSING_INTERNET_PERMISSION); } catch (final StateChangingException e) { this.changeStatus(e.state); - } catch (final UnknownHostException | ConnectException | SocksSocketFactory.HostNotFoundException e) { + } catch (final UnknownHostException + | ConnectException + | SocksSocketFactory.HostNotFoundException e) { this.changeStatus(Account.State.SERVER_NOT_FOUND); } catch (final SocksSocketFactory.SocksProxyNotFoundException e) { this.changeStatus(Account.State.TOR_NOT_AVAILABLE); @@ -396,7 +463,10 @@ public class XmppConnection implements Runnable { if (!Thread.currentThread().isInterrupted()) { forceCloseSocket(); } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": not force closing socket because thread was interrupted"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": not force closing socket because thread was interrupted"); } } } @@ -430,17 +500,26 @@ public class XmppConnection implements Runnable { return tag != null && tag.isStart("stream"); } - private SSLSocketFactory getSSLSocketFactory() throws NoSuchAlgorithmException, KeyManagementException { + private SSLSocketFactory getSSLSocketFactory() + throws NoSuchAlgorithmException, KeyManagementException { final SSLContext sc = SSLSocketHelper.getSSLContext(); - final MemorizingTrustManager trustManager = this.mXmppConnectionService.getMemorizingTrustManager(); + final MemorizingTrustManager trustManager = + this.mXmppConnectionService.getMemorizingTrustManager(); final KeyManager[] keyManager; if (account.getPrivateKeyAlias() != null) { - keyManager = new KeyManager[]{new MyKeyManager()}; + keyManager = new KeyManager[] {new MyKeyManager()}; } else { keyManager = null; } final String domain = account.getServer(); - sc.init(keyManager, new X509TrustManager[]{mInteractive ? trustManager.getInteractive(domain) : trustManager.getNonInteractive(domain)}, mXmppConnectionService.getRNG()); + sc.init( + keyManager, + new X509TrustManager[] { + mInteractive + ? trustManager.getInteractive(domain) + : trustManager.getNonInteractive(domain) + }, + mXmppConnectionService.getRNG()); return sc.getSocketFactory(); } @@ -449,7 +528,10 @@ public class XmppConnection implements Runnable { synchronized (this) { this.mThread = Thread.currentThread(); if (this.mThread.isInterrupted()) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": aborting connect because thread was interrupted"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": aborting connect because thread was interrupted"); return; } forceCloseSocket(); @@ -540,20 +622,16 @@ public class XmppConnection implements Runnable { Log.d( Config.LOGTAG, account.getJid().asBareJid().toString() - + ": stream management(" - + smVersion - + ") enabled (resumable)"); + + ": stream management enabled (resumable)"); } else { Log.d( Config.LOGTAG, account.getJid().asBareJid().toString() - + ": stream management(" - + smVersion - + ") enabled"); + + ": stream management enabled"); } this.stanzasReceived = 0; this.inSmacksSession = true; - final RequestPacket r = new RequestPacket(smVersion); + final RequestPacket r = new RequestPacket(); tagWriter.writeStanzaAsync(r); } else if (nextTag.isStart("resumed")) { final Element resumed = tagReader.readElement(nextTag); @@ -567,7 +645,7 @@ public class XmppConnection implements Runnable { + ": acknowledging stanza #" + this.stanzasReceived); } - final AckPacket ack = new AckPacket(this.stanzasReceived, smVersion); + final AckPacket ack = new AckPacket(this.stanzasReceived); tagWriter.writeStanzaAsync(ack); } else if (nextTag.isStart("a")) { boolean accountUiNeedsRefresh = false; @@ -628,7 +706,8 @@ public class XmppConnection implements Runnable { } } - private boolean processSuccess(final Element success) throws IOException, XmlPullParserException { + private boolean processSuccess(final Element success) + throws IOException, XmlPullParserException { final SaslMechanism.Version version; try { version = SaslMechanism.Version.of(success); @@ -651,20 +730,22 @@ public class XmppConnection implements Runnable { } Log.d( Config.LOGTAG, - account.getJid().asBareJid().toString() - + ": logged in (using " - + version - + ")"); - account.setKey( - Account.PINNED_MECHANISM_KEY, String.valueOf(saslMechanism.getPriority())); + account.getJid().asBareJid().toString() + ": logged in (using " + version + ")"); + account.setKey(Account.PINNED_MECHANISM_KEY, String.valueOf(saslMechanism.getPriority())); if (version == SaslMechanism.Version.SASL_2) { final String authorizationIdentifier = success.findChildContent("authorization-identifier"); final Jid authorizationJid; try { - authorizationJid = Strings.isNullOrEmpty(authorizationIdentifier) ? null : Jid.ofEscaped(authorizationIdentifier); + authorizationJid = + Strings.isNullOrEmpty(authorizationIdentifier) + ? null + : Jid.ofEscaped(authorizationIdentifier); } catch (final IllegalArgumentException e) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": SASL 2.0 authorization identifier was not a valid jid"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": SASL 2.0 authorization identifier was not a valid jid"); throw new StateChangingException(Account.State.BIND_FAILURE); } if (authorizationJid == null) { @@ -676,11 +757,18 @@ public class XmppConnection implements Runnable { + ": SASL 2.0 authorization identifier was " + authorizationJid); if (!account.getJid().getDomain().equals(authorizationJid.getDomain())) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server tried to re-assign domain to " + authorizationJid.getDomain()); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": server tried to re-assign domain to " + + authorizationJid.getDomain()); throw new StateChangingError(Account.State.BIND_FAILURE); } if (authorizationJid.isFullJid() && account.setJid(authorizationJid)) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": jid changed during SASL 2.0. updating database"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": jid changed during SASL 2.0. updating database"); mXmppConnectionService.databaseBackend.updateAccount(account); } final Element resumed = success.findChild("resumed", "urn:xmpp:sm:3"); @@ -709,7 +797,7 @@ public class XmppConnection implements Runnable { private void processResumed(final Element resumed) throws StateChangingException { this.inSmacksSession = true; this.isBound = true; - this.tagWriter.writeStanzaAsync(new RequestPacket(smVersion)); + this.tagWriter.writeStanzaAsync(new RequestPacket()); lastPacketReceived = SystemClock.elapsedRealtime(); final String h = resumed.getAttribute("h"); if (h == null) { @@ -795,13 +883,22 @@ public class XmppConnection implements Runnable { private boolean acknowledgeStanzaUpTo(int serverCount) { if (serverCount > stanzasSent) { - Log.e(Config.LOGTAG, "server acknowledged more stanzas than we sent. serverCount=" + serverCount + ", ourCount=" + stanzasSent); + Log.e( + Config.LOGTAG, + "server acknowledged more stanzas than we sent. serverCount=" + + serverCount + + ", ourCount=" + + stanzasSent); } boolean acknowledgedMessages = false; for (int i = 0; i < mStanzaQueue.size(); ++i) { if (serverCount >= mStanzaQueue.keyAt(i)) { if (Config.EXTENDED_SM_LOGGING) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server acknowledged stanza #" + mStanzaQueue.keyAt(i)); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": server acknowledged stanza #" + + mStanzaQueue.keyAt(i)); } final AbstractAcknowledgeableStanza stanza = mStanzaQueue.valueAt(i); if (stanza instanceof MessagePacket && acknowledgedListener != null) { @@ -809,7 +906,8 @@ public class XmppConnection implements Runnable { final String id = packet.getId(); final Jid to = packet.getTo(); if (id != null && to != null) { - acknowledgedMessages |= acknowledgedListener.onMessageAcknowledged(account, to, id); + acknowledgedMessages |= + acknowledgedListener.onMessageAcknowledged(account, to, id); } } mStanzaQueue.removeAt(i); @@ -819,8 +917,8 @@ public class XmppConnection implements Runnable { return acknowledgedMessages; } - private @NonNull - Element processPacket(final Tag currentTag, final int packetType) throws IOException { + private @NonNull Element processPacket(final Tag currentTag, final int packetType) + throws IOException { final Element element; switch (packetType) { case PACKET_IQ: @@ -856,7 +954,12 @@ public class XmppConnection implements Runnable { if (inSmacksSession) { ++stanzasReceived; } else if (features.sm()) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": not counting stanza(" + element.getClass().getSimpleName() + "). Not in smacks session."); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": not counting stanza(" + + element.getClass().getSimpleName() + + "). Not in smacks session."); } lastPacketReceived = SystemClock.elapsedRealtime(); if (Config.BACKGROUND_STANZA_LOGGING && mXmppConnectionService.checkListeners()) { @@ -874,7 +977,13 @@ public class XmppConnection implements Runnable { private void processIq(final Tag currentTag) throws IOException { final IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ); if (!packet.valid()) { - Log.e(Config.LOGTAG, "encountered invalid iq from='" + packet.getFrom() + "' to='" + packet.getTo() + "'"); + Log.e( + Config.LOGTAG, + "encountered invalid iq from='" + + packet.getFrom() + + "' to='" + + packet.getTo() + + "'"); return; } if (packet instanceof JinglePacket) { @@ -884,7 +993,8 @@ public class XmppConnection implements Runnable { } else { OnIqPacketReceived callback = null; synchronized (this.packetCallbacks) { - final Pair packetCallbackDuple = packetCallbacks.get(packet.getId()); + final Pair packetCallbackDuple = + packetCallbacks.get(packet.getId()); if (packetCallbackDuple != null) { // Packets to the server should have responses from the server if (packetCallbackDuple.first.toServer(account)) { @@ -892,17 +1002,25 @@ public class XmppConnection implements Runnable { callback = packetCallbackDuple.second; packetCallbacks.remove(packet.getId()); } else { - Log.e(Config.LOGTAG, account.getJid().asBareJid().toString() + ": ignoring spoofed iq packet"); + Log.e( + Config.LOGTAG, + account.getJid().asBareJid().toString() + + ": ignoring spoofed iq packet"); } } else { - if (packet.getFrom() != null && packet.getFrom().equals(packetCallbackDuple.first.getTo())) { + if (packet.getFrom() != null + && packet.getFrom().equals(packetCallbackDuple.first.getTo())) { callback = packetCallbackDuple.second; packetCallbacks.remove(packet.getId()); } else { - Log.e(Config.LOGTAG, account.getJid().asBareJid().toString() + ": ignoring spoofed iq packet"); + Log.e( + Config.LOGTAG, + account.getJid().asBareJid().toString() + + ": ignoring spoofed iq packet"); } } - } else if (packet.getType() == IqPacket.TYPE.GET || packet.getType() == IqPacket.TYPE.SET) { + } else if (packet.getType() == IqPacket.TYPE.GET + || packet.getType() == IqPacket.TYPE.SET) { callback = this.unregisteredIqListener; } } @@ -919,7 +1037,13 @@ public class XmppConnection implements Runnable { private void processMessage(final Tag currentTag) throws IOException { final MessagePacket packet = (MessagePacket) processPacket(currentTag, PACKET_MESSAGE); if (!packet.valid()) { - Log.e(Config.LOGTAG, "encountered invalid message from='" + packet.getFrom() + "' to='" + packet.getTo() + "'"); + Log.e( + Config.LOGTAG, + "encountered invalid message from='" + + packet.getFrom() + + "' to='" + + packet.getTo() + + "'"); return; } this.messageListener.onMessagePacketReceived(account, packet); @@ -928,7 +1052,13 @@ public class XmppConnection implements Runnable { private void processPresence(final Tag currentTag) throws IOException { PresencePacket packet = (PresencePacket) processPacket(currentTag, PACKET_PRESENCE); if (!packet.valid()) { - Log.e(Config.LOGTAG, "encountered invalid presence from='" + packet.getFrom() + "' to='" + packet.getTo() + "'"); + Log.e( + Config.LOGTAG, + "encountered invalid presence from='" + + packet.getFrom() + + "' to='" + + packet.getTo() + + "'"); return; } this.presenceListener.onPresencePacketReceived(account, packet); @@ -967,14 +1097,21 @@ public class XmppConnection implements Runnable { throw new StateChangingException(Account.State.TLS_ERROR); } final InetAddress address = socket.getInetAddress(); - final SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(socket, address.getHostAddress(), socket.getPort(), true); + final SSLSocket sslSocket = + (SSLSocket) + sslSocketFactory.createSocket( + socket, address.getHostAddress(), socket.getPort(), true); SSLSocketHelper.setSecurity(sslSocket); SSLSocketHelper.setHostname(sslSocket, IDN.toASCII(account.getServer())); SSLSocketHelper.setApplicationProtocol(sslSocket, "xmpp-client"); final XmppDomainVerifier xmppDomainVerifier = new XmppDomainVerifier(); try { - if (!xmppDomainVerifier.verify(account.getServer(), this.verifiedHostname, sslSocket.getSession())) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": TLS certificate domain verification failed"); + if (!xmppDomainVerifier.verify( + account.getServer(), this.verifiedHostname, sslSocket.getSession())) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": TLS certificate domain verification failed"); FileBackend.close(sslSocket); throw new StateChangingException(Account.State.TLS_ERROR_DOMAIN); } @@ -1016,7 +1153,7 @@ public class XmppConnection implements Runnable { && shouldAuthenticate && isSecure) { authenticate(SaslMechanism.Version.SASL); - } else if (this.streamFeatures.hasChild("sm", "urn:xmpp:sm:" + smVersion) + } else if (this.streamFeatures.hasChild("sm", Namespace.STREAM_MANAGEMENT) && streamId != null) { if (Config.EXTENDED_SM_LOGGING) { Log.d( @@ -1025,7 +1162,7 @@ public class XmppConnection implements Runnable { + ": resuming after stanza #" + stanzasReceived); } - final ResumePacket resume = new ResumePacket(this.streamId, stanzasReceived, smVersion); + final ResumePacket resume = new ResumePacket(this.streamId, stanzasReceived); this.mSmCatchupMessageCounter.set(0); this.mWaitingForSmCatchup.set(true); this.tagWriter.writeStanzaAsync(resume); @@ -1059,7 +1196,8 @@ public class XmppConnection implements Runnable { saslMechanism = new ScramSha256(tagWriter, account, mXmppConnectionService.getRNG()); } else if (mechanisms.contains(ScramSha1.MECHANISM)) { saslMechanism = new ScramSha1(tagWriter, account, mXmppConnectionService.getRNG()); - } else if (mechanisms.contains(Plain.MECHANISM) && !account.getJid().getDomain().toEscapedString().equals("nimbuzz.com")) { + } else if (mechanisms.contains(Plain.MECHANISM) + && !account.getJid().getDomain().toEscapedString().equals("nimbuzz.com")) { saslMechanism = new Plain(tagWriter, account); } else if (mechanisms.contains(DigestMd5.MECHANISM)) { saslMechanism = new DigestMd5(tagWriter, account, mXmppConnectionService.getRNG()); @@ -1067,15 +1205,24 @@ public class XmppConnection implements Runnable { saslMechanism = new Anonymous(tagWriter, account, mXmppConnectionService.getRNG()); } if (saslMechanism == null) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to find supported SASL mechanism in " + mechanisms); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": unable to find supported SASL mechanism in " + + mechanisms); throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } final int pinnedMechanism = account.getKeyAsInt(Account.PINNED_MECHANISM_KEY, -1); if (pinnedMechanism > saslMechanism.getPriority()) { - Log.e(Config.LOGTAG, "Auth failed. Authentication mechanism " + saslMechanism.getMechanism() + - " has lower priority (" + saslMechanism.getPriority() + - ") than pinned priority (" + pinnedMechanism + - "). Possible downgrade attack?"); + Log.e( + Config.LOGTAG, + "Auth failed. Authentication mechanism " + + saslMechanism.getMechanism() + + " has lower priority (" + + saslMechanism.getPriority() + + ") than pinned priority (" + + pinnedMechanism + + "). Possible downgrade attack?"); throw new StateChangingException(Account.State.DOWNGRADE_ATTACK); } final String firstMessage = saslMechanism.getClientFirstMessage(); @@ -1091,9 +1238,11 @@ public class XmppConnection implements Runnable { authenticate.addChild("initial-response").setContent(firstMessage); } final Element inline = this.streamFeatures.findChild("inline", Namespace.SASL_2); - final boolean inlineStreamManagement = inline != null && inline.hasChild("sm", "urn:xmpp:sm:3"); + final boolean inlineStreamManagement = + inline != null && inline.hasChild("sm", "urn:xmpp:sm:3"); + final boolean inlineBind2 = inline != null && inline.hasChild("bind", Namespace.BIND2); if (inlineStreamManagement && streamId != null) { - final ResumePacket resume = new ResumePacket(this.streamId, stanzasReceived, smVersion); + final ResumePacket resume = new ResumePacket(this.streamId, stanzasReceived); this.mSmCatchupMessageCounter.set(0); this.mWaitingForSmCatchup.set(true); authenticate.addChild(resume); @@ -1102,35 +1251,46 @@ public class XmppConnection implements Runnable { throw new AssertionError("Missing implementation for " + version); } - Log.d(Config.LOGTAG, account.getJid().toString() + ": Authenticating with "+version+ "/" + saslMechanism.getMechanism()); + Log.d( + Config.LOGTAG, + account.getJid().toString() + + ": Authenticating with " + + version + + "/" + + saslMechanism.getMechanism()); authenticate.setAttribute("mechanism", saslMechanism.getMechanism()); tagWriter.writeElement(authenticate); } private static List extractMechanisms(final Element stream) { - final ArrayList mechanisms = new ArrayList<>(stream - .getChildren().size()); + final ArrayList mechanisms = new ArrayList<>(stream.getChildren().size()); for (final Element child : stream.getChildren()) { mechanisms.add(child.getContent()); } return mechanisms; } - private void register() { final String preAuth = account.getKey(Account.PRE_AUTH_REGISTRATION_TOKEN); if (preAuth != null && features.invite()) { final IqPacket preAuthRequest = new IqPacket(IqPacket.TYPE.SET); preAuthRequest.addChild("preauth", Namespace.PARS).setAttribute("token", preAuth); - sendUnmodifiedIqPacket(preAuthRequest, (account, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { - sendRegistryRequest(); - } else { - final String error = response.getErrorCondition(); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": failed to pre auth. " + error); - throw new StateChangingError(Account.State.REGISTRATION_INVALID_TOKEN); - } - }, true); + sendUnmodifiedIqPacket( + preAuthRequest, + (account, response) -> { + if (response.getType() == IqPacket.TYPE.RESULT) { + sendRegistryRequest(); + } else { + final String error = response.getErrorCondition(); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": failed to pre auth. " + + error); + throw new StateChangingError(Account.State.REGISTRATION_INVALID_TOKEN); + } + }, + true); } else { sendRegistryRequest(); } @@ -1140,79 +1300,91 @@ public class XmppConnection implements Runnable { final IqPacket register = new IqPacket(IqPacket.TYPE.GET); register.query(Namespace.REGISTER); register.setTo(account.getDomain()); - sendUnmodifiedIqPacket(register, (account, packet) -> { - if (packet.getType() == IqPacket.TYPE.TIMEOUT) { - return; - } - if (packet.getType() == IqPacket.TYPE.ERROR) { - throw new StateChangingError(Account.State.REGISTRATION_FAILED); - } - final Element query = packet.query(Namespace.REGISTER); - if (query.hasChild("username") && (query.hasChild("password"))) { - final IqPacket register1 = new IqPacket(IqPacket.TYPE.SET); - final Element username = new Element("username").setContent(account.getUsername()); - final Element password = new Element("password").setContent(account.getPassword()); - register1.query(Namespace.REGISTER).addChild(username); - register1.query().addChild(password); - register1.setFrom(account.getJid().asBareJid()); - sendUnmodifiedIqPacket(register1, registrationResponseListener, true); - } else if (query.hasChild("x", Namespace.DATA)) { - final Data data = Data.parse(query.findChild("x", Namespace.DATA)); - final Element blob = query.findChild("data", "urn:xmpp:bob"); - final String id = packet.getId(); - InputStream is; - if (blob != null) { - try { - final String base64Blob = blob.getContent(); - final byte[] strBlob = Base64.decode(base64Blob, Base64.DEFAULT); - is = new ByteArrayInputStream(strBlob); - } catch (Exception e) { - is = null; + sendUnmodifiedIqPacket( + register, + (account, packet) -> { + if (packet.getType() == IqPacket.TYPE.TIMEOUT) { + return; } - } else { - final boolean useTor = mXmppConnectionService.useTorToConnect() || account.isOnion(); - try { - final String url = data.getValue("url"); - final String fallbackUrl = data.getValue("captcha-fallback-url"); - if (url != null) { - is = HttpConnectionManager.open(url, useTor); - } else if (fallbackUrl != null) { - is = HttpConnectionManager.open(fallbackUrl, useTor); + if (packet.getType() == IqPacket.TYPE.ERROR) { + throw new StateChangingError(Account.State.REGISTRATION_FAILED); + } + final Element query = packet.query(Namespace.REGISTER); + if (query.hasChild("username") && (query.hasChild("password"))) { + final IqPacket register1 = new IqPacket(IqPacket.TYPE.SET); + final Element username = + new Element("username").setContent(account.getUsername()); + final Element password = + new Element("password").setContent(account.getPassword()); + register1.query(Namespace.REGISTER).addChild(username); + register1.query().addChild(password); + register1.setFrom(account.getJid().asBareJid()); + sendUnmodifiedIqPacket(register1, registrationResponseListener, true); + } else if (query.hasChild("x", Namespace.DATA)) { + final Data data = Data.parse(query.findChild("x", Namespace.DATA)); + final Element blob = query.findChild("data", "urn:xmpp:bob"); + final String id = packet.getId(); + InputStream is; + if (blob != null) { + try { + final String base64Blob = blob.getContent(); + final byte[] strBlob = Base64.decode(base64Blob, Base64.DEFAULT); + is = new ByteArrayInputStream(strBlob); + } catch (Exception e) { + is = null; + } } else { - is = null; + final boolean useTor = + mXmppConnectionService.useTorToConnect() || account.isOnion(); + try { + final String url = data.getValue("url"); + final String fallbackUrl = data.getValue("captcha-fallback-url"); + if (url != null) { + is = HttpConnectionManager.open(url, useTor); + } else if (fallbackUrl != null) { + is = HttpConnectionManager.open(fallbackUrl, useTor); + } else { + is = null; + } + } catch (final IOException e) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": unable to fetch captcha", + e); + is = null; + } } - } catch (final IOException e) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": unable to fetch captcha", e); - is = null; - } - } - if (is != null) { - Bitmap captcha = BitmapFactory.decodeStream(is); - try { - if (mXmppConnectionService.displayCaptchaRequest(account, id, data, captcha)) { - return; + if (is != null) { + Bitmap captcha = BitmapFactory.decodeStream(is); + try { + if (mXmppConnectionService.displayCaptchaRequest( + account, id, data, captcha)) { + return; + } + } catch (Exception e) { + throw new StateChangingError(Account.State.REGISTRATION_FAILED); + } + } + throw new StateChangingError(Account.State.REGISTRATION_FAILED); + } else if (query.hasChild("instructions") + || query.hasChild("x", Namespace.OOB)) { + final String instructions = query.findChildContent("instructions"); + final Element oob = query.findChild("x", Namespace.OOB); + final String url = oob == null ? null : oob.findChildContent("url"); + if (url != null) { + setAccountCreationFailed(url); + } else if (instructions != null) { + final Matcher matcher = Patterns.AUTOLINK_WEB_URL.matcher(instructions); + if (matcher.find()) { + setAccountCreationFailed( + instructions.substring(matcher.start(), matcher.end())); + } } - } catch (Exception e) { throw new StateChangingError(Account.State.REGISTRATION_FAILED); } - } - throw new StateChangingError(Account.State.REGISTRATION_FAILED); - } else if (query.hasChild("instructions") || query.hasChild("x", Namespace.OOB)) { - final String instructions = query.findChildContent("instructions"); - final Element oob = query.findChild("x", Namespace.OOB); - final String url = oob == null ? null : oob.findChildContent("url"); - if (url != null) { - setAccountCreationFailed(url); - } else if (instructions != null) { - final Matcher matcher = Patterns.AUTOLINK_WEB_URL.matcher(instructions); - if (matcher.find()) { - setAccountCreationFailed(instructions.substring(matcher.start(), matcher.end())); - } - } - throw new StateChangingError(Account.State.REGISTRATION_FAILED); - } - }, true); + }, + true); } private void setAccountCreationFailed(final String url) { @@ -1247,7 +1419,10 @@ public class XmppConnection implements Runnable { try { mXmppConnectionService.restoredFromDatabaseLatch.await(); } catch (InterruptedException e) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": interrupted while waiting for DB restore during bind"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": interrupted while waiting for DB restore during bind"); return; } clearIqCallbacks(); @@ -1257,49 +1432,76 @@ public class XmppConnection implements Runnable { fixResource(mXmppConnectionService, account); } final IqPacket iq = new IqPacket(IqPacket.TYPE.SET); - final String resource = Config.USE_RANDOM_RESOURCE_ON_EVERY_BIND ? nextRandomId() : account.getResource(); + final String resource = + Config.USE_RANDOM_RESOURCE_ON_EVERY_BIND ? nextRandomId() : account.getResource(); iq.addChild("bind", Namespace.BIND).addChild("resource").setContent(resource); - this.sendUnmodifiedIqPacket(iq, (account, packet) -> { - if (packet.getType() == IqPacket.TYPE.TIMEOUT) { - return; - } - final Element bind = packet.findChild("bind"); - if (bind != null && packet.getType() == IqPacket.TYPE.RESULT) { - isBound = true; - final Element jid = bind.findChild("jid"); - if (jid != null && jid.getContent() != null) { - try { - Jid assignedJid = Jid.ofEscaped(jid.getContent()); - if (!account.getJid().getDomain().equals(assignedJid.getDomain())) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server tried to re-assign domain to " + assignedJid.getDomain()); - throw new StateChangingError(Account.State.BIND_FAILURE); - } - if (account.setJid(assignedJid)) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": jid changed during bind. updating database"); - mXmppConnectionService.databaseBackend.updateAccount(account); - } - if (streamFeatures.hasChild("session") - && !streamFeatures.findChild("session").hasChild("optional")) { - sendStartSession(); + this.sendUnmodifiedIqPacket( + iq, + (account, packet) -> { + if (packet.getType() == IqPacket.TYPE.TIMEOUT) { + return; + } + final Element bind = packet.findChild("bind"); + if (bind != null && packet.getType() == IqPacket.TYPE.RESULT) { + isBound = true; + final Element jid = bind.findChild("jid"); + if (jid != null && jid.getContent() != null) { + try { + Jid assignedJid = Jid.ofEscaped(jid.getContent()); + if (!account.getJid().getDomain().equals(assignedJid.getDomain())) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": server tried to re-assign domain to " + + assignedJid.getDomain()); + throw new StateChangingError(Account.State.BIND_FAILURE); + } + if (account.setJid(assignedJid)) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": jid changed during bind. updating database"); + mXmppConnectionService.databaseBackend.updateAccount(account); + } + if (streamFeatures.hasChild("session") + && !streamFeatures + .findChild("session") + .hasChild("optional")) { + sendStartSession(); + } else { + sendPostBindInitialization(); + } + return; + } catch (final IllegalArgumentException e) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": server reported invalid jid (" + + jid.getContent() + + ") on bind"); + } } else { - sendPostBindInitialization(); + Log.d( + Config.LOGTAG, + account.getJid() + + ": disconnecting because of bind failure. (no jid)"); } - return; - } catch (final IllegalArgumentException e) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server reported invalid jid (" + jid.getContent() + ") on bind"); + } else { + Log.d( + Config.LOGTAG, + account.getJid() + + ": disconnecting because of bind failure (" + + packet); } - } else { - Log.d(Config.LOGTAG, account.getJid() + ": disconnecting because of bind failure. (no jid)"); - } - } else { - Log.d(Config.LOGTAG, account.getJid() + ": disconnecting because of bind failure (" + packet); - } - final Element error = packet.findChild("error"); - if (packet.getType() == IqPacket.TYPE.ERROR && error != null && error.hasChild("conflict")) { - account.setResource(createNewResource()); - } - throw new StateChangingError(Account.State.BIND_FAILURE); - }, true); + final Element error = packet.findChild("error"); + if (packet.getType() == IqPacket.TYPE.ERROR + && error != null + && error.hasChild("conflict")) { + account.setResource(createNewResource()); + } + throw new StateChangingError(Account.State.BIND_FAILURE); + }, + true); } private void clearIqCallbacks() { @@ -1309,8 +1511,14 @@ public class XmppConnection implements Runnable { if (this.packetCallbacks.size() == 0) { return; } - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": clearing " + this.packetCallbacks.size() + " iq callbacks"); - final Iterator> iterator = this.packetCallbacks.values().iterator(); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": clearing " + + this.packetCallbacks.size() + + " iq callbacks"); + final Iterator> iterator = + this.packetCallbacks.values().iterator(); while (iterator.hasNext()) { Pair entry = iterator.next(); callbacks.add(entry.second); @@ -1321,43 +1529,56 @@ public class XmppConnection implements Runnable { try { callback.onIqPacketReceived(account, failurePacket); } catch (StateChangingError error) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": caught StateChangingError(" + error.state.toString() + ") while clearing callbacks"); - //ignore + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": caught StateChangingError(" + + error.state.toString() + + ") while clearing callbacks"); + // ignore } } - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": done clearing iq callbacks. " + this.packetCallbacks.size() + " left"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": done clearing iq callbacks. " + + this.packetCallbacks.size() + + " left"); } public void sendDiscoTimeout() { if (mWaitForDisco.compareAndSet(true, false)) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": finalizing bind after disco timeout"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": finalizing bind after disco timeout"); finalizeBind(); } } private void sendStartSession() { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending legacy session to outdated server"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": sending legacy session to outdated server"); final IqPacket startSession = new IqPacket(IqPacket.TYPE.SET); startSession.addChild("session", "urn:ietf:params:xml:ns:xmpp-session"); - this.sendUnmodifiedIqPacket(startSession, (account, packet) -> { - if (packet.getType() == IqPacket.TYPE.RESULT) { - sendPostBindInitialization(); - } else if (packet.getType() != IqPacket.TYPE.TIMEOUT) { - throw new StateChangingError(Account.State.SESSION_FAILURE); - } - }, true); + this.sendUnmodifiedIqPacket( + startSession, + (account, packet) -> { + if (packet.getType() == IqPacket.TYPE.RESULT) { + sendPostBindInitialization(); + } else if (packet.getType() != IqPacket.TYPE.TIMEOUT) { + throw new StateChangingError(Account.State.SESSION_FAILURE); + } + }, + true); } private void sendPostBindInitialization() { - smVersion = 0; - if (streamFeatures.hasChild("sm", "urn:xmpp:sm:3")) { - smVersion = 3; - } else if (streamFeatures.hasChild("sm", "urn:xmpp:sm:2")) { - smVersion = 2; - } - if (smVersion != 0) { + final boolean streamManagement = + this.streamFeatures.hasChild("sm", Namespace.STREAM_MANAGEMENT); + if (streamManagement) { synchronized (this.mStanzaQueue) { - final EnablePacket enable = new EnablePacket(smVersion); + final EnablePacket enable = new EnablePacket(); tagWriter.writeStanzaAsync(enable); stanzasSent = 0; mStanzaQueue.clear(); @@ -1370,22 +1591,29 @@ public class XmppConnection implements Runnable { } Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": starting service discovery"); mPendingServiceDiscoveries.set(0); - if (smVersion == 0 || Patches.DISCO_EXCEPTIONS.contains(account.getJid().getDomain().toEscapedString())) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": do not wait for service discovery"); + if (!streamManagement + || Patches.DISCO_EXCEPTIONS.contains( + account.getJid().getDomain().toEscapedString())) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": do not wait for service discovery"); mWaitForDisco.set(false); } else { mWaitForDisco.set(true); } lastDiscoStarted = SystemClock.elapsedRealtime(); - mXmppConnectionService.scheduleWakeUpCall(Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode()); + mXmppConnectionService.scheduleWakeUpCall( + Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode()); Element caps = streamFeatures.findChild("c"); final String hash = caps == null ? null : caps.getAttribute("hash"); final String ver = caps == null ? null : caps.getAttribute("ver"); ServiceDiscoveryResult discoveryResult = null; if (hash != null && ver != null) { - discoveryResult = mXmppConnectionService.getCachedServiceDiscoveryResult(new Pair<>(hash, ver)); + discoveryResult = + mXmppConnectionService.getCachedServiceDiscoveryResult(new Pair<>(hash, ver)); } - final boolean requestDiscoItemsFirst = !account.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY); + final boolean requestDiscoItemsFirst = + !account.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY); if (requestDiscoItemsFirst) { sendServiceDiscoveryItems(account.getDomain()); } @@ -1412,84 +1640,109 @@ public class XmppConnection implements Runnable { final IqPacket iq = new IqPacket(IqPacket.TYPE.GET); iq.setTo(jid); iq.query("http://jabber.org/protocol/disco#info"); - this.sendIqPacket(iq, (account, packet) -> { - if (packet.getType() == IqPacket.TYPE.RESULT) { - boolean advancedStreamFeaturesLoaded; - synchronized (XmppConnection.this.disco) { - ServiceDiscoveryResult result = new ServiceDiscoveryResult(packet); - if (jid.equals(account.getDomain())) { - mXmppConnectionService.databaseBackend.insertDiscoveryResult(result); + this.sendIqPacket( + iq, + (account, packet) -> { + if (packet.getType() == IqPacket.TYPE.RESULT) { + boolean advancedStreamFeaturesLoaded; + synchronized (XmppConnection.this.disco) { + ServiceDiscoveryResult result = new ServiceDiscoveryResult(packet); + if (jid.equals(account.getDomain())) { + mXmppConnectionService.databaseBackend.insertDiscoveryResult( + result); + } + disco.put(jid, result); + advancedStreamFeaturesLoaded = + disco.containsKey(account.getDomain()) + && disco.containsKey(account.getJid().asBareJid()); + } + if (advancedStreamFeaturesLoaded + && (jid.equals(account.getDomain()) + || jid.equals(account.getJid().asBareJid()))) { + enableAdvancedStreamFeatures(); + } + } else if (packet.getType() == IqPacket.TYPE.ERROR) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": could not query disco info for " + + jid.toString()); + final boolean serverOrAccount = + jid.equals(account.getDomain()) + || jid.equals(account.getJid().asBareJid()); + final boolean advancedStreamFeaturesLoaded; + if (serverOrAccount) { + synchronized (XmppConnection.this.disco) { + disco.put(jid, ServiceDiscoveryResult.empty()); + advancedStreamFeaturesLoaded = + disco.containsKey(account.getDomain()) + && disco.containsKey(account.getJid().asBareJid()); + } + } else { + advancedStreamFeaturesLoaded = false; + } + if (advancedStreamFeaturesLoaded) { + enableAdvancedStreamFeatures(); + } } - disco.put(jid, result); - advancedStreamFeaturesLoaded = disco.containsKey(account.getDomain()) - && disco.containsKey(account.getJid().asBareJid()); - } - if (advancedStreamFeaturesLoaded && (jid.equals(account.getDomain()) || jid.equals(account.getJid().asBareJid()))) { - enableAdvancedStreamFeatures(); - } - } else if (packet.getType() == IqPacket.TYPE.ERROR) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could not query disco info for " + jid.toString()); - final boolean serverOrAccount = jid.equals(account.getDomain()) || jid.equals(account.getJid().asBareJid()); - final boolean advancedStreamFeaturesLoaded; - if (serverOrAccount) { - synchronized (XmppConnection.this.disco) { - disco.put(jid, ServiceDiscoveryResult.empty()); - advancedStreamFeaturesLoaded = disco.containsKey(account.getDomain()) && disco.containsKey(account.getJid().asBareJid()); + if (packet.getType() != IqPacket.TYPE.TIMEOUT) { + if (mPendingServiceDiscoveries.decrementAndGet() == 0 + && mWaitForDisco.compareAndSet(true, false)) { + finalizeBind(); + } } - } else { - advancedStreamFeaturesLoaded = false; - } - if (advancedStreamFeaturesLoaded) { - enableAdvancedStreamFeatures(); - } - } - if (packet.getType() != IqPacket.TYPE.TIMEOUT) { - if (mPendingServiceDiscoveries.decrementAndGet() == 0 - && mWaitForDisco.compareAndSet(true, false)) { - finalizeBind(); - } - } - }); + }); } private void discoverMamPreferences() { IqPacket request = new IqPacket(IqPacket.TYPE.GET); request.addChild("prefs", MessageArchiveService.Version.MAM_2.namespace); - sendIqPacket(request, (account, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { - Element prefs = response.findChild("prefs", MessageArchiveService.Version.MAM_2.namespace); - isMamPreferenceAlways = "always".equals(prefs == null ? null : prefs.getAttribute("default")); - } - }); + sendIqPacket( + request, + (account, response) -> { + if (response.getType() == IqPacket.TYPE.RESULT) { + Element prefs = + response.findChild( + "prefs", MessageArchiveService.Version.MAM_2.namespace); + isMamPreferenceAlways = + "always" + .equals( + prefs == null + ? null + : prefs.getAttribute("default")); + } + }); } private void discoverCommands() { final IqPacket request = new IqPacket(IqPacket.TYPE.GET); request.setTo(account.getDomain()); request.addChild("query", Namespace.DISCO_ITEMS).setAttribute("node", Namespace.COMMANDS); - sendIqPacket(request, (account, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { - final Element query = response.findChild("query", Namespace.DISCO_ITEMS); - if (query == null) { - return; - } - final HashMap commands = new HashMap<>(); - for (final Element child : query.getChildren()) { - if ("item".equals(child.getName())) { - final String node = child.getAttribute("node"); - final Jid jid = child.getAttributeAsJid("jid"); - if (node != null && jid != null) { - commands.put(node, jid); + sendIqPacket( + request, + (account, response) -> { + if (response.getType() == IqPacket.TYPE.RESULT) { + final Element query = response.findChild("query", Namespace.DISCO_ITEMS); + if (query == null) { + return; + } + final HashMap commands = new HashMap<>(); + for (final Element child : query.getChildren()) { + if ("item".equals(child.getName())) { + final String node = child.getAttribute("node"); + final Jid jid = child.getAttributeAsJid("jid"); + if (node != null && jid != null) { + commands.put(node, jid); + } + } + } + Log.d(Config.LOGTAG, commands.toString()); + synchronized (this.commands) { + this.commands.clear(); + this.commands.putAll(commands); } } - } - Log.d(Config.LOGTAG, commands.toString()); - synchronized (this.commands) { - this.commands.clear(); - this.commands.putAll(commands); - } - } - }); + }); } public boolean isMamPreferenceAlways() { @@ -1497,7 +1750,9 @@ public class XmppConnection implements Runnable { } private void finalizeBind() { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": online with resource " + account.getResource()); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": online with resource " + account.getResource()); if (bindListener != null) { bindListener.onBind(account); } @@ -1507,9 +1762,11 @@ public class XmppConnection implements Runnable { private void enableAdvancedStreamFeatures() { if (getFeatures().blocking() && !features.blockListRequested) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": Requesting block list"); - this.sendIqPacket(getIqGenerator().generateGetBlockList(), mXmppConnectionService.getIqParser()); + this.sendIqPacket( + getIqGenerator().generateGetBlockList(), mXmppConnectionService.getIqParser()); } - for (final OnAdvancedStreamFeaturesLoaded listener : advancedStreamFeaturesLoadedListeners) { + for (final OnAdvancedStreamFeaturesLoaded listener : + advancedStreamFeaturesLoadedListeners) { listener.onAdvancedStreamFeaturesAvailable(account); } if (getFeatures().carbons() && !features.carbonsEnabled) { @@ -1525,46 +1782,60 @@ public class XmppConnection implements Runnable { final IqPacket iq = new IqPacket(IqPacket.TYPE.GET); iq.setTo(server.getDomain()); iq.query("http://jabber.org/protocol/disco#items"); - this.sendIqPacket(iq, (account, packet) -> { - if (packet.getType() == IqPacket.TYPE.RESULT) { - final HashSet items = new HashSet<>(); - final List elements = packet.query().getChildren(); - for (final Element element : elements) { - if (element.getName().equals("item")) { - final Jid jid = InvalidJid.getNullForInvalid(element.getAttributeAsJid("jid")); - if (jid != null && !jid.equals(account.getDomain())) { - items.add(jid); + this.sendIqPacket( + iq, + (account, packet) -> { + if (packet.getType() == IqPacket.TYPE.RESULT) { + final HashSet items = new HashSet<>(); + final List elements = packet.query().getChildren(); + for (final Element element : elements) { + if (element.getName().equals("item")) { + final Jid jid = + InvalidJid.getNullForInvalid( + element.getAttributeAsJid("jid")); + if (jid != null && !jid.equals(account.getDomain())) { + items.add(jid); + } + } } + for (Jid jid : items) { + sendServiceDiscoveryInfo(jid); + } + } else { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": could not query disco items of " + + server); } - } - for (Jid jid : items) { - sendServiceDiscoveryInfo(jid); - } - } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could not query disco items of " + server); - } - if (packet.getType() != IqPacket.TYPE.TIMEOUT) { - if (mPendingServiceDiscoveries.decrementAndGet() == 0 - && mWaitForDisco.compareAndSet(true, false)) { - finalizeBind(); - } - } - }); + if (packet.getType() != IqPacket.TYPE.TIMEOUT) { + if (mPendingServiceDiscoveries.decrementAndGet() == 0 + && mWaitForDisco.compareAndSet(true, false)) { + finalizeBind(); + } + } + }); } private void sendEnableCarbons() { final IqPacket iq = new IqPacket(IqPacket.TYPE.SET); iq.addChild("enable", "urn:xmpp:carbons:2"); - this.sendIqPacket(iq, (account, packet) -> { - if (!packet.hasChild("error")) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() - + ": successfully enabled carbons"); - features.carbonsEnabled = true; - } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() - + ": could not enable carbons " + packet); - } - }); + this.sendIqPacket( + iq, + (account, packet) -> { + if (!packet.hasChild("error")) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": successfully enabled carbons"); + features.carbonsEnabled = true; + } else { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": could not enable carbons " + + packet); + } + }); } private void processStreamError(final Tag currentTag) throws IOException { @@ -1574,7 +1845,12 @@ public class XmppConnection implements Runnable { } if (streamError.hasChild("conflict")) { account.setResource(createNewResource()); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": switching resource due to conflict (" + account.getResource() + ")"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": switching resource due to conflict (" + + account.getResource() + + ")"); throw new IOException(); } else if (streamError.hasChild("host-unknown")) { throw new StateChangingException(Account.State.HOST_UNKNOWN); @@ -1598,11 +1874,8 @@ public class XmppConnection implements Runnable { final MessagePacket packet = (MessagePacket) stanza; final String id = packet.getId(); final Jid to = packet.getTo(); - mXmppConnectionService.markMessage(account, - to.asBareJid(), - id, - Message.STATUS_SEND_FAILED, - error); + mXmppConnectionService.markMessage( + account, to.asBareJid(), id, Message.STATUS_SEND_FAILED, error); } } } @@ -1635,7 +1908,8 @@ public class XmppConnection implements Runnable { return this.sendUnmodifiedIqPacket(packet, callback, false); } - public synchronized String sendUnmodifiedIqPacket(final IqPacket packet, final OnIqPacketReceived callback, boolean force) { + public synchronized String sendUnmodifiedIqPacket( + final IqPacket packet, final OnIqPacketReceived callback, boolean force) { if (packet.getId() == null) { packet.setAttribute("id", nextRandomId()); } @@ -1670,7 +1944,11 @@ public class XmppConnection implements Runnable { if (force || isBound) { tagWriter.writeStanzaAsync(packet); } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + " do not write stanza to unbound stream " + packet.toString()); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + " do not write stanza to unbound stream " + + packet.toString()); } if (packet instanceof AbstractAcknowledgeableStanza) { AbstractAcknowledgeableStanza stanza = (AbstractAcknowledgeableStanza) packet; @@ -1686,9 +1964,13 @@ public class XmppConnection implements Runnable { this.mStanzaQueue.append(stanzasSent, stanza); if (stanza instanceof MessagePacket && stanza.getId() != null && inSmacksSession) { if (Config.EXTENDED_SM_LOGGING) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": requesting ack for message stanza #" + stanzasSent); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": requesting ack for message stanza #" + + stanzasSent); } - tagWriter.writeStanzaAsync(new RequestPacket(this.smVersion)); + tagWriter.writeStanzaAsync(new RequestPacket()); } } } @@ -1704,23 +1986,19 @@ public class XmppConnection implements Runnable { this.lastPingSent = SystemClock.elapsedRealtime(); } - public void setOnMessagePacketReceivedListener( - final OnMessagePacketReceived listener) { + public void setOnMessagePacketReceivedListener(final OnMessagePacketReceived listener) { this.messageListener = listener; } - public void setOnUnregisteredIqPacketReceivedListener( - final OnIqPacketReceived listener) { + public void setOnUnregisteredIqPacketReceivedListener(final OnIqPacketReceived listener) { this.unregisteredIqListener = listener; } - public void setOnPresencePacketReceivedListener( - final OnPresencePacketReceived listener) { + public void setOnPresencePacketReceivedListener(final OnPresencePacketReceived listener) { this.presenceListener = listener; } - public void setOnJinglePacketReceivedListener( - final OnJinglePacketReceived listener) { + public void setOnJinglePacketReceivedListener(final OnJinglePacketReceived listener) { this.jingleListener = listener; } @@ -1736,7 +2014,8 @@ public class XmppConnection implements Runnable { this.acknowledgedListener = listener; } - public void addOnAdvancedStreamFeaturesAvailableListener(final OnAdvancedStreamFeaturesLoaded listener) { + public void addOnAdvancedStreamFeaturesAvailableListener( + final OnAdvancedStreamFeaturesLoaded listener) { this.advancedStreamFeaturesLoadedListeners.add(listener); } @@ -1768,15 +2047,28 @@ public class XmppConnection implements Runnable { currentTagWriter.writeTag(Tag.end("stream:stream")); if (streamCountDownLatch != null) { if (streamCountDownLatch.await(1, TimeUnit.SECONDS)) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": remote ended stream"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": remote ended stream"); } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": remote has not closed socket. force closing"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": remote has not closed socket. force closing"); } } } catch (InterruptedException e) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": interrupted while gracefully closing stream"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": interrupted while gracefully closing stream"); } catch (final IOException e) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": io exception during disconnect (" + e.getMessage() + ")"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": io exception during disconnect (" + + e.getMessage() + + ")"); } finally { FileBackend.close(currentSocket); } @@ -1812,7 +2104,7 @@ public class XmppConnection implements Runnable { public boolean r() { if (getFeatures().sm()) { - this.tagWriter.writeStanzaAsync(new RequestPacket(smVersion)); + this.tagWriter.writeStanzaAsync(new RequestPacket()); return true; } else { return false; @@ -1847,9 +2139,11 @@ public class XmppConnection implements Runnable { } public int getTimeToNextAttempt() { - final int additionalTime = account.getLastErrorStatus() == Account.State.POLICY_VIOLATION ? 3 : 0; + final int additionalTime = + account.getLastErrorStatus() == Account.State.POLICY_VIOLATION ? 3 : 0; final int interval = Math.min((int) (25 * Math.pow(1.3, (additionalTime + attempt))), 300); - final int secondsSinceLast = (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000); + final int secondsSinceLast = + (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000); return interval - secondsSinceLast; } @@ -1908,7 +2202,9 @@ public class XmppConnection implements Runnable { return Identity.UNKNOWN; } for (final ServiceDiscoveryResult.Identity id : result.getIdentities()) { - if (id.getType().equals("im") && id.getCategory().equals("server") && id.getName() != null) { + if (id.getType().equals("im") + && id.getCategory().equals("server") + && id.getName() != null) { switch (id.getName()) { case "Prosody": return Identity.PROSODY; @@ -1961,7 +2257,7 @@ public class XmppConnection implements Runnable { @Override public String[] getClientAliases(String s, Principal[] principals) { final String alias = account.getPrivateKeyAlias(); - return alias != null ? new String[]{alias} : new String[0]; + return alias != null ? new String[] {alias} : new String[0]; } @Override @@ -2007,8 +2303,8 @@ public class XmppConnection implements Runnable { private boolean hasDiscoFeature(final Jid server, final String feature) { synchronized (XmppConnection.this.disco) { - return connection.disco.containsKey(server) && - connection.disco.get(server).getFeatures().contains(feature); + return connection.disco.containsKey(server) + && connection.disco.get(server).getFeatures().contains(feature); } } @@ -2027,11 +2323,13 @@ public class XmppConnection implements Runnable { } public boolean bookmarksConversion() { - return hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS_CONVERSION) && pepPublishOptions(); + return hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS_CONVERSION) + && pepPublishOptions(); } public boolean avatarConversion() { - return hasDiscoFeature(account.getJid().asBareJid(), Namespace.AVATAR_CONVERSION) && pepPublishOptions(); + return hasDiscoFeature(account.getJid().asBareJid(), Namespace.AVATAR_CONVERSION) + && pepPublishOptions(); } public boolean blocking() { @@ -2043,7 +2341,8 @@ public class XmppConnection implements Runnable { } public boolean flexibleOfflineMessageRetrieval() { - return hasDiscoFeature(account.getDomain(), Namespace.FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL); + return hasDiscoFeature( + account.getDomain(), Namespace.FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL); } public boolean register() { @@ -2051,16 +2350,19 @@ public class XmppConnection implements Runnable { } public boolean invite() { - return connection.streamFeatures != null && connection.streamFeatures.hasChild("register", Namespace.INVITE); + return connection.streamFeatures != null + && connection.streamFeatures.hasChild("register", Namespace.INVITE); } public boolean sm() { return streamId != null - || (connection.streamFeatures != null && connection.streamFeatures.hasChild("sm")); + || (connection.streamFeatures != null + && connection.streamFeatures.hasChild("sm")); } public boolean csi() { - return connection.streamFeatures != null && connection.streamFeatures.hasChild("csi", "urn:xmpp:csi:0"); + return connection.streamFeatures != null + && connection.streamFeatures.hasChild("csi", Namespace.CSI); } public boolean pep() { @@ -2073,7 +2375,9 @@ public class XmppConnection implements Runnable { public boolean pepPersistent() { synchronized (XmppConnection.this.disco) { ServiceDiscoveryResult info = disco.get(account.getJid().asBareJid()); - return info != null && info.getFeatures().contains("http://jabber.org/protocol/pubsub#persistent-items"); + return info != null + && info.getFeatures() + .contains("http://jabber.org/protocol/pubsub#persistent-items"); } } @@ -2082,7 +2386,8 @@ public class XmppConnection implements Runnable { } public boolean pepOmemoWhitelisted() { - return hasDiscoFeature(account.getJid().asBareJid(), AxolotlService.PEP_OMEMO_WHITELISTED); + return hasDiscoFeature( + account.getJid().asBareJid(), AxolotlService.PEP_OMEMO_WHITELISTED); } public boolean mam() { @@ -2111,15 +2416,29 @@ public class XmppConnection implements Runnable { if (Config.DISABLE_HTTP_UPLOAD) { return false; } else { - for (String namespace : new String[]{Namespace.HTTP_UPLOAD, Namespace.HTTP_UPLOAD_LEGACY}) { - List> items = findDiscoItemsByFeature(namespace); + for (String namespace : + new String[] {Namespace.HTTP_UPLOAD, Namespace.HTTP_UPLOAD_LEGACY}) { + List> items = + findDiscoItemsByFeature(namespace); if (items.size() > 0) { try { - long maxsize = Long.parseLong(items.get(0).getValue().getExtendedDiscoInformation(namespace, "max-file-size")); + long maxsize = + Long.parseLong( + items.get(0) + .getValue() + .getExtendedDiscoInformation( + namespace, "max-file-size")); if (filesize <= maxsize) { return true; } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": http upload is not available for files with size " + filesize + " (max is " + maxsize + ")"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": http upload is not available for files with size " + + filesize + + " (max is " + + maxsize + + ")"); return false; } } catch (Exception e) { @@ -2132,17 +2451,22 @@ public class XmppConnection implements Runnable { } public boolean useLegacyHttpUpload() { - return findDiscoItemByFeature(Namespace.HTTP_UPLOAD) == null && findDiscoItemByFeature(Namespace.HTTP_UPLOAD_LEGACY) != null; + return findDiscoItemByFeature(Namespace.HTTP_UPLOAD) == null + && findDiscoItemByFeature(Namespace.HTTP_UPLOAD_LEGACY) != null; } public long getMaxHttpUploadSize() { - for (String namespace : new String[]{Namespace.HTTP_UPLOAD, Namespace.HTTP_UPLOAD_LEGACY}) { + for (String namespace : + new String[] {Namespace.HTTP_UPLOAD, Namespace.HTTP_UPLOAD_LEGACY}) { List> items = findDiscoItemsByFeature(namespace); if (items.size() > 0) { try { - return Long.parseLong(items.get(0).getValue().getExtendedDiscoInformation(namespace, "max-file-size")); + return Long.parseLong( + items.get(0) + .getValue() + .getExtendedDiscoInformation(namespace, "max-file-size")); } catch (Exception e) { - //ignored + // ignored } } } @@ -2154,7 +2478,8 @@ public class XmppConnection implements Runnable { } public boolean bookmarks2() { - return Config.USE_BOOKMARKS2 /* || hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS2_COMPAT)*/; + return Config + .USE_BOOKMARKS2 /* || hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS2_COMPAT)*/; } public boolean externalServiceDiscovery() { diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/ActivePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/ActivePacket.java index 78ab66d8ff382a8b5c395f1c0ca350067cb09fa4..e1c465f726b44c02fc28648ed893fc1b45a50ae2 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/ActivePacket.java +++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/ActivePacket.java @@ -1,10 +1,11 @@ package eu.siacs.conversations.xmpp.stanzas.csi; +import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; public class ActivePacket extends AbstractStanza { public ActivePacket() { super("active"); - setAttribute("xmlns", "urn:xmpp:csi:0"); + setAttribute("xmlns", Namespace.CSI); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/InactivePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/InactivePacket.java index f109280f146aa842c596cc49ce70206f4c049c6a..1b74de066d17eb5a806506a65c3fff8addcd49e7 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/InactivePacket.java +++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/InactivePacket.java @@ -1,10 +1,11 @@ package eu.siacs.conversations.xmpp.stanzas.csi; +import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; public class InactivePacket extends AbstractStanza { public InactivePacket() { super("inactive"); - setAttribute("xmlns", "urn:xmpp:csi:0"); + setAttribute("xmlns", Namespace.CSI); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/AckPacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/AckPacket.java index f93b5d8705180564df5c0c0ca5b79153c97473f9..9e7b991a4b1c29e6399f6771398c635680cc016a 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/AckPacket.java +++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/AckPacket.java @@ -1,12 +1,13 @@ package eu.siacs.conversations.xmpp.stanzas.streammgmt; +import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; public class AckPacket extends AbstractStanza { - public AckPacket(int sequence, int smVersion) { + public AckPacket(final int sequence) { super("a"); - this.setAttribute("xmlns", "urn:xmpp:sm:" + smVersion); + this.setAttribute("xmlns", Namespace.STREAM_MANAGEMENT); this.setAttribute("h", Integer.toString(sequence)); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/EnablePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/EnablePacket.java index 78cd81edc3347e709e1c80c55e984278807fbb9a..95558b143230ba3e02e87544089c52c2b030581a 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/EnablePacket.java +++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/EnablePacket.java @@ -1,12 +1,13 @@ package eu.siacs.conversations.xmpp.stanzas.streammgmt; +import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; public class EnablePacket extends AbstractStanza { - public EnablePacket(int smVersion) { + public EnablePacket() { super("enable"); - this.setAttribute("xmlns", "urn:xmpp:sm:" + smVersion); + this.setAttribute("xmlns", Namespace.STREAM_MANAGEMENT); this.setAttribute("resume", "true"); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/RequestPacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/RequestPacket.java index 98cfc748b1b97117dd6497e3caf60d2b85536cc9..4e0e0f11aa192f97bd461fffcb7d762477d90e9d 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/RequestPacket.java +++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/RequestPacket.java @@ -1,12 +1,13 @@ package eu.siacs.conversations.xmpp.stanzas.streammgmt; +import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; public class RequestPacket extends AbstractStanza { - public RequestPacket(int smVersion) { + public RequestPacket() { super("r"); - this.setAttribute("xmlns", "urn:xmpp:sm:" + smVersion); + this.setAttribute("xmlns", Namespace.STREAM_MANAGEMENT); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/ResumePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/ResumePacket.java index 9cdcfa5ec57399583e8bacc17a82323043bfd5c2..38681d7c1a370957ad4a251d310279c71b387ac8 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/ResumePacket.java +++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/ResumePacket.java @@ -1,12 +1,13 @@ package eu.siacs.conversations.xmpp.stanzas.streammgmt; +import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; public class ResumePacket extends AbstractStanza { - public ResumePacket(String id, int sequence, int smVersion) { + public ResumePacket(final String id, final int sequence) { super("resume"); - this.setAttribute("xmlns", "urn:xmpp:sm:" + smVersion); + this.setAttribute("xmlns", Namespace.STREAM_MANAGEMENT); this.setAttribute("previd", id); this.setAttribute("h", Integer.toString(sequence)); } From eb49a7f5e57a627b5c4217e6848fc9135db2236a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 3 Sep 2022 12:33:27 +0200 Subject: [PATCH 019/101] fix crash in buggy connection manager. fixes #4368 --- .../services/XmppConnectionService.java | 6 ++++-- .../siacs/conversations/ui/XmppActivity.java | 13 ++---------- .../conversations/utils/Compatibility.java | 21 +++++++++++++++---- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index af68db19ca1589ef8749b660e9f795cc22c2e345..ba2c8514e8e554ce24ef0ebe4073cc14018979d1 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -945,9 +945,11 @@ public class XmppConnectionService extends Service { public boolean isDataSaverDisabled() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE); + final ConnectivityManager connectivityManager = + (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE); return !connectivityManager.isActiveNetworkMetered() - || connectivityManager.getRestrictBackgroundStatus() == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED; + || Compatibility.getRestrictBackgroundStatus(connectivityManager) + == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED; } else { return true; } diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java index 6ac8f7279de8a568c0906e881d6690e9be4dd5ff..644bd7ec55244059146ab2a1b1ef384d7672e564 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -80,6 +80,7 @@ import eu.siacs.conversations.ui.util.MenuDoubleTabUtil; import eu.siacs.conversations.ui.util.PresenceSelector; import eu.siacs.conversations.ui.util.SoftKeyboardUtils; import eu.siacs.conversations.utils.AccountUtils; +import eu.siacs.conversations.utils.Compatibility; import eu.siacs.conversations.utils.ExceptionHelper; import eu.siacs.conversations.ui.util.SettingsUtils; import eu.siacs.conversations.utils.ThemeHelper; @@ -448,22 +449,12 @@ public abstract class XmppActivity extends ActionBarActivity { final ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); return cm != null && cm.isActiveNetworkMetered() - && getRestrictBackgroundStatus(cm) == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED; + && Compatibility.getRestrictBackgroundStatus(cm) == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED; } else { return false; } } - @RequiresApi(api = Build.VERSION_CODES.N) - private static int getRestrictBackgroundStatus(@NonNull final ConnectivityManager connectivityManager) { - try { - return connectivityManager.getRestrictBackgroundStatus(); - } catch (final Exception e) { - Log.d(Config.LOGTAG,"platform bug detected. Unable to get restrict background status",e); - return -1; - } - } - private boolean usingEnterKey() { return getBooleanPreference("display_enter_key", R.bool.display_enter_key); } diff --git a/src/main/java/eu/siacs/conversations/utils/Compatibility.java b/src/main/java/eu/siacs/conversations/utils/Compatibility.java index c28b8fe29681302eabaf5efa59d8c6dbf6cc412b..b1145794d13eaf946dc9d7bc5fa7de2ac5e65348 100644 --- a/src/main/java/eu/siacs/conversations/utils/Compatibility.java +++ b/src/main/java/eu/siacs/conversations/utils/Compatibility.java @@ -8,6 +8,7 @@ import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; +import android.net.ConnectivityManager; import android.os.Build; import android.preference.Preference; import android.preference.PreferenceCategory; @@ -15,6 +16,8 @@ import android.preference.PreferenceManager; import android.util.Log; import androidx.annotation.BoolRes; +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; import androidx.core.content.ContextCompat; import java.util.Arrays; @@ -158,10 +161,20 @@ public class Compatibility { @SuppressLint("UnsupportedChromeOsCameraSystemFeature") public static boolean hasFeatureCamera(final Context context) { final PackageManager packageManager = context.getPackageManager(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - return packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY); - } else { - return packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA); + return packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY); + } + + @RequiresApi(api = Build.VERSION_CODES.N) + public static int getRestrictBackgroundStatus( + @NonNull final ConnectivityManager connectivityManager) { + try { + return connectivityManager.getRestrictBackgroundStatus(); + } catch (final Exception e) { + Log.d( + Config.LOGTAG, + "platform bug detected. Unable to get restrict background status", + e); + return ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED; } } } From e204457c319b6a4327ad7f4a1babb17a22c6f802 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 3 Sep 2022 15:51:12 +0200 Subject: [PATCH 020/101] show toast warning about unavailable calls when using tor closes #4103 --- .../conversations/ui/EditAccountActivity.java | 3 + .../conversations/ui/SettingsActivity.java | 868 ++++++++++-------- src/main/res/values/strings.xml | 1 + 3 files changed, 472 insertions(+), 400 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index fc21f62965a22906f54420b6232cced30e5990f7..19424ee2bdf81aa753528bd2b8bf88e205af9a7e 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -293,6 +293,9 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat } binding.hostnameLayout.setError(null); binding.portLayout.setError(null); + if (mAccount.isOnion()) { + Toast.makeText(EditAccountActivity.this, R.string.audio_video_disabled_tor, Toast.LENGTH_LONG).show(); + } if (mAccount.isEnabled() && !registerNewAccount && !mInitMode) { diff --git a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java index 7073b881dbf9085f100ad7891766ff3db36b47da..21d2b956c84a4a17ea1b04554f744e9840dd75d5 100644 --- a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java @@ -38,409 +38,477 @@ import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.services.ExportBackupService; import eu.siacs.conversations.services.MemorizingTrustManager; import eu.siacs.conversations.services.QuickConversationsService; +import eu.siacs.conversations.ui.util.SettingsUtils; import eu.siacs.conversations.ui.util.StyledAttributes; import eu.siacs.conversations.utils.GeoHelper; -import eu.siacs.conversations.ui.util.SettingsUtils; import eu.siacs.conversations.utils.TimeFrameUtils; import eu.siacs.conversations.xmpp.Jid; -public class SettingsActivity extends XmppActivity implements - OnSharedPreferenceChangeListener { - - public static final String KEEP_FOREGROUND_SERVICE = "enable_foreground_service"; - public static final String AWAY_WHEN_SCREEN_IS_OFF = "away_when_screen_off"; - public static final String TREAT_VIBRATE_AS_SILENT = "treat_vibrate_as_silent"; - public static final String DND_ON_SILENT_MODE = "dnd_on_silent_mode"; - public static final String MANUALLY_CHANGE_PRESENCE = "manually_change_presence"; - public static final String BLIND_TRUST_BEFORE_VERIFICATION = "btbv"; - public static final String AUTOMATIC_MESSAGE_DELETION = "automatic_message_deletion"; - public static final String BROADCAST_LAST_ACTIVITY = "last_activity"; - public static final String THEME = "theme"; - public static final String SHOW_DYNAMIC_TAGS = "show_dynamic_tags"; - public static final String OMEMO_SETTING = "omemo"; - public static final String PREVENT_SCREENSHOTS = "prevent_screenshots"; - - public static final int REQUEST_CREATE_BACKUP = 0xbf8701; - - private SettingsFragment mSettingsFragment; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_settings); - FragmentManager fm = getFragmentManager(); - mSettingsFragment = (SettingsFragment) fm.findFragmentById(R.id.settings_content); - if (mSettingsFragment == null || !mSettingsFragment.getClass().equals(SettingsFragment.class)) { - mSettingsFragment = new SettingsFragment(); - fm.beginTransaction().replace(R.id.settings_content, mSettingsFragment).commit(); - } - mSettingsFragment.setActivityIntent(getIntent()); - this.mTheme = findTheme(); - setTheme(this.mTheme); - getWindow().getDecorView().setBackgroundColor(StyledAttributes.getColor(this, R.attr.color_background_primary)); - setSupportActionBar(findViewById(R.id.toolbar)); - configureActionBar(getSupportActionBar()); - } - - @Override - void onBackendConnected() { - - } - - @Override - public void onStart() { - super.onStart(); - PreferenceManager.getDefaultSharedPreferences(this).registerOnSharedPreferenceChangeListener(this); - - changeOmemoSettingSummary(); - - if (QuickConversationsService.isQuicksy()) { - final PreferenceCategory connectionOptions = (PreferenceCategory) mSettingsFragment.findPreference("connection_options"); - final PreferenceCategory groupChats = (PreferenceCategory) mSettingsFragment.findPreference("group_chats"); - final Preference channelDiscoveryMethod = mSettingsFragment.findPreference("channel_discovery_method"); - PreferenceScreen expert = (PreferenceScreen) mSettingsFragment.findPreference("expert"); - if (connectionOptions != null) { - expert.removePreference(connectionOptions); - } - if (groupChats != null && channelDiscoveryMethod != null) { - groupChats.removePreference(channelDiscoveryMethod); - } - } - - PreferenceScreen mainPreferenceScreen = (PreferenceScreen) mSettingsFragment.findPreference("main_screen"); - - PreferenceCategory attachmentsCategory = (PreferenceCategory) mSettingsFragment.findPreference("attachments"); - CheckBoxPreference locationPlugin = (CheckBoxPreference) mSettingsFragment.findPreference("use_share_location_plugin"); - if (attachmentsCategory != null && locationPlugin != null) { - if (!GeoHelper.isLocationPluginInstalled(this)) { - attachmentsCategory.removePreference(locationPlugin); - } - } - - //this feature is only available on Huawei Android 6. - PreferenceScreen huaweiPreferenceScreen = (PreferenceScreen) mSettingsFragment.findPreference("huawei"); - if (huaweiPreferenceScreen != null) { - Intent intent = huaweiPreferenceScreen.getIntent(); - //remove when Api version is above M (Version 6.0) or if the intent is not callable - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M || !isCallable(intent)) { - PreferenceCategory generalCategory = (PreferenceCategory) mSettingsFragment.findPreference("general"); - generalCategory.removePreference(huaweiPreferenceScreen); - if (generalCategory.getPreferenceCount() == 0) { - if (mainPreferenceScreen != null) { - mainPreferenceScreen.removePreference(generalCategory); - } - } - } - } - - ListPreference automaticMessageDeletionList = (ListPreference) mSettingsFragment.findPreference(AUTOMATIC_MESSAGE_DELETION); - if (automaticMessageDeletionList != null) { - final int[] choices = getResources().getIntArray(R.array.automatic_message_deletion_values); - CharSequence[] entries = new CharSequence[choices.length]; - CharSequence[] entryValues = new CharSequence[choices.length]; - for (int i = 0; i < choices.length; ++i) { - entryValues[i] = String.valueOf(choices[i]); - if (choices[i] == 0) { - entries[i] = getString(R.string.never); - } else { - entries[i] = TimeFrameUtils.resolve(this, 1000L * choices[i]); - } - } - automaticMessageDeletionList.setEntries(entries); - automaticMessageDeletionList.setEntryValues(entryValues); - } - - - boolean removeLocation = new Intent("eu.siacs.conversations.location.request").resolveActivity(getPackageManager()) == null; - boolean removeVoice = new Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION).resolveActivity(getPackageManager()) == null; - - ListPreference quickAction = (ListPreference) mSettingsFragment.findPreference("quick_action"); - if (quickAction != null && (removeLocation || removeVoice)) { - ArrayList entries = new ArrayList<>(Arrays.asList(quickAction.getEntries())); - ArrayList entryValues = new ArrayList<>(Arrays.asList(quickAction.getEntryValues())); - int index = entryValues.indexOf("location"); - if (index > 0 && removeLocation) { - entries.remove(index); - entryValues.remove(index); - } - index = entryValues.indexOf("voice"); - if (index > 0 && removeVoice) { - entries.remove(index); - entryValues.remove(index); - } - quickAction.setEntries(entries.toArray(new CharSequence[entries.size()])); - quickAction.setEntryValues(entryValues.toArray(new CharSequence[entryValues.size()])); - } - - final Preference removeCertsPreference = mSettingsFragment.findPreference("remove_trusted_certificates"); - if (removeCertsPreference != null) { - removeCertsPreference.setOnPreferenceClickListener(preference -> { - final MemorizingTrustManager mtm = xmppConnectionService.getMemorizingTrustManager(); - final ArrayList aliases = Collections.list(mtm.getCertificates()); - if (aliases.size() == 0) { - displayToast(getString(R.string.toast_no_trusted_certs)); - return true; - } - final ArrayList selectedItems = new ArrayList<>(); - final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(SettingsActivity.this); - dialogBuilder.setTitle(getResources().getString(R.string.dialog_manage_certs_title)); - dialogBuilder.setMultiChoiceItems(aliases.toArray(new CharSequence[aliases.size()]), null, - (dialog, indexSelected, isChecked) -> { - if (isChecked) { - selectedItems.add(indexSelected); - } else if (selectedItems.contains(indexSelected)) { - selectedItems.remove(Integer.valueOf(indexSelected)); - } - ((AlertDialog) dialog).getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(selectedItems.size() > 0); - }); - - dialogBuilder.setPositiveButton( - getResources().getString(R.string.dialog_manage_certs_positivebutton), (dialog, which) -> { - int count = selectedItems.size(); - if (count > 0) { - for (int i = 0; i < count; i++) { - try { - Integer item = Integer.valueOf(selectedItems.get(i).toString()); - String alias = aliases.get(item); - mtm.deleteCertificate(alias); - } catch (KeyStoreException e) { - e.printStackTrace(); - displayToast("Error: " + e.getLocalizedMessage()); - } - } - if (xmppConnectionServiceBound) { - reconnectAccounts(); - } - displayToast(getResources().getQuantityString(R.plurals.toast_delete_certificates, count, count)); - } - }); - dialogBuilder.setNegativeButton(getResources().getString(R.string.dialog_manage_certs_negativebutton), null); - AlertDialog removeCertsDialog = dialogBuilder.create(); - removeCertsDialog.show(); - removeCertsDialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); - return true; - }); - } - - final Preference createBackupPreference = mSettingsFragment.findPreference("create_backup"); - if (createBackupPreference != null) { - createBackupPreference.setSummary(getString(R.string.pref_create_backup_summary, FileBackend.getBackupDirectory(this).getAbsolutePath())); - createBackupPreference.setOnPreferenceClickListener(preference -> { - if (hasStoragePermission(REQUEST_CREATE_BACKUP)) { - createBackup(); - } - return true; - }); - } - - if (Config.ONLY_INTERNAL_STORAGE) { - final Preference cleanCachePreference = mSettingsFragment.findPreference("clean_cache"); - if (cleanCachePreference != null) { - cleanCachePreference.setOnPreferenceClickListener(preference -> cleanCache()); - } - - final Preference cleanPrivateStoragePreference = mSettingsFragment.findPreference("clean_private_storage"); - if (cleanPrivateStoragePreference != null) { - cleanPrivateStoragePreference.setOnPreferenceClickListener(preference -> cleanPrivateStorage()); - } - } - - final Preference deleteOmemoPreference = mSettingsFragment.findPreference("delete_omemo_identities"); - if (deleteOmemoPreference != null) { - deleteOmemoPreference.setOnPreferenceClickListener(preference -> deleteOmemoIdentities()); - } - } - - private void changeOmemoSettingSummary() { - ListPreference omemoPreference = (ListPreference) mSettingsFragment.findPreference(OMEMO_SETTING); - if (omemoPreference != null) { - String value = omemoPreference.getValue(); - switch (value) { - case "always": - omemoPreference.setSummary(R.string.pref_omemo_setting_summary_always); - break; - case "default_on": - omemoPreference.setSummary(R.string.pref_omemo_setting_summary_default_on); - break; - case "default_off": - omemoPreference.setSummary(R.string.pref_omemo_setting_summary_default_off); - break; - } - } else { - Log.d(Config.LOGTAG,"unable to find preference named "+OMEMO_SETTING); - } - } - - private boolean isCallable(final Intent i) { - return i != null && getPackageManager().queryIntentActivities(i, PackageManager.MATCH_DEFAULT_ONLY).size() > 0; - } - - - private boolean cleanCache() { - Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS); - intent.setData(Uri.parse("package:" + getPackageName())); - startActivity(intent); - return true; - } - - private boolean cleanPrivateStorage() { - for(String type : Arrays.asList("Images", "Videos", "Files", "Recordings")) { - cleanPrivateFiles(type); - } - return true; - } - - private void cleanPrivateFiles(final String type) { - try { - File dir = new File(getFilesDir().getAbsolutePath(), "/" + type + "/"); - File[] array = dir.listFiles(); - if (array != null) { - for (int b = 0; b < array.length; b++) { - String name = array[b].getName().toLowerCase(); - if (name.equals(".nomedia")) { - continue; - } - if (array[b].isFile()) { - array[b].delete(); - } - } - } - } catch (Throwable e) { - Log.e("CleanCache", e.toString()); - } - } - - private boolean deleteOmemoIdentities() { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.pref_delete_omemo_identities); - final List accounts = new ArrayList<>(); - for (Account account : xmppConnectionService.getAccounts()) { - if (account.isEnabled()) { - accounts.add(account.getJid().asBareJid().toString()); - } - } - final boolean[] checkedItems = new boolean[accounts.size()]; - builder.setMultiChoiceItems(accounts.toArray(new CharSequence[accounts.size()]), checkedItems, (dialog, which, isChecked) -> { - checkedItems[which] = isChecked; - final AlertDialog alertDialog = (AlertDialog) dialog; - for (boolean item : checkedItems) { - if (item) { - alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(true); - return; - } - } - alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(false); - }); - builder.setNegativeButton(R.string.cancel, null); - builder.setPositiveButton(R.string.delete_selected_keys, (dialog, which) -> { - for (int i = 0; i < checkedItems.length; ++i) { - if (checkedItems[i]) { - try { - Jid jid = Jid.of(accounts.get(i).toString()); - Account account = xmppConnectionService.findAccountByJid(jid); - if (account != null) { - account.getAxolotlService().regenerateKeys(true); - } - } catch (IllegalArgumentException e) { - // - } - - } - } - }); - AlertDialog dialog = builder.create(); - dialog.show(); - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); - return true; - } - - @Override - public void onStop() { - super.onStop(); - PreferenceManager.getDefaultSharedPreferences(this) - .unregisterOnSharedPreferenceChangeListener(this); - } - - @Override - public void onSharedPreferenceChanged(SharedPreferences preferences, String name) { - final List resendPresence = Arrays.asList( - "confirm_messages", - DND_ON_SILENT_MODE, - AWAY_WHEN_SCREEN_IS_OFF, - "allow_message_correction", - TREAT_VIBRATE_AS_SILENT, - MANUALLY_CHANGE_PRESENCE, - BROADCAST_LAST_ACTIVITY); - if (name.equals(OMEMO_SETTING)) { - OmemoSetting.load(this, preferences); - changeOmemoSettingSummary(); - } else if (name.equals(KEEP_FOREGROUND_SERVICE)) { - xmppConnectionService.toggleForegroundService(); - } else if (resendPresence.contains(name)) { - if (xmppConnectionServiceBound) { - if (name.equals(AWAY_WHEN_SCREEN_IS_OFF) || name.equals(MANUALLY_CHANGE_PRESENCE)) { - xmppConnectionService.toggleScreenEventReceiver(); - } - xmppConnectionService.refreshAllPresences(); - } - } else if (name.equals("dont_trust_system_cas")) { - xmppConnectionService.updateMemorizingTrustmanager(); - reconnectAccounts(); - } else if (name.equals("use_tor")) { - reconnectAccounts(); - xmppConnectionService.reinitializeMuclumbusService(); - } else if (name.equals(AUTOMATIC_MESSAGE_DELETION)) { - xmppConnectionService.expireOldMessages(true); - } else if (name.equals(THEME)) { - final int theme = findTheme(); - if (this.mTheme != theme) { - recreate(); - } - } else if(name.equals(PREVENT_SCREENSHOTS)){ - SettingsUtils.applyScreenshotPreventionSetting(this); - } - } - - @Override - public void onResume(){ - super.onResume(); - SettingsUtils.applyScreenshotPreventionSetting(this); - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - if (grantResults.length > 0) - if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { - if (requestCode == REQUEST_CREATE_BACKUP) { - createBackup(); - } - } else { - Toast.makeText(this, getString(R.string.no_storage_permission, getString(R.string.app_name)), Toast.LENGTH_SHORT).show(); - } - } - - private void createBackup() { - ContextCompat.startForegroundService(this, new Intent(this, ExportBackupService.class)); - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setMessage(R.string.backup_started_message); - builder.setPositiveButton(R.string.ok, null); - builder.create().show(); - } - - private void displayToast(final String msg) { - runOnUiThread(() -> Toast.makeText(SettingsActivity.this, msg, Toast.LENGTH_LONG).show()); - } - - private void reconnectAccounts() { - for (Account account : xmppConnectionService.getAccounts()) { - if (account.isEnabled()) { - xmppConnectionService.reconnectAccountInBackground(account); - } - } - } - - public void refreshUiReal() { - //nothing to do. This Activity doesn't implement any listeners - } - +public class SettingsActivity extends XmppActivity implements OnSharedPreferenceChangeListener { + + public static final String KEEP_FOREGROUND_SERVICE = "enable_foreground_service"; + public static final String AWAY_WHEN_SCREEN_IS_OFF = "away_when_screen_off"; + public static final String TREAT_VIBRATE_AS_SILENT = "treat_vibrate_as_silent"; + public static final String DND_ON_SILENT_MODE = "dnd_on_silent_mode"; + public static final String MANUALLY_CHANGE_PRESENCE = "manually_change_presence"; + public static final String BLIND_TRUST_BEFORE_VERIFICATION = "btbv"; + public static final String AUTOMATIC_MESSAGE_DELETION = "automatic_message_deletion"; + public static final String BROADCAST_LAST_ACTIVITY = "last_activity"; + public static final String THEME = "theme"; + public static final String SHOW_DYNAMIC_TAGS = "show_dynamic_tags"; + public static final String OMEMO_SETTING = "omemo"; + public static final String PREVENT_SCREENSHOTS = "prevent_screenshots"; + + public static final int REQUEST_CREATE_BACKUP = 0xbf8701; + + private SettingsFragment mSettingsFragment; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_settings); + FragmentManager fm = getFragmentManager(); + mSettingsFragment = (SettingsFragment) fm.findFragmentById(R.id.settings_content); + if (mSettingsFragment == null + || !mSettingsFragment.getClass().equals(SettingsFragment.class)) { + mSettingsFragment = new SettingsFragment(); + fm.beginTransaction().replace(R.id.settings_content, mSettingsFragment).commit(); + } + mSettingsFragment.setActivityIntent(getIntent()); + this.mTheme = findTheme(); + setTheme(this.mTheme); + getWindow() + .getDecorView() + .setBackgroundColor( + StyledAttributes.getColor(this, R.attr.color_background_primary)); + setSupportActionBar(findViewById(R.id.toolbar)); + configureActionBar(getSupportActionBar()); + } + + @Override + void onBackendConnected() {} + + @Override + public void onStart() { + super.onStart(); + PreferenceManager.getDefaultSharedPreferences(this) + .registerOnSharedPreferenceChangeListener(this); + + changeOmemoSettingSummary(); + + if (QuickConversationsService.isQuicksy()) { + final PreferenceCategory connectionOptions = + (PreferenceCategory) mSettingsFragment.findPreference("connection_options"); + final PreferenceCategory groupChats = + (PreferenceCategory) mSettingsFragment.findPreference("group_chats"); + final Preference channelDiscoveryMethod = + mSettingsFragment.findPreference("channel_discovery_method"); + PreferenceScreen expert = (PreferenceScreen) mSettingsFragment.findPreference("expert"); + if (connectionOptions != null) { + expert.removePreference(connectionOptions); + } + if (groupChats != null && channelDiscoveryMethod != null) { + groupChats.removePreference(channelDiscoveryMethod); + } + } + + PreferenceScreen mainPreferenceScreen = + (PreferenceScreen) mSettingsFragment.findPreference("main_screen"); + + PreferenceCategory attachmentsCategory = + (PreferenceCategory) mSettingsFragment.findPreference("attachments"); + CheckBoxPreference locationPlugin = + (CheckBoxPreference) mSettingsFragment.findPreference("use_share_location_plugin"); + if (attachmentsCategory != null && locationPlugin != null) { + if (!GeoHelper.isLocationPluginInstalled(this)) { + attachmentsCategory.removePreference(locationPlugin); + } + } + + // this feature is only available on Huawei Android 6. + PreferenceScreen huaweiPreferenceScreen = + (PreferenceScreen) mSettingsFragment.findPreference("huawei"); + if (huaweiPreferenceScreen != null) { + Intent intent = huaweiPreferenceScreen.getIntent(); + // remove when Api version is above M (Version 6.0) or if the intent is not callable + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M || !isCallable(intent)) { + PreferenceCategory generalCategory = + (PreferenceCategory) mSettingsFragment.findPreference("general"); + generalCategory.removePreference(huaweiPreferenceScreen); + if (generalCategory.getPreferenceCount() == 0) { + if (mainPreferenceScreen != null) { + mainPreferenceScreen.removePreference(generalCategory); + } + } + } + } + + ListPreference automaticMessageDeletionList = + (ListPreference) mSettingsFragment.findPreference(AUTOMATIC_MESSAGE_DELETION); + if (automaticMessageDeletionList != null) { + final int[] choices = + getResources().getIntArray(R.array.automatic_message_deletion_values); + CharSequence[] entries = new CharSequence[choices.length]; + CharSequence[] entryValues = new CharSequence[choices.length]; + for (int i = 0; i < choices.length; ++i) { + entryValues[i] = String.valueOf(choices[i]); + if (choices[i] == 0) { + entries[i] = getString(R.string.never); + } else { + entries[i] = TimeFrameUtils.resolve(this, 1000L * choices[i]); + } + } + automaticMessageDeletionList.setEntries(entries); + automaticMessageDeletionList.setEntryValues(entryValues); + } + + boolean removeLocation = + new Intent("eu.siacs.conversations.location.request") + .resolveActivity(getPackageManager()) + == null; + boolean removeVoice = + new Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION) + .resolveActivity(getPackageManager()) + == null; + + ListPreference quickAction = + (ListPreference) mSettingsFragment.findPreference("quick_action"); + if (quickAction != null && (removeLocation || removeVoice)) { + ArrayList entries = + new ArrayList<>(Arrays.asList(quickAction.getEntries())); + ArrayList entryValues = + new ArrayList<>(Arrays.asList(quickAction.getEntryValues())); + int index = entryValues.indexOf("location"); + if (index > 0 && removeLocation) { + entries.remove(index); + entryValues.remove(index); + } + index = entryValues.indexOf("voice"); + if (index > 0 && removeVoice) { + entries.remove(index); + entryValues.remove(index); + } + quickAction.setEntries(entries.toArray(new CharSequence[entries.size()])); + quickAction.setEntryValues(entryValues.toArray(new CharSequence[entryValues.size()])); + } + + final Preference removeCertsPreference = + mSettingsFragment.findPreference("remove_trusted_certificates"); + if (removeCertsPreference != null) { + removeCertsPreference.setOnPreferenceClickListener( + preference -> { + final MemorizingTrustManager mtm = + xmppConnectionService.getMemorizingTrustManager(); + final ArrayList aliases = Collections.list(mtm.getCertificates()); + if (aliases.size() == 0) { + displayToast(getString(R.string.toast_no_trusted_certs)); + return true; + } + final ArrayList selectedItems = new ArrayList<>(); + final AlertDialog.Builder dialogBuilder = + new AlertDialog.Builder(SettingsActivity.this); + dialogBuilder.setTitle( + getResources().getString(R.string.dialog_manage_certs_title)); + dialogBuilder.setMultiChoiceItems( + aliases.toArray(new CharSequence[aliases.size()]), + null, + (dialog, indexSelected, isChecked) -> { + if (isChecked) { + selectedItems.add(indexSelected); + } else if (selectedItems.contains(indexSelected)) { + selectedItems.remove(Integer.valueOf(indexSelected)); + } + ((AlertDialog) dialog) + .getButton(DialogInterface.BUTTON_POSITIVE) + .setEnabled(selectedItems.size() > 0); + }); + + dialogBuilder.setPositiveButton( + getResources() + .getString(R.string.dialog_manage_certs_positivebutton), + (dialog, which) -> { + int count = selectedItems.size(); + if (count > 0) { + for (int i = 0; i < count; i++) { + try { + Integer item = + Integer.valueOf( + selectedItems.get(i).toString()); + String alias = aliases.get(item); + mtm.deleteCertificate(alias); + } catch (KeyStoreException e) { + e.printStackTrace(); + displayToast("Error: " + e.getLocalizedMessage()); + } + } + if (xmppConnectionServiceBound) { + reconnectAccounts(); + } + displayToast( + getResources() + .getQuantityString( + R.plurals.toast_delete_certificates, + count, + count)); + } + }); + dialogBuilder.setNegativeButton( + getResources() + .getString(R.string.dialog_manage_certs_negativebutton), + null); + AlertDialog removeCertsDialog = dialogBuilder.create(); + removeCertsDialog.show(); + removeCertsDialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); + return true; + }); + } + + final Preference createBackupPreference = mSettingsFragment.findPreference("create_backup"); + if (createBackupPreference != null) { + createBackupPreference.setSummary( + getString( + R.string.pref_create_backup_summary, + FileBackend.getBackupDirectory(this).getAbsolutePath())); + createBackupPreference.setOnPreferenceClickListener( + preference -> { + if (hasStoragePermission(REQUEST_CREATE_BACKUP)) { + createBackup(); + } + return true; + }); + } + + if (Config.ONLY_INTERNAL_STORAGE) { + final Preference cleanCachePreference = mSettingsFragment.findPreference("clean_cache"); + if (cleanCachePreference != null) { + cleanCachePreference.setOnPreferenceClickListener(preference -> cleanCache()); + } + + final Preference cleanPrivateStoragePreference = + mSettingsFragment.findPreference("clean_private_storage"); + if (cleanPrivateStoragePreference != null) { + cleanPrivateStoragePreference.setOnPreferenceClickListener( + preference -> cleanPrivateStorage()); + } + } + + final Preference deleteOmemoPreference = + mSettingsFragment.findPreference("delete_omemo_identities"); + if (deleteOmemoPreference != null) { + deleteOmemoPreference.setOnPreferenceClickListener( + preference -> deleteOmemoIdentities()); + } + } + + private void changeOmemoSettingSummary() { + ListPreference omemoPreference = + (ListPreference) mSettingsFragment.findPreference(OMEMO_SETTING); + if (omemoPreference != null) { + String value = omemoPreference.getValue(); + switch (value) { + case "always": + omemoPreference.setSummary(R.string.pref_omemo_setting_summary_always); + break; + case "default_on": + omemoPreference.setSummary(R.string.pref_omemo_setting_summary_default_on); + break; + case "default_off": + omemoPreference.setSummary(R.string.pref_omemo_setting_summary_default_off); + break; + } + } else { + Log.d(Config.LOGTAG, "unable to find preference named " + OMEMO_SETTING); + } + } + + private boolean isCallable(final Intent i) { + return i != null + && getPackageManager() + .queryIntentActivities(i, PackageManager.MATCH_DEFAULT_ONLY) + .size() + > 0; + } + + private boolean cleanCache() { + Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.setData(Uri.parse("package:" + getPackageName())); + startActivity(intent); + return true; + } + + private boolean cleanPrivateStorage() { + for (String type : Arrays.asList("Images", "Videos", "Files", "Recordings")) { + cleanPrivateFiles(type); + } + return true; + } + + private void cleanPrivateFiles(final String type) { + try { + File dir = new File(getFilesDir().getAbsolutePath(), "/" + type + "/"); + File[] array = dir.listFiles(); + if (array != null) { + for (int b = 0; b < array.length; b++) { + String name = array[b].getName().toLowerCase(); + if (name.equals(".nomedia")) { + continue; + } + if (array[b].isFile()) { + array[b].delete(); + } + } + } + } catch (Throwable e) { + Log.e("CleanCache", e.toString()); + } + } + + private boolean deleteOmemoIdentities() { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.pref_delete_omemo_identities); + final List accounts = new ArrayList<>(); + for (Account account : xmppConnectionService.getAccounts()) { + if (account.isEnabled()) { + accounts.add(account.getJid().asBareJid().toString()); + } + } + final boolean[] checkedItems = new boolean[accounts.size()]; + builder.setMultiChoiceItems( + accounts.toArray(new CharSequence[accounts.size()]), + checkedItems, + (dialog, which, isChecked) -> { + checkedItems[which] = isChecked; + final AlertDialog alertDialog = (AlertDialog) dialog; + for (boolean item : checkedItems) { + if (item) { + alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(true); + return; + } + } + alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(false); + }); + builder.setNegativeButton(R.string.cancel, null); + builder.setPositiveButton( + R.string.delete_selected_keys, + (dialog, which) -> { + for (int i = 0; i < checkedItems.length; ++i) { + if (checkedItems[i]) { + try { + Jid jid = Jid.of(accounts.get(i).toString()); + Account account = xmppConnectionService.findAccountByJid(jid); + if (account != null) { + account.getAxolotlService().regenerateKeys(true); + } + } catch (IllegalArgumentException e) { + // + } + } + } + }); + AlertDialog dialog = builder.create(); + dialog.show(); + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); + return true; + } + + @Override + public void onStop() { + super.onStop(); + PreferenceManager.getDefaultSharedPreferences(this) + .unregisterOnSharedPreferenceChangeListener(this); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences preferences, String name) { + final List resendPresence = + Arrays.asList( + "confirm_messages", + DND_ON_SILENT_MODE, + AWAY_WHEN_SCREEN_IS_OFF, + "allow_message_correction", + TREAT_VIBRATE_AS_SILENT, + MANUALLY_CHANGE_PRESENCE, + BROADCAST_LAST_ACTIVITY); + if (name.equals(OMEMO_SETTING)) { + OmemoSetting.load(this, preferences); + changeOmemoSettingSummary(); + } else if (name.equals(KEEP_FOREGROUND_SERVICE)) { + xmppConnectionService.toggleForegroundService(); + } else if (resendPresence.contains(name)) { + if (xmppConnectionServiceBound) { + if (name.equals(AWAY_WHEN_SCREEN_IS_OFF) || name.equals(MANUALLY_CHANGE_PRESENCE)) { + xmppConnectionService.toggleScreenEventReceiver(); + } + xmppConnectionService.refreshAllPresences(); + } + } else if (name.equals("dont_trust_system_cas")) { + xmppConnectionService.updateMemorizingTrustmanager(); + reconnectAccounts(); + } else if (name.equals("use_tor")) { + if (preferences.getBoolean(name, false)) { + displayToast(getString(R.string.audio_video_disabled_tor)); + } + reconnectAccounts(); + xmppConnectionService.reinitializeMuclumbusService(); + } else if (name.equals(AUTOMATIC_MESSAGE_DELETION)) { + xmppConnectionService.expireOldMessages(true); + } else if (name.equals(THEME)) { + final int theme = findTheme(); + if (this.mTheme != theme) { + recreate(); + } + } else if (name.equals(PREVENT_SCREENSHOTS)) { + SettingsUtils.applyScreenshotPreventionSetting(this); + } + } + + @Override + public void onResume() { + super.onResume(); + SettingsUtils.applyScreenshotPreventionSetting(this); + } + + @Override + public void onRequestPermissionsResult( + int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (grantResults.length > 0) + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + if (requestCode == REQUEST_CREATE_BACKUP) { + createBackup(); + } + } else { + Toast.makeText( + this, + getString( + R.string.no_storage_permission, + getString(R.string.app_name)), + Toast.LENGTH_SHORT) + .show(); + } + } + + private void createBackup() { + ContextCompat.startForegroundService(this, new Intent(this, ExportBackupService.class)); + final AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setMessage(R.string.backup_started_message); + builder.setPositiveButton(R.string.ok, null); + builder.create().show(); + } + + private void displayToast(final String msg) { + runOnUiThread(() -> Toast.makeText(SettingsActivity.this, msg, Toast.LENGTH_LONG).show()); + } + + private void reconnectAccounts() { + for (Account account : xmppConnectionService.getAccounts()) { + if (account.isEnabled()) { + xmppConnectionService.reconnectAccountInBackground(account); + } + } + } + + public void refreshUiReal() { + // nothing to do. This Activity doesn't implement any listeners + } } diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index cd4412588e1444dd20e7bec99c457a889cb4e4d4..f668e3f25d9fc767bdb839102bddcf0339b03f46 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -986,5 +986,6 @@ No XMPP address found Temporary authentication failure Delete avatar + Calls are disabled when using Tor From 052c58f3770e0702077221dbfa1913c15854519a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 3 Sep 2022 20:17:29 +0200 Subject: [PATCH 021/101] rudimentary bind 2 implementation --- .../conversations/parser/MessageParser.java | 6 +- .../eu/siacs/conversations/xml/Namespace.java | 3 +- .../conversations/xmpp/XmppConnection.java | 116 ++++++++++++++---- 3 files changed, 94 insertions(+), 31 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 50743312ca4fa0f03a43b17a97aaf147a63eac6e..76945c472d7573ff22145de8dc25f5c4793b7a9b 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -313,7 +313,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece private boolean handleErrorMessage(final Account account, final MessagePacket packet) { if (packet.getType() == MessagePacket.TYPE_ERROR) { if (packet.fromServer(account)) { - final Pair forwarded = packet.getForwardedMessagePacket("received", "urn:xmpp:carbons:2"); + final Pair forwarded = packet.getForwardedMessagePacket("received", Namespace.CARBONS); if (forwarded != null) { return handleErrorMessage(account, forwarded.first); } @@ -389,8 +389,8 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece return; } else if (original.fromServer(account)) { Pair f; - f = original.getForwardedMessagePacket("received", "urn:xmpp:carbons:2"); - f = f == null ? original.getForwardedMessagePacket("sent", "urn:xmpp:carbons:2") : f; + f = original.getForwardedMessagePacket("received", Namespace.CARBONS); + f = f == null ? original.getForwardedMessagePacket("sent", Namespace.CARBONS) : f; packet = f != null ? f.first : original; if (handleErrorMessage(account, packet)) { return; diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index e28e69add7008fefb453bfcb7e3de486f7edcc52..a4fd8c06393d993587f5455606f428e63e80cf20 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -25,9 +25,10 @@ public final class Namespace { public static final String NICK = "http://jabber.org/protocol/nick"; public static final String FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL = "http://jabber.org/protocol/offline"; public static final String BIND = "urn:ietf:params:xml:ns:xmpp-bind"; - public static final String BIND2 = "urn:xmpp:bind2:0"; + public static final String BIND2 = "urn:xmpp:bind2:1"; public static final String STREAM_MANAGEMENT = "urn:xmpp:sm:3"; public static final String CSI = "urn:xmpp:csi:0"; + public static final String CARBONS = "urn:xmpp:carbons:2"; public static final String BOOKMARKS_CONVERSION = "urn:xmpp:bookmarks-conversion:0"; public static final String BOOKMARKS = "storage:bookmarks"; public static final String SYNCHRONIZATION = "im.quicksy.synchronization:0"; diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 6efbfbf152fdd121da5e617aa6c142766a71c28f..525151d62206547fa67a13ead3ff6c2b397cf46c 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -13,6 +13,7 @@ import android.util.SparseArray; import androidx.annotation.NonNull; import com.google.common.base.Strings; +import com.google.common.collect.Collections2; import org.xmlpull.v1.XmlPullParserException; @@ -32,6 +33,7 @@ import java.security.PrivateKey; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -274,7 +276,7 @@ public class XmppConnection implements Runnable { this.attempt++; this.verifiedHostname = null; // will be set if user entered hostname is being used or hostname was verified - // with dnssec + // with dnssec try { Socket localSocket; shouldAuthenticate = !account.isOptionSet(Account.OPTION_REGISTER); @@ -409,7 +411,7 @@ public class XmppConnection implements Runnable { if (startXmpp(localSocket)) { localSocket.setSoTimeout( 0); // reset to 0; once the connection is established we don’t - // want this + // want this if (!hardcoded && !result.equals(storedBackupResult)) { mXmppConnectionService.databaseBackend.saveResolverResult( domain, result); @@ -615,24 +617,9 @@ public class XmppConnection implements Runnable { throw new StateChangingException(Account.State.UNAUTHORIZED); } tagWriter.writeElement(response); - } else if (nextTag.isStart("enabled")) { + } else if (nextTag.isStart("enabled", Namespace.STREAM_MANAGEMENT)) { final Element enabled = tagReader.readElement(nextTag); - if (enabled.getAttributeAsBoolean("resume")) { - this.streamId = enabled.getAttribute("id"); - Log.d( - Config.LOGTAG, - account.getJid().asBareJid().toString() - + ": stream management enabled (resumable)"); - } else { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid().toString() - + ": stream management enabled"); - } - this.stanzasReceived = 0; - this.inSmacksSession = true; - final RequestPacket r = new RequestPacket(); - tagWriter.writeStanzaAsync(r); + processEnabled(enabled); } else if (nextTag.isStart("resumed")) { final Element resumed = tagReader.readElement(nextTag); processResumed(resumed); @@ -771,13 +758,31 @@ public class XmppConnection implements Runnable { + ": jid changed during SASL 2.0. updating database"); mXmppConnectionService.databaseBackend.updateAccount(account); } + final Element bound = success.findChild("bound", Namespace.BIND2); final Element resumed = success.findChild("resumed", "urn:xmpp:sm:3"); final Element failed = success.findChild("failed", "urn:xmpp:sm:3"); + // TODO check if resumed and bound exist and throw bind failure if (resumed != null && streamId != null) { processResumed(resumed); } else if (failed != null) { processFailed(failed, false); // wait for new stream features } + if (bound != null) { + this.isBound = true; + final Element streamManagementEnabled = + bound.findChild("enabled", Namespace.STREAM_MANAGEMENT); + final Element carbonsEnabled = bound.findChild("enabled", Namespace.CARBONS); + if (streamManagementEnabled != null) { + processEnabled(streamManagementEnabled); + } + if (carbonsEnabled != null) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": successfully enabled carbons"); + features.carbonsEnabled = true; + } + sendPostBindInitialization(streamManagementEnabled != null, carbonsEnabled != null); + } } if (version == SaslMechanism.Version.SASL) { tagReader.reset(); @@ -794,6 +799,27 @@ public class XmppConnection implements Runnable { } } + private void processEnabled(final Element enabled) { + final String streamId; + if (enabled.getAttributeAsBoolean("resume")) { + streamId = enabled.getAttribute("id"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid().toString() + + ": stream management enabled (resumable)"); + } else { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid().toString() + ": stream management enabled"); + streamId = null; + } + this.streamId = streamId; + this.stanzasReceived = 0; + this.inSmacksSession = true; + final RequestPacket r = new RequestPacket(); + // tagWriter.writeStanzaAsync(r); + } + private void processResumed(final Element resumed) throws StateChangingException { this.inSmacksSession = true; this.isBound = true; @@ -1241,6 +1267,16 @@ public class XmppConnection implements Runnable { final boolean inlineStreamManagement = inline != null && inline.hasChild("sm", "urn:xmpp:sm:3"); final boolean inlineBind2 = inline != null && inline.hasChild("bind", Namespace.BIND2); + final Element inlineBindFeatures = + this.streamFeatures.findChild("inline", Namespace.BIND2); + if (inlineBind2 && inlineBindFeatures != null) { + final Element bind = + generateBindRequest( + Collections2.transform( + inlineBindFeatures.getChildren(), + c -> c == null ? null : c.getAttribute("var"))); + authenticate.addChild(bind); + } if (inlineStreamManagement && streamId != null) { final ResumePacket resume = new ResumePacket(this.streamId, stanzasReceived); this.mSmCatchupMessageCounter.set(0); @@ -1259,9 +1295,26 @@ public class XmppConnection implements Runnable { + "/" + saslMechanism.getMechanism()); authenticate.setAttribute("mechanism", saslMechanism.getMechanism()); + Log.d(Config.LOGTAG, "authenticate " + authenticate); tagWriter.writeElement(authenticate); } + private Element generateBindRequest(final Collection bindFeatures) { + Log.d(Config.LOGTAG, "inline bind features: " + bindFeatures); + final Element bind = new Element("bind", Namespace.BIND2); + final Element clientId = bind.addChild("client-id"); + clientId.setAttribute("tag", mXmppConnectionService.getString(R.string.app_name)); + clientId.setContent(account.getUuid()); + final Element features = bind.addChild("features"); + if (bindFeatures.contains(Namespace.CARBONS)) { + features.addChild("enable", Namespace.CARBONS); + } + if (bindFeatures.contains(Namespace.STREAM_MANAGEMENT)) { + features.addChild("enable", Namespace.STREAM_MANAGEMENT); + } + return bind; + } + private static List extractMechanisms(final Element stream) { final ArrayList mechanisms = new ArrayList<>(stream.getChildren().size()); for (final Element child : stream.getChildren()) { @@ -1469,7 +1522,8 @@ public class XmppConnection implements Runnable { .hasChild("optional")) { sendStartSession(); } else { - sendPostBindInitialization(); + final boolean waitForDisco = enableStreamManagement(); + sendPostBindInitialization(waitForDisco, false); } return; } catch (final IllegalArgumentException e) { @@ -1565,7 +1619,8 @@ public class XmppConnection implements Runnable { startSession, (account, packet) -> { if (packet.getType() == IqPacket.TYPE.RESULT) { - sendPostBindInitialization(); + final boolean waitForDisco = enableStreamManagement(); + sendPostBindInitialization(waitForDisco, false); } else if (packet.getType() != IqPacket.TYPE.TIMEOUT) { throw new StateChangingError(Account.State.SESSION_FAILURE); } @@ -1573,7 +1628,7 @@ public class XmppConnection implements Runnable { true); } - private void sendPostBindInitialization() { + private boolean enableStreamManagement() { final boolean streamManagement = this.streamFeatures.hasChild("sm", Namespace.STREAM_MANAGEMENT); if (streamManagement) { @@ -1583,15 +1638,22 @@ public class XmppConnection implements Runnable { stanzasSent = 0; mStanzaQueue.clear(); } + return true; + } else { + return false; } - features.carbonsEnabled = false; + } + + private void sendPostBindInitialization( + final boolean waitForDisco, final boolean carbonsEnabled) { + features.carbonsEnabled = carbonsEnabled; features.blockListRequested = false; synchronized (this.disco) { this.disco.clear(); } Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": starting service discovery"); mPendingServiceDiscoveries.set(0); - if (!streamManagement + if (!waitForDisco || Patches.DISCO_EXCEPTIONS.contains( account.getJid().getDomain().toEscapedString())) { Log.d( @@ -1819,11 +1881,11 @@ public class XmppConnection implements Runnable { private void sendEnableCarbons() { final IqPacket iq = new IqPacket(IqPacket.TYPE.SET); - iq.addChild("enable", "urn:xmpp:carbons:2"); + iq.addChild("enable", Namespace.CARBONS); this.sendIqPacket( iq, (account, packet) -> { - if (!packet.hasChild("error")) { + if (packet.getType() == IqPacket.TYPE.RESULT) { Log.d( Config.LOGTAG, account.getJid().asBareJid() + ": successfully enabled carbons"); @@ -2309,7 +2371,7 @@ public class XmppConnection implements Runnable { } public boolean carbons() { - return hasDiscoFeature(account.getDomain(), "urn:xmpp:carbons:2"); + return hasDiscoFeature(account.getDomain(), Namespace.CARBONS); } public boolean commands() { From e0bd1d168c6937201f4076db0db8b9a6b436c204 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 4 Sep 2022 09:28:00 +0200 Subject: [PATCH 022/101] do not attempt resume when already in smacks session --- .../siacs/conversations/utils/XmlHelper.java | 47 ++++++++++--------- .../conversations/xmpp/XmppConnection.java | 14 +++--- 2 files changed, 31 insertions(+), 30 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/utils/XmlHelper.java b/src/main/java/eu/siacs/conversations/utils/XmlHelper.java index 4964bd5efed85e0ab3294369361a59b40cb892ee..7287297e32c7a8a042312de31225605449c47017 100644 --- a/src/main/java/eu/siacs/conversations/utils/XmlHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/XmlHelper.java @@ -1,30 +1,31 @@ package eu.siacs.conversations.utils; +import com.google.common.base.Joiner; +import com.google.common.collect.Lists; + +import java.util.Collections; +import java.util.List; + import eu.siacs.conversations.xml.Element; public class XmlHelper { - public static String encodeEntities(String content) { - content = content.replace("&", "&"); - content = content.replace("<", "<"); - content = content.replace(">", ">"); - content = content.replace("\"", """); - content = content.replace("'", "'"); - content = content.replaceAll("[\\p{Cntrl}&&[^\n\t\r]]", ""); - return content; - } + public static String encodeEntities(String content) { + content = content.replace("&", "&"); + content = content.replace("<", "<"); + content = content.replace(">", ">"); + content = content.replace("\"", """); + content = content.replace("'", "'"); + content = content.replaceAll("[\\p{Cntrl}&&[^\n\t\r]]", ""); + return content; + } - public static String printElementNames(final Element element) { - final StringBuilder builder = new StringBuilder(); - builder.append('['); - if (element != null) { - for (Element child : element.getChildren()) { - if (builder.length() != 1) { - builder.append(','); - } - builder.append(child.getName()); - } - } - builder.append(']'); - return builder.toString(); - } + public static String printElementNames(final Element element) { + final List features = + element == null + ? Collections.emptyList() + : Lists.transform( + element.getChildren(), + child -> child != null ? child.getName() : null); + return Joiner.on(", ").join(features); + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 525151d62206547fa67a13ead3ff6c2b397cf46c..64ffa4e12334cbad4578b1871981baf17dd96310 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -817,7 +817,7 @@ public class XmppConnection implements Runnable { this.stanzasReceived = 0; this.inSmacksSession = true; final RequestPacket r = new RequestPacket(); - // tagWriter.writeStanzaAsync(r); + tagWriter.writeStanzaAsync(r); } private void processResumed(final Element resumed) throws StateChangingException { @@ -1180,7 +1180,8 @@ public class XmppConnection implements Runnable { && isSecure) { authenticate(SaslMechanism.Version.SASL); } else if (this.streamFeatures.hasChild("sm", Namespace.STREAM_MANAGEMENT) - && streamId != null) { + && streamId != null + && !inSmacksSession) { if (Config.EXTENDED_SM_LOGGING) { Log.d( Config.LOGTAG, @@ -1208,7 +1209,7 @@ public class XmppConnection implements Runnable { Config.LOGTAG, account.getJid().asBareJid() + ": received NOP stream features " - + this.streamFeatures); + + XmlHelper.printElementNames(this.streamFeatures)); } } @@ -1295,7 +1296,6 @@ public class XmppConnection implements Runnable { + "/" + saslMechanism.getMechanism()); authenticate.setAttribute("mechanism", saslMechanism.getMechanism()); - Log.d(Config.LOGTAG, "authenticate " + authenticate); tagWriter.writeElement(authenticate); } @@ -1310,7 +1310,7 @@ public class XmppConnection implements Runnable { features.addChild("enable", Namespace.CARBONS); } if (bindFeatures.contains(Namespace.STREAM_MANAGEMENT)) { - features.addChild("enable", Namespace.STREAM_MANAGEMENT); + features.addChild(new EnablePacket()); } return bind; } @@ -2365,8 +2365,8 @@ public class XmppConnection implements Runnable { private boolean hasDiscoFeature(final Jid server, final String feature) { synchronized (XmppConnection.this.disco) { - return connection.disco.containsKey(server) - && connection.disco.get(server).getFeatures().contains(feature); + final ServiceDiscoveryResult sdr = connection.disco.get(server); + return sdr != null && sdr.getFeatures().contains(feature); } } From eee14a822a7f4657ccf4b1c586473ad49667eebe Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 5 Sep 2022 11:07:25 +0200 Subject: [PATCH 023/101] add todos --- src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 64ffa4e12334cbad4578b1871981baf17dd96310..70bc347b380ce63a6138761df8a08230d856d241 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -718,6 +718,7 @@ public class XmppConnection implements Runnable { Log.d( Config.LOGTAG, account.getJid().asBareJid().toString() + ": logged in (using " + version + ")"); + //TODO store mechanism name account.setKey(Account.PINNED_MECHANISM_KEY, String.valueOf(saslMechanism.getPriority())); if (version == SaslMechanism.Version.SASL_2) { final String authorizationIdentifier = @@ -781,6 +782,7 @@ public class XmppConnection implements Runnable { account.getJid().asBareJid() + ": successfully enabled carbons"); features.carbonsEnabled = true; } + //TODO if both are set mark account ready for pipelining sendPostBindInitialization(streamManagementEnabled != null, carbonsEnabled != null); } } From 22f41292620e83c0fbc61621124df96a945a2dd6 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 5 Sep 2022 12:17:13 +0200 Subject: [PATCH 024/101] increase quoting depth to 2 --- src/main/java/eu/siacs/conversations/Config.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index c89ce81bb1a0906dd7066e516dda8e8938ff7894..377be3ba1caf469b1308b75d256772bbfe89740f 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -205,5 +205,5 @@ public final class Config { // How deep nested quotes should be displayed. '2' means one quote nested in another. public static final int QUOTE_MAX_DEPTH = 7; // How deep nested quotes should be created on quoting a message. - public static final int QUOTING_MAX_DEPTH = 1; + public static final int QUOTING_MAX_DEPTH = 2; } From 562ffd200332b0537dbc8108d5a18bb34b7eb7bc Mon Sep 17 00:00:00 2001 From: Millesimus <32270710+Millesimus@users.noreply.github.com> Date: Mon, 5 Sep 2022 12:17:57 +0200 Subject: [PATCH 025/101] preserve new lines when quoting. fixes #3876 --- .../java/eu/siacs/conversations/ui/util/QuoteHelper.java | 8 ++++---- .../eu/siacs/conversations/ui/widget/EditMessage.java | 8 +++++++- .../java/eu/siacs/conversations/utils/MessageUtils.java | 6 +----- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/util/QuoteHelper.java b/src/main/java/eu/siacs/conversations/ui/util/QuoteHelper.java index cf49be76703dcefb04958c979c4fc53c15b51b2e..c2a69e6074f9506232f4a47ad2156ddc8e45cbfa 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/QuoteHelper.java +++ b/src/main/java/eu/siacs/conversations/ui/util/QuoteHelper.java @@ -84,13 +84,13 @@ public class QuoteHelper { if (isPositionQuoteStart(line, 0)) { int nestingDepth = 1; for (int i = 1; i < line.length(); i++) { - if (isPositionQuoteStart(line, i)) { + if (isPositionQuoteCharacter(line, i)) { nestingDepth++; - } - if (nestingDepth > (Config.QUOTING_MAX_DEPTH - 1)) { - return true; + } else if (line.charAt(i) != ' ') { + break; } } + return nestingDepth >= (Config.QUOTING_MAX_DEPTH); } return false; } diff --git a/src/main/java/eu/siacs/conversations/ui/widget/EditMessage.java b/src/main/java/eu/siacs/conversations/ui/widget/EditMessage.java index e890e5984c54cb28d9b8c1d71ebf9d1e025f2828..455c3ba440589cebdbb19f2ee7f318e4cf21a6ec 100644 --- a/src/main/java/eu/siacs/conversations/ui/widget/EditMessage.java +++ b/src/main/java/eu/siacs/conversations/ui/widget/EditMessage.java @@ -145,7 +145,13 @@ public class EditMessage extends AppCompatEditText { public void insertAsQuote(String text) { text = QuoteHelper.replaceAltQuoteCharsInText(text); - text = text.replaceAll("(\n *){2,}", "\n").replaceAll("(^|\n)(" + QuoteHelper.QUOTE_CHAR + ")", "$1$2$2").replaceAll("(^|\n)([^" + QuoteHelper.QUOTE_CHAR + "])", "$1> $2").replaceAll("\n$", ""); + text = text + // first replace all '>' at the beginning of the line with nice and tidy '>>' + // for nested quoting + .replaceAll("(^|\n)(" + QuoteHelper.QUOTE_CHAR + ")", "$1$2$2") + // then find all other lines and have them start with a '> ' + .replaceAll("(^|\n)(?!" + QuoteHelper.QUOTE_CHAR + ")(.*)", "$1> $2") + ; Editable editable = getEditableText(); int position = getSelectionEnd(); if (position == -1) position = editable.length(); diff --git a/src/main/java/eu/siacs/conversations/utils/MessageUtils.java b/src/main/java/eu/siacs/conversations/utils/MessageUtils.java index 0a11cd720412476334f94f8ae127b89a11f4ddfd..9687a7b14ba5a087db88813f69bfd08ea1d7aaab 100644 --- a/src/main/java/eu/siacs/conversations/utils/MessageUtils.java +++ b/src/main/java/eu/siacs/conversations/utils/MessageUtils.java @@ -66,11 +66,7 @@ public class MessageUtils { body = message.getMergedBody().toString(); } for (String line : body.split("\n")) { - if (line.length() <= 0) { - continue; - } - final char c = line.charAt(0); - if (QuoteHelper.isNestedTooDeeply(line)) { + if (!(line.length() <= 0) && QuoteHelper.isNestedTooDeeply(line)) { continue; } if (builder.length() != 0) { From 511dfa13c49347d3c5640c5cacebb68057361a2f Mon Sep 17 00:00:00 2001 From: Licaon_Kter Date: Mon, 5 Sep 2022 13:45:49 +0000 Subject: [PATCH 026/101] Fastlane description, remove fee (#4372) --- fastlane/metadata/android/en-US/full_description.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index e3b806b02b8a9a361ba5c6902d5a5d2ea7333388..e30ca25c1ab6358fbc2fc3aa8758bd8c17ece282 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -20,7 +20,7 @@ Features: * Multiple accounts / unified inbox * Very low impact on battery life -Conversations makes it very easy to create an account on the conversations.im server. Using that server comes with an annual fee of 8 Euro after a 6 month trial period. However Conversations will work with any other XMPP server as well. A lot of XMPP servers are run by volunteers and are free of charge. +Conversations makes it very easy to create an account on the free conversations.im server. However Conversations will work with any other XMPP server as well. A lot of XMPP servers are run by volunteers and are free of charge. XMPP Features: From a210568a9ce00cbafc36563b0aa70bf10ef90047 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 6 Sep 2022 09:25:09 +0200 Subject: [PATCH 027/101] refactor SASL choice into factory; remove unused TagWriter --- .../crypto/axolotl/AxolotlService.java | 4 +- .../conversations/crypto/sasl/Anonymous.java | 7 +- .../conversations/crypto/sasl/DigestMd5.java | 72 +++++--- .../conversations/crypto/sasl/External.java | 10 +- .../conversations/crypto/sasl/Plain.java | 15 +- .../crypto/sasl/SaslMechanism.java | 102 +++++++---- .../crypto/sasl/ScramMechanism.java | 168 ++++++++++-------- .../conversations/crypto/sasl/ScramSha1.java | 11 +- .../crypto/sasl/ScramSha256.java | 11 +- .../crypto/sasl/ScramSha512.java | 11 +- .../conversations/crypto/sasl/Tokenizer.java | 17 +- .../http/HttpConnectionManager.java | 4 +- .../http/HttpUploadConnection.java | 4 +- .../services/MessageArchiveService.java | 4 +- .../services/XmppConnectionService.java | 11 +- .../ui/CreatePublicChannelDialog.java | 3 +- .../conversations/utils/CryptoHelper.java | 14 +- .../eu/siacs/conversations/utils/Random.java | 13 ++ .../conversations/xmpp/XmppConnection.java | 38 ++-- 19 files changed, 288 insertions(+), 231 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/utils/Random.java diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index faef2e098dfa2ff9d501bc278625759575183740..3d4f23360096f97a45ba7354dccbd672a4d586d0 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.crypto.axolotl; +import static eu.siacs.conversations.utils.Random.SECURE_RANDOM; + import android.os.Bundle; import android.security.KeyChain; import android.util.Log; @@ -499,7 +501,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { PrivateKey x509PrivateKey = KeyChain.getPrivateKey(mXmppConnectionService, account.getPrivateKeyAlias()); X509Certificate[] chain = KeyChain.getCertificateChain(mXmppConnectionService, account.getPrivateKeyAlias()); Signature verifier = Signature.getInstance("sha256WithRSA"); - verifier.initSign(x509PrivateKey, mXmppConnectionService.getRNG()); + verifier.initSign(x509PrivateKey, SECURE_RANDOM); verifier.update(axolotlPublicKey.serialize()); byte[] signature = verifier.sign(); IqPacket packet = mXmppConnectionService.getIqGenerator().publishVerification(signature, chain, getOwnDeviceId()); diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/Anonymous.java b/src/main/java/eu/siacs/conversations/crypto/sasl/Anonymous.java index a9abb2bf84f485e7f25e7fd8d30feae9fc471cad..22cf80e6546a33734c9c69fa43fc57638a1fe881 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/Anonymous.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/Anonymous.java @@ -1,16 +1,13 @@ package eu.siacs.conversations.crypto.sasl; -import java.security.SecureRandom; - import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.xml.TagWriter; public class Anonymous extends SaslMechanism { public static final String MECHANISM = "ANONYMOUS"; - public Anonymous(TagWriter tagWriter, Account account, SecureRandom rng) { - super(tagWriter, account, rng); + public Anonymous(final Account account) { + super(account); } @Override diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/DigestMd5.java b/src/main/java/eu/siacs/conversations/crypto/sasl/DigestMd5.java index 74d4463d5d60afc4f21ca9a0e58919f86c00141a..7229299effa6e7ecb78ee4c599b1bb10dc46a74a 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/DigestMd5.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/DigestMd5.java @@ -5,18 +5,17 @@ import android.util.Base64; import java.nio.charset.Charset; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.utils.CryptoHelper; -import eu.siacs.conversations.xml.TagWriter; public class DigestMd5 extends SaslMechanism { public static final String MECHANISM = "DIGEST-MD5"; + private State state = State.INITIAL; - public DigestMd5(final TagWriter tagWriter, final Account account, final SecureRandom rng) { - super(tagWriter, account, rng); + public DigestMd5(final Account account) { + super(account); } @Override @@ -29,8 +28,6 @@ public class DigestMd5 extends SaslMechanism { return MECHANISM; } - private State state = State.INITIAL; - @Override public String getResponse(final String challenge) throws AuthenticationException { switch (state) { @@ -38,7 +35,8 @@ public class DigestMd5 extends SaslMechanism { state = State.RESPONSE_SENT; final String encodedResponse; try { - final Tokenizer tokenizer = new Tokenizer(Base64.decode(challenge, Base64.DEFAULT)); + final Tokenizer tokenizer = + new Tokenizer(Base64.decode(challenge, Base64.DEFAULT)); String nonce = ""; for (final String token : tokenizer) { final String[] parts = token.split("=", 2); @@ -50,29 +48,49 @@ public class DigestMd5 extends SaslMechanism { } final String digestUri = "xmpp/" + account.getServer(); final String nonceCount = "00000001"; - final String x = account.getUsername() + ":" + account.getServer() + ":" - + account.getPassword(); + final String x = + account.getUsername() + + ":" + + account.getServer() + + ":" + + account.getPassword(); final MessageDigest md = MessageDigest.getInstance("MD5"); final byte[] y = md.digest(x.getBytes(Charset.defaultCharset())); - final String cNonce = CryptoHelper.random(100, rng); - final byte[] a1 = CryptoHelper.concatenateByteArrays(y, - (":" + nonce + ":" + cNonce).getBytes(Charset.defaultCharset())); + final String cNonce = CryptoHelper.random(100); + final byte[] a1 = + CryptoHelper.concatenateByteArrays( + y, + (":" + nonce + ":" + cNonce) + .getBytes(Charset.defaultCharset())); final String a2 = "AUTHENTICATE:" + digestUri; final String ha1 = CryptoHelper.bytesToHex(md.digest(a1)); - final String ha2 = CryptoHelper.bytesToHex(md.digest(a2.getBytes(Charset - .defaultCharset()))); - final String kd = ha1 + ":" + nonce + ":" + nonceCount + ":" + cNonce - + ":auth:" + ha2; - final String response = CryptoHelper.bytesToHex(md.digest(kd.getBytes(Charset - .defaultCharset()))); - final String saslString = "username=\"" + account.getUsername() - + "\",realm=\"" + account.getServer() + "\",nonce=\"" - + nonce + "\",cnonce=\"" + cNonce + "\",nc=" + nonceCount - + ",qop=auth,digest-uri=\"" + digestUri + "\",response=" - + response + ",charset=utf-8"; - encodedResponse = Base64.encodeToString( - saslString.getBytes(Charset.defaultCharset()), - Base64.NO_WRAP); + final String ha2 = + CryptoHelper.bytesToHex( + md.digest(a2.getBytes(Charset.defaultCharset()))); + final String kd = + ha1 + ":" + nonce + ":" + nonceCount + ":" + cNonce + ":auth:" + ha2; + final String response = + CryptoHelper.bytesToHex( + md.digest(kd.getBytes(Charset.defaultCharset()))); + final String saslString = + "username=\"" + + account.getUsername() + + "\",realm=\"" + + account.getServer() + + "\",nonce=\"" + + nonce + + "\",cnonce=\"" + + cNonce + + "\",nc=" + + nonceCount + + ",qop=auth,digest-uri=\"" + + digestUri + + "\",response=" + + response + + ",charset=utf-8"; + encodedResponse = + Base64.encodeToString( + saslString.getBytes(Charset.defaultCharset()), Base64.NO_WRAP); } catch (final NoSuchAlgorithmException e) { throw new AuthenticationException(e); } @@ -83,7 +101,7 @@ public class DigestMd5 extends SaslMechanism { break; case VALID_SERVER_RESPONSE: if (challenge == null) { - return null; //everything is fine + return null; // everything is fine } default: throw new InvalidStateException(state); diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/External.java b/src/main/java/eu/siacs/conversations/crypto/sasl/External.java index 6e0ed43900391372febacaccd694daedd8fc908c..06323f039b57f1fde0539907ef9be1025eba9472 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/External.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/External.java @@ -2,17 +2,14 @@ package eu.siacs.conversations.crypto.sasl; import android.util.Base64; -import java.security.SecureRandom; - import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.xml.TagWriter; public class External extends SaslMechanism { public static final String MECHANISM = "EXTERNAL"; - public External(TagWriter tagWriter, Account account, SecureRandom rng) { - super(tagWriter, account, rng); + public External(final Account account) { + super(account); } @Override @@ -27,6 +24,7 @@ public class External extends SaslMechanism { @Override public String getClientFirstMessage() { - return Base64.encodeToString(account.getJid().asBareJid().toEscapedString().getBytes(), Base64.NO_WRAP); + return Base64.encodeToString( + account.getJid().asBareJid().toEscapedString().getBytes(), Base64.NO_WRAP); } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java b/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java index d5cc037e1dc5bceec667f701480a945c8332cbca..875538becc99e9a68f3e148ad902fe76c686f3f7 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java @@ -5,14 +5,18 @@ import android.util.Base64; import java.nio.charset.Charset; import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.xml.TagWriter; public class Plain extends SaslMechanism { public static final String MECHANISM = "PLAIN"; - public Plain(final TagWriter tagWriter, final Account account) { - super(tagWriter, account, null); + public Plain(final Account account) { + super(account); + } + + public static String getMessage(String username, String password) { + final String message = '\u0000' + username + '\u0000' + password; + return Base64.encodeToString(message.getBytes(Charset.defaultCharset()), Base64.NO_WRAP); } @Override @@ -29,9 +33,4 @@ public class Plain extends SaslMechanism { public String getClientFirstMessage() { return getMessage(account.getUsername(), account.getPassword()); } - - public static String getMessage(String username, String password) { - final String message = '\u0000' + username + '\u0000' + password; - return Base64.encodeToString(message.getBytes(Charset.defaultCharset()), Base64.NO_WRAP); - } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java index b255b6f42e745c598ea2d86c6692bb09f9b0de74..ce2d5cd6adfed36634a553488dacaacb1199b1d8 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java @@ -2,18 +2,38 @@ package eu.siacs.conversations.crypto.sasl; import com.google.common.base.Strings; -import java.security.SecureRandom; +import java.util.Collection; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.xml.TagWriter; public abstract class SaslMechanism { - final protected TagWriter tagWriter; - final protected Account account; - final protected SecureRandom rng; + protected final Account account; + + protected SaslMechanism(final Account account) { + this.account = account; + } + + /** + * The priority is used to pin the authentication mechanism. If authentication fails, it MAY be + * retried with another mechanism of the same priority, but MUST NOT be tried with a mechanism + * of lower priority (to prevent downgrade attacks). + * + * @return An arbitrary int representing the priority + */ + public abstract int getPriority(); + + public abstract String getMechanism(); + + public String getClientFirstMessage() { + return ""; + } + + public String getResponse(final String challenge) throws AuthenticationException { + return ""; + } protected enum State { INITIAL, @@ -22,6 +42,22 @@ public abstract class SaslMechanism { VALID_SERVER_RESPONSE, } + public enum Version { + SASL, + SASL_2; + + public static Version of(final Element element) { + switch (Strings.nullToEmpty(element.getNamespace())) { + case Namespace.SASL: + return SASL; + case Namespace.SASL_2: + return SASL_2; + default: + throw new IllegalArgumentException("Unrecognized SASL namespace"); + } + } + } + public static class AuthenticationException extends Exception { public AuthenticationException(final String message) { super(message); @@ -46,42 +82,32 @@ public abstract class SaslMechanism { } } - public SaslMechanism(final TagWriter tagWriter, final Account account, final SecureRandom rng) { - this.tagWriter = tagWriter; - this.account = account; - this.rng = rng; - } + public static final class Factory { - /** - * The priority is used to pin the authentication mechanism. If authentication fails, it MAY be retried with another - * mechanism of the same priority, but MUST NOT be tried with a mechanism of lower priority (to prevent downgrade - * attacks). - * - * @return An arbitrary int representing the priority - */ - public abstract int getPriority(); - - public abstract String getMechanism(); + private final Account account; - public String getClientFirstMessage() { - return ""; - } - - public String getResponse(final String challenge) throws AuthenticationException { - return ""; - } - - public enum Version { - SASL, SASL_2; + public Factory(final Account account) { + this.account = account; + } - public static Version of(final Element element) { - switch ( Strings.nullToEmpty(element.getNamespace())) { - case Namespace.SASL: - return SASL; - case Namespace.SASL_2: - return SASL_2; - default: - throw new IllegalArgumentException("Unrecognized SASL namespace"); + public SaslMechanism of(final Collection mechanisms) { + if (mechanisms.contains(External.MECHANISM) && account.getPrivateKeyAlias() != null) { + return new External(account); + } else if (mechanisms.contains(ScramSha512.MECHANISM)) { + return new ScramSha512(account); + } else if (mechanisms.contains(ScramSha256.MECHANISM)) { + return new ScramSha256(account); + } else if (mechanisms.contains(ScramSha1.MECHANISM)) { + return new ScramSha1(account); + } else if (mechanisms.contains(Plain.MECHANISM) + && !account.getServer().equals("nimbuzz.com")) { + return new Plain(account); + } else if (mechanisms.contains(DigestMd5.MECHANISM)) { + return new DigestMd5(account); + } else if (mechanisms.contains(Anonymous.MECHANISM)) { + return new Anonymous(account); + } else { + return null; } } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java index 807056bf8b246129a965ce9aae73de45cbe9408a..0fe7434a8b7f80928a3d1f61caeccb840085837f 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java @@ -12,78 +12,53 @@ import org.bouncycastle.crypto.params.KeyParameter; import java.nio.charset.Charset; import java.security.InvalidKeyException; -import java.security.SecureRandom; import java.util.concurrent.ExecutionException; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.utils.CryptoHelper; -import eu.siacs.conversations.xml.TagWriter; abstract class ScramMechanism extends SaslMechanism { - // TODO: When channel binding (SCRAM-SHA1-PLUS) is supported in future, generalize this to indicate support and/or usage. - private final static String GS2_HEADER = "n,,"; + // TODO: When channel binding (SCRAM-SHA1-PLUS) is supported in future, generalize this to + // indicate support and/or usage. + private static final String GS2_HEADER = "n,,"; private static final byte[] CLIENT_KEY_BYTES = "Client Key".getBytes(); private static final byte[] SERVER_KEY_BYTES = "Server Key".getBytes(); - - protected abstract HMac getHMAC(); - - protected abstract Digest getDigest(); - - private static final Cache CACHE = CacheBuilder.newBuilder().maximumSize(10).build(); - - private static class CacheKey { - final String algorithm; - final String password; - final String salt; - final int iterations; - - private CacheKey(String algorithm, String password, String salt, int iterations) { - this.algorithm = algorithm; - this.password = password; - this.salt = salt; - this.iterations = iterations; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - CacheKey cacheKey = (CacheKey) o; - return iterations == cacheKey.iterations && - Objects.equal(algorithm, cacheKey.algorithm) && - Objects.equal(password, cacheKey.password) && - Objects.equal(salt, cacheKey.salt); - } - - @Override - public int hashCode() { - return Objects.hashCode(algorithm, password, salt, iterations); - } - } - - private KeyPair getKeyPair(final String password, final String salt, final int iterations) throws ExecutionException { - return CACHE.get(new CacheKey(getHMAC().getAlgorithmName(), password, salt, iterations), () -> { - final byte[] saltedPassword, serverKey, clientKey; - saltedPassword = hi(password.getBytes(), Base64.decode(salt, Base64.DEFAULT), iterations); - serverKey = hmac(saltedPassword, SERVER_KEY_BYTES); - clientKey = hmac(saltedPassword, CLIENT_KEY_BYTES); - return new KeyPair(clientKey, serverKey); - }); - } - + private static final Cache CACHE = + CacheBuilder.newBuilder().maximumSize(10).build(); private final String clientNonce; protected State state = State.INITIAL; private String clientFirstMessageBare; private byte[] serverSignature = null; - ScramMechanism(final TagWriter tagWriter, final Account account, final SecureRandom rng) { - super(tagWriter, account, rng); + ScramMechanism(final Account account) { + super(account); // This nonce should be different for each authentication attempt. - clientNonce = CryptoHelper.random(100, rng); + this.clientNonce = CryptoHelper.random(100); clientFirstMessageBare = ""; } + protected abstract HMac getHMAC(); + + protected abstract Digest getDigest(); + + private KeyPair getKeyPair(final String password, final String salt, final int iterations) + throws ExecutionException { + return CACHE.get( + new CacheKey(getHMAC().getAlgorithmName(), password, salt, iterations), + () -> { + final byte[] saltedPassword, serverKey, clientKey; + saltedPassword = + hi( + password.getBytes(), + Base64.decode(salt, Base64.DEFAULT), + iterations); + serverKey = hmac(saltedPassword, SERVER_KEY_BYTES); + clientKey = hmac(saltedPassword, CLIENT_KEY_BYTES); + return new KeyPair(clientKey, serverKey); + }); + } + private byte[] hmac(final byte[] key, final byte[] input) throws InvalidKeyException { final HMac hMac = getHMAC(); hMac.init(new KeyParameter(key)); @@ -123,8 +98,11 @@ abstract class ScramMechanism extends SaslMechanism { @Override public String getClientFirstMessage() { if (clientFirstMessageBare.isEmpty() && state == State.INITIAL) { - clientFirstMessageBare = "n=" + CryptoHelper.saslEscape(CryptoHelper.saslPrep(account.getUsername())) + - ",r=" + this.clientNonce; + clientFirstMessageBare = + "n=" + + CryptoHelper.saslEscape(CryptoHelper.saslPrep(account.getUsername())) + + ",r=" + + this.clientNonce; state = State.AUTH_TEXT_SENT; } return Base64.encodeToString( @@ -173,7 +151,8 @@ abstract class ScramMechanism extends SaslMechanism { * MUST cause authentication failure when the attribute is parsed by * the other end. */ - throw new AuthenticationException("Server sent reserved token: `m'"); + throw new AuthenticationException( + "Server sent reserved token: `m'"); } } } @@ -182,20 +161,33 @@ abstract class ScramMechanism extends SaslMechanism { throw new AuthenticationException("Server did not send iteration count"); } if (nonce.isEmpty() || !nonce.startsWith(clientNonce)) { - throw new AuthenticationException("Server nonce does not contain client nonce: " + nonce); + throw new AuthenticationException( + "Server nonce does not contain client nonce: " + nonce); } if (salt.isEmpty()) { throw new AuthenticationException("Server sent empty salt"); } - final String clientFinalMessageWithoutProof = "c=" + Base64.encodeToString( - GS2_HEADER.getBytes(), Base64.NO_WRAP) + ",r=" + nonce; - final byte[] authMessage = (clientFirstMessageBare + ',' + new String(serverFirstMessage) + ',' - + clientFinalMessageWithoutProof).getBytes(); + final String clientFinalMessageWithoutProof = + "c=" + + Base64.encodeToString(GS2_HEADER.getBytes(), Base64.NO_WRAP) + + ",r=" + + nonce; + final byte[] authMessage = + (clientFirstMessageBare + + ',' + + new String(serverFirstMessage) + + ',' + + clientFinalMessageWithoutProof) + .getBytes(); final KeyPair keys; try { - keys = getKeyPair(CryptoHelper.saslPrep(account.getPassword()), salt, iterationCount); + keys = + getKeyPair( + CryptoHelper.saslPrep(account.getPassword()), + salt, + iterationCount); } catch (ExecutionException e) { throw new AuthenticationException("Invalid keys generated"); } @@ -213,35 +205,69 @@ abstract class ScramMechanism extends SaslMechanism { final byte[] clientProof = new byte[keys.clientKey.length]; if (clientSignature.length < keys.clientKey.length) { - throw new AuthenticationException("client signature was shorter than clientKey"); + throw new AuthenticationException( + "client signature was shorter than clientKey"); } for (int i = 0; i < clientProof.length; i++) { clientProof[i] = (byte) (keys.clientKey[i] ^ clientSignature[i]); } - - final String clientFinalMessage = clientFinalMessageWithoutProof + ",p=" + - Base64.encodeToString(clientProof, Base64.NO_WRAP); + final String clientFinalMessage = + clientFinalMessageWithoutProof + + ",p=" + + Base64.encodeToString(clientProof, Base64.NO_WRAP); state = State.RESPONSE_SENT; return Base64.encodeToString(clientFinalMessage.getBytes(), Base64.NO_WRAP); case RESPONSE_SENT: try { - final String clientCalculatedServerFinalMessage = "v=" + - Base64.encodeToString(serverSignature, Base64.NO_WRAP); - if (!clientCalculatedServerFinalMessage.equals(new String(Base64.decode(challenge, Base64.DEFAULT)))) { + final String clientCalculatedServerFinalMessage = + "v=" + Base64.encodeToString(serverSignature, Base64.NO_WRAP); + if (!clientCalculatedServerFinalMessage.equals( + new String(Base64.decode(challenge, Base64.DEFAULT)))) { throw new Exception(); } state = State.VALID_SERVER_RESPONSE; return ""; } catch (Exception e) { - throw new AuthenticationException("Server final message does not match calculated final message"); + throw new AuthenticationException( + "Server final message does not match calculated final message"); } default: throw new InvalidStateException(state); } } + private static class CacheKey { + final String algorithm; + final String password; + final String salt; + final int iterations; + + private CacheKey(String algorithm, String password, String salt, int iterations) { + this.algorithm = algorithm; + this.password = password; + this.salt = salt; + this.iterations = iterations; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CacheKey cacheKey = (CacheKey) o; + return iterations == cacheKey.iterations + && Objects.equal(algorithm, cacheKey.algorithm) + && Objects.equal(password, cacheKey.password) + && Objects.equal(salt, cacheKey.salt); + } + + @Override + public int hashCode() { + return Objects.hashCode(algorithm, password, salt, iterations); + } + } + private static class KeyPair { final byte[] clientKey; final byte[] serverKey; diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java index c58dd147c224767334526848297b57f0caec4aa4..472c4dea161b2b6666b9c1ec8ae71ba08725f4d3 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java @@ -4,15 +4,16 @@ import org.bouncycastle.crypto.Digest; import org.bouncycastle.crypto.digests.SHA1Digest; import org.bouncycastle.crypto.macs.HMac; -import java.security.SecureRandom; - import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.xml.TagWriter; public class ScramSha1 extends ScramMechanism { public static final String MECHANISM = "SCRAM-SHA-1"; + public ScramSha1(final Account account) { + super(account); + } + @Override protected HMac getHMAC() { return new HMac(new SHA1Digest()); @@ -23,10 +24,6 @@ public class ScramSha1 extends ScramMechanism { return new SHA1Digest(); } - public ScramSha1(final TagWriter tagWriter, final Account account, final SecureRandom rng) { - super(tagWriter, account, rng); - } - @Override public int getPriority() { return 20; diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256.java index d5dc42b07e9e3a7cd0b78eaa89a4b3ec447a00b1..f3f6cab57f7f87a5bd79e9f66a8f349bcaeb9546 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256.java @@ -4,15 +4,16 @@ import org.bouncycastle.crypto.Digest; import org.bouncycastle.crypto.digests.SHA256Digest; import org.bouncycastle.crypto.macs.HMac; -import java.security.SecureRandom; - import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.xml.TagWriter; public class ScramSha256 extends ScramMechanism { public static final String MECHANISM = "SCRAM-SHA-256"; + public ScramSha256(final Account account) { + super(account); + } + @Override protected HMac getHMAC() { return new HMac(new SHA256Digest()); @@ -23,10 +24,6 @@ public class ScramSha256 extends ScramMechanism { return new SHA256Digest(); } - public ScramSha256(final TagWriter tagWriter, final Account account, final SecureRandom rng) { - super(tagWriter, account, rng); - } - @Override public int getPriority() { return 25; diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512.java index dbd30945cc3cd75afa9841b663d6bf5aab46d0e3..9a2f1a82d4946e9cec790bd199fdaf646c564fec 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512.java @@ -4,15 +4,16 @@ import org.bouncycastle.crypto.Digest; import org.bouncycastle.crypto.digests.SHA512Digest; import org.bouncycastle.crypto.macs.HMac; -import java.security.SecureRandom; - import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.xml.TagWriter; public class ScramSha512 extends ScramMechanism { public static final String MECHANISM = "SCRAM-SHA-512"; + public ScramSha512(final Account account) { + super(account); + } + @Override protected HMac getHMAC() { return new HMac(new SHA512Digest()); @@ -23,10 +24,6 @@ public class ScramSha512 extends ScramMechanism { return new SHA512Digest(); } - public ScramSha512(final TagWriter tagWriter, final Account account, final SecureRandom rng) { - super(tagWriter, account, rng); - } - @Override public int getPriority() { return 30; diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/Tokenizer.java b/src/main/java/eu/siacs/conversations/crypto/sasl/Tokenizer.java index f9ba24f094d480e2cb9cb19e705dbcdd523667fb..3038fb060a07d41d1f158e52d913d39d4a27638d 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/Tokenizer.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/Tokenizer.java @@ -6,9 +6,7 @@ import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; -/** - * A tokenizer for GS2 header strings - */ +/** A tokenizer for GS2 header strings */ public final class Tokenizer implements Iterator, Iterable { private final List parts; private int index; @@ -50,18 +48,19 @@ public final class Tokenizer implements Iterator, Iterable { } /** - * Removes the last object returned by {@code next} from the collection. - * This method can only be called once between each call to {@code next}. + * Removes the last object returned by {@code next} from the collection. This method can only be + * called once between each call to {@code next}. * * @throws UnsupportedOperationException if removing is not supported by the collection being - * iterated. - * @throws IllegalStateException if {@code next} has not been called, or {@code remove} has - * already been called after the last call to {@code next}. + * iterated. + * @throws IllegalStateException if {@code next} has not been called, or {@code remove} has + * already been called after the last call to {@code next}. */ @Override public void remove() { if (index <= 0) { - throw new IllegalStateException("You can't delete an element before first next() method call"); + throw new IllegalStateException( + "You can't delete an element before first next() method call"); } parts.remove(--index); } diff --git a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java index 566ce1e6a7e6bc14c48bc29ac88926615531391f..27f5c3fc7bdcbb8f78aa00c5b964c0a3efee4ae6 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.http; +import static eu.siacs.conversations.utils.Random.SECURE_RANDOM; + import android.os.Build; import android.util.Log; @@ -147,7 +149,7 @@ public class HttpConnectionManager extends AbstractConnectionManager { trustManager = mXmppConnectionService.getMemorizingTrustManager().getNonInteractive(); } try { - final SSLSocketFactory sf = new TLSSocketFactory(new X509TrustManager[]{trustManager}, mXmppConnectionService.getRNG()); + final SSLSocketFactory sf = new TLSSocketFactory(new X509TrustManager[]{trustManager}, SECURE_RANDOM); builder.sslSocketFactory(sf, trustManager); builder.hostnameVerifier(new StrictHostnameVerifier()); } catch (final KeyManagementException | NoSuchAlgorithmException ignored) { diff --git a/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java index 3e478dd0ff5f9ff0effedf66d39ba88f5969ace2..e2366dd48922982071737e9883bb43d534fd60b5 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java +++ b/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.http; +import static eu.siacs.conversations.utils.Random.SECURE_RANDOM; + import android.util.Log; import androidx.annotation.NonNull; @@ -124,7 +126,7 @@ public class HttpUploadConnection implements Transferable, AbstractConnectionMan || message.getEncryption() == Message.ENCRYPTION_AXOLOTL || message.getEncryption() == Message.ENCRYPTION_OTR) { this.key = new byte[44]; - mXmppConnectionService.getRNG().nextBytes(this.key); + SECURE_RANDOM.nextBytes(this.key); this.file.setKeyAndIv(this.key); } this.file.setExpectedSize(originalFileSize + (file.getKey() != null ? 16 : 0)); diff --git a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java index 79cad952090e5a3546947ebbbb317b88e37011b8..e74af3773b157b8d8e60408f8c10be54e3832884 100644 --- a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java +++ b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.services; +import static eu.siacs.conversations.utils.Random.SECURE_RANDOM; + import android.util.Log; import org.jetbrains.annotations.NotNull; @@ -502,7 +504,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { this.start = start.getTimestamp(); } this.end = end; - this.queryId = new BigInteger(50, mXmppConnectionService.getRNG()).toString(32); + this.queryId = new BigInteger(50, SECURE_RANDOM).toString(32); this.version = version; } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index ba2c8514e8e554ce24ef0ebe4073cc14018979d1..586b717ff79445fdaee1b3b038d9faa5be24962a 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -1,6 +1,7 @@ package eu.siacs.conversations.services; import static eu.siacs.conversations.utils.Compatibility.s; +import static eu.siacs.conversations.utils.Random.SECURE_RANDOM; import android.Manifest; import android.annotation.SuppressLint; @@ -379,7 +380,6 @@ public class XmppConnectionService extends Service { } }; private final AtomicLong mLastExpiryRun = new AtomicLong(0); - private SecureRandom mRandom; private final LruCache, ServiceDiscoveryResult> discoCache = new LruCache<>(20); private final OnStatusChanged statusListener = new OnStatusChanged() { @@ -451,7 +451,7 @@ public class XmppConnectionService extends Service { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": went into offline state during low ping mode. reconnecting now"); reconnectAccount(account, true, false); } else { - int timeToReconnect = mRandom.nextInt(10) + 2; + final int timeToReconnect = SECURE_RANDOM.nextInt(10) + 2; scheduleWakeUpCall(timeToReconnect, account.getUuid().hashCode()); } } else if (account.getStatus() == Account.State.REGISTRATION_SUCCESSFUL) { @@ -1143,7 +1143,6 @@ public class XmppConnectionService extends Service { Log.e(Config.LOGTAG, "unable to initialize security provider", throwable); } Resolver.init(this); - this.mRandom = new SecureRandom(); updateMemorizingTrustmanager(); final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); final int cacheSize = maxMemory / 8; @@ -3269,7 +3268,7 @@ public class XmppConnectionService extends Service { } return false; } - final Jid jid = Jid.of(CryptoHelper.pronounceable(getRNG()), server, null); + final Jid jid = Jid.of(CryptoHelper.pronounceable(), server, null); final Conversation conversation = findOrCreateConversation(account, jid, true, false, true); joinMuc(conversation, new OnConferenceJoined() { @Override @@ -4366,10 +4365,6 @@ public class XmppConnectionService extends Service { } } - public SecureRandom getRNG() { - return this.mRandom; - } - public MemorizingTrustManager getMemorizingTrustManager() { return this.mMemorizingTrustManager; } diff --git a/src/main/java/eu/siacs/conversations/ui/CreatePublicChannelDialog.java b/src/main/java/eu/siacs/conversations/ui/CreatePublicChannelDialog.java index 3c23b06eba35c461791c8d32c938eace7dc6d76c..8f5e2e6d2e3742391813fdc025a2c5e1f87a1925 100644 --- a/src/main/java/eu/siacs/conversations/ui/CreatePublicChannelDialog.java +++ b/src/main/java/eu/siacs/conversations/ui/CreatePublicChannelDialog.java @@ -43,7 +43,6 @@ public class CreatePublicChannelDialog extends DialogFragment implements OnBacke private boolean jidWasModified = false; private boolean nameEntered = false; private boolean skipTetxWatcher = false; - private static final SecureRandom RANDOM = new SecureRandom(); public static CreatePublicChannelDialog newInstance(List accounts) { CreatePublicChannelDialog dialog = new CreatePublicChannelDialog(); @@ -158,7 +157,7 @@ public class CreatePublicChannelDialog extends DialogFragment implements OnBacke try { return Jid.of(localpart, domain, null).toEscapedString(); } catch (IllegalArgumentException e) { - return Jid.of(CryptoHelper.pronounceable(RANDOM), domain, null).toEscapedString(); + return Jid.of(CryptoHelper.pronounceable(), domain, null).toEscapedString(); } } } diff --git a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java index a92d48825e70cf48b7c135b8e122c7c46d3e3070..85ea2bb862f6a08af03bd0f5f8a46f4938f02951 100644 --- a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.utils; +import static eu.siacs.conversations.utils.Random.SECURE_RANDOM; + import android.os.Bundle; import android.util.Base64; import android.util.Pair; @@ -59,12 +61,12 @@ public final class CryptoHelper { return builder.toString(); } - public static String pronounceable(SecureRandom random) { - final int rand = random.nextInt(4); + public static String pronounceable() { + final int rand = SECURE_RANDOM.nextInt(4); char[] output = new char[rand * 2 + (5 - rand)]; - boolean vowel = random.nextBoolean(); + boolean vowel = SECURE_RANDOM.nextBoolean(); for (int i = 0; i < output.length; ++i) { - output[i] = vowel ? VOWELS[random.nextInt(VOWELS.length)] : CONSONANTS[random.nextInt(CONSONANTS.length)]; + output[i] = vowel ? VOWELS[SECURE_RANDOM.nextInt(VOWELS.length)] : CONSONANTS[SECURE_RANDOM.nextInt(CONSONANTS.length)]; vowel = !vowel; } return String.valueOf(output); @@ -117,9 +119,9 @@ public final class CryptoHelper { return Normalizer.normalize(s, Normalizer.Form.NFKC); } - public static String random(int length, SecureRandom random) { + public static String random(final int length) { final byte[] bytes = new byte[length]; - random.nextBytes(bytes); + SECURE_RANDOM.nextBytes(bytes); return Base64.encodeToString(bytes, Base64.NO_PADDING | Base64.NO_WRAP | Base64.URL_SAFE); } diff --git a/src/main/java/eu/siacs/conversations/utils/Random.java b/src/main/java/eu/siacs/conversations/utils/Random.java new file mode 100644 index 0000000000000000000000000000000000000000..792c1fce1d708ab615bd03ea7bf9f10d091e019b --- /dev/null +++ b/src/main/java/eu/siacs/conversations/utils/Random.java @@ -0,0 +1,13 @@ +package eu.siacs.conversations.utils; + +import java.security.SecureRandom; + +public final class Random { + + public static final SecureRandom SECURE_RANDOM = new SecureRandom(); + + private Random() { + + } + +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 70bc347b380ce63a6138761df8a08230d856d241..a1336e22cf340ea1f817482796d87c1a3b752612 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.xmpp; +import static eu.siacs.conversations.utils.Random.SECURE_RANDOM; + import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; @@ -521,7 +523,7 @@ public class XmppConnection implements Runnable { ? trustManager.getInteractive(domain) : trustManager.getNonInteractive(domain) }, - mXmppConnectionService.getRNG()); + SECURE_RANDOM); return sc.getSocketFactory(); } @@ -1216,23 +1218,11 @@ public class XmppConnection implements Runnable { } private void authenticate(final SaslMechanism.Version version) throws IOException { - final List mechanisms = extractMechanisms(streamFeatures.findChild("mechanisms")); - if (mechanisms.contains(External.MECHANISM) && account.getPrivateKeyAlias() != null) { - saslMechanism = new External(tagWriter, account, mXmppConnectionService.getRNG()); - } else if (mechanisms.contains(ScramSha512.MECHANISM)) { - saslMechanism = new ScramSha512(tagWriter, account, mXmppConnectionService.getRNG()); - } else if (mechanisms.contains(ScramSha256.MECHANISM)) { - saslMechanism = new ScramSha256(tagWriter, account, mXmppConnectionService.getRNG()); - } else if (mechanisms.contains(ScramSha1.MECHANISM)) { - saslMechanism = new ScramSha1(tagWriter, account, mXmppConnectionService.getRNG()); - } else if (mechanisms.contains(Plain.MECHANISM) - && !account.getJid().getDomain().toEscapedString().equals("nimbuzz.com")) { - saslMechanism = new Plain(tagWriter, account); - } else if (mechanisms.contains(DigestMd5.MECHANISM)) { - saslMechanism = new DigestMd5(tagWriter, account, mXmppConnectionService.getRNG()); - } else if (mechanisms.contains(Anonymous.MECHANISM)) { - saslMechanism = new Anonymous(tagWriter, account, mXmppConnectionService.getRNG()); - } + final Element element = streamFeatures.findChild("mechanisms"); + final Collection mechanisms = Collections2.transform(element.getChildren(), c -> c == null ? null : c.getContent()); + final SaslMechanism.Factory factory = new SaslMechanism.Factory(account); + this.saslMechanism = factory.of(mechanisms); + if (saslMechanism == null) { Log.d( Config.LOGTAG, @@ -1317,12 +1307,8 @@ public class XmppConnection implements Runnable { return bind; } - private static List extractMechanisms(final Element stream) { - final ArrayList mechanisms = new ArrayList<>(stream.getChildren().size()); - for (final Element child : stream.getChildren()) { - mechanisms.add(child.getContent()); - } - return mechanisms; + private static Collection extractMechanisms(final Element stream) { + return Collections2.transform(stream.getChildren(), c -> c == null ? null : c.getContent()); } private void register() { @@ -1963,8 +1949,8 @@ public class XmppConnection implements Runnable { return nextRandomId(false); } - private String nextRandomId(boolean s) { - return CryptoHelper.random(s ? 3 : 9, mXmppConnectionService.getRNG()); + private String nextRandomId(final boolean s) { + return CryptoHelper.random(s ? 3 : 9); } public String sendIqPacket(final IqPacket packet, final OnIqPacketReceived callback) { From b78acb6fcad3609b383f5d302a6e831f544a746d Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 6 Sep 2022 14:53:12 +0200 Subject: [PATCH 028/101] extract channel binding types via XEP-0440 --- .../crypto/sasl/ChannelBinding.java | 27 +++++++++++++ .../crypto/sasl/SaslMechanism.java | 3 +- .../crypto/sasl/ScramMechanism.java | 5 ++- .../conversations/crypto/sasl/ScramSha1.java | 2 +- .../crypto/sasl/ScramSha256.java | 2 +- .../crypto/sasl/ScramSha512.java | 2 +- .../eu/siacs/conversations/xml/Namespace.java | 1 + .../conversations/xmpp/XmppConnection.java | 39 +++++++++++++------ 8 files changed, 63 insertions(+), 18 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java new file mode 100644 index 0000000000000000000000000000000000000000..847c50e9d7213fe1696bcf43b6b41eb7bad3efb1 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java @@ -0,0 +1,27 @@ +package eu.siacs.conversations.crypto.sasl; + +import android.util.Log; + +import com.google.common.base.CaseFormat; + +import eu.siacs.conversations.Config; + +public enum ChannelBinding { + NONE, + TLS_EXPORTER, + TLS_SERVER_END_POINT, + TLS_UNIQUE; + + public static ChannelBinding of(final String type) { + if (type == null) { + return null; + } + try { + return valueOf( + CaseFormat.LOWER_HYPHEN.converterTo(CaseFormat.UPPER_UNDERSCORE).convert(type)); + } catch (final IllegalArgumentException e) { + Log.d(Config.LOGTAG, type + " is not a known channel binding"); + return null; + } + } +} diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java index ce2d5cd6adfed36634a553488dacaacb1199b1d8..13360a06380a3b0a0e34ce711d748604417af38e 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java @@ -90,7 +90,8 @@ public abstract class SaslMechanism { this.account = account; } - public SaslMechanism of(final Collection mechanisms) { + public SaslMechanism of( + final Collection mechanisms, final Collection bindings) { if (mechanisms.contains(External.MECHANISM) && account.getPrivateKeyAlias() != null) { return new External(account); } else if (mechanisms.contains(ScramSha512.MECHANISM)) { diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java index 0fe7434a8b7f80928a3d1f61caeccb840085837f..887128a0c1088db9f89fd7aecb0305752136d562 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java @@ -25,14 +25,15 @@ abstract class ScramMechanism extends SaslMechanism { private static final byte[] SERVER_KEY_BYTES = "Server Key".getBytes(); private static final Cache CACHE = CacheBuilder.newBuilder().maximumSize(10).build(); + protected final ChannelBinding channelBinding; private final String clientNonce; protected State state = State.INITIAL; private String clientFirstMessageBare; private byte[] serverSignature = null; - ScramMechanism(final Account account) { + ScramMechanism(final Account account, final ChannelBinding channelBinding) { super(account); - + this.channelBinding = channelBinding; // This nonce should be different for each authentication attempt. this.clientNonce = CryptoHelper.random(100); clientFirstMessageBare = ""; diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java index 472c4dea161b2b6666b9c1ec8ae71ba08725f4d3..9bcc8ad47915b33911ade6831b0b1876e0972c7d 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java @@ -11,7 +11,7 @@ public class ScramSha1 extends ScramMechanism { public static final String MECHANISM = "SCRAM-SHA-1"; public ScramSha1(final Account account) { - super(account); + super(account, ChannelBinding.NONE); } @Override diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256.java index f3f6cab57f7f87a5bd79e9f66a8f349bcaeb9546..610ed788bbb10138c1f44b48657e1321e6d8c18f 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256.java @@ -11,7 +11,7 @@ public class ScramSha256 extends ScramMechanism { public static final String MECHANISM = "SCRAM-SHA-256"; public ScramSha256(final Account account) { - super(account); + super(account, ChannelBinding.NONE); } @Override diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512.java index 9a2f1a82d4946e9cec790bd199fdaf646c564fec..3d54b39e962cb48c2545680853a35ebef782a3c6 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512.java @@ -11,7 +11,7 @@ public class ScramSha512 extends ScramMechanism { public static final String MECHANISM = "SCRAM-SHA-512"; public ScramSha512(final Account account) { - super(account); + super(account, ChannelBinding.NONE); } @Override diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index a4fd8c06393d993587f5455606f428e63e80cf20..e9f9639ec1fbdd2fd52f1403436e2e3b5da8a17f 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -17,6 +17,7 @@ public final class Namespace { public static final String OOB = "jabber:x:oob"; public static final String SASL = "urn:ietf:params:xml:ns:xmpp-sasl"; public static final String SASL_2 = "urn:xmpp:sasl:1"; + public static final String CHANNEL_BINDING = "urn:xmpp:sasl-cb:0"; public static final String TLS = "urn:ietf:params:xml:ns:xmpp-tls"; public static final String PUBSUB = "http://jabber.org/protocol/pubsub"; public static final String PUBSUB_PUBLISH_OPTIONS = PUBSUB + "#publish-options"; diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index a1336e22cf340ea1f817482796d87c1a3b752612..b5d7fd1af01f26cfacd136d7996be9d41982f9b0 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -14,6 +14,7 @@ import android.util.SparseArray; import androidx.annotation.NonNull; +import com.google.common.base.Predicates; import com.google.common.base.Strings; import com.google.common.collect.Collections2; @@ -62,14 +63,8 @@ import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.XmppDomainVerifier; import eu.siacs.conversations.crypto.axolotl.AxolotlService; -import eu.siacs.conversations.crypto.sasl.Anonymous; -import eu.siacs.conversations.crypto.sasl.DigestMd5; -import eu.siacs.conversations.crypto.sasl.External; -import eu.siacs.conversations.crypto.sasl.Plain; +import eu.siacs.conversations.crypto.sasl.ChannelBinding; import eu.siacs.conversations.crypto.sasl.SaslMechanism; -import eu.siacs.conversations.crypto.sasl.ScramSha1; -import eu.siacs.conversations.crypto.sasl.ScramSha256; -import eu.siacs.conversations.crypto.sasl.ScramSha512; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.ServiceDiscoveryResult; @@ -720,7 +715,7 @@ public class XmppConnection implements Runnable { Log.d( Config.LOGTAG, account.getJid().asBareJid().toString() + ": logged in (using " + version + ")"); - //TODO store mechanism name + // TODO store mechanism name account.setKey(Account.PINNED_MECHANISM_KEY, String.valueOf(saslMechanism.getPriority())); if (version == SaslMechanism.Version.SASL_2) { final String authorizationIdentifier = @@ -784,7 +779,7 @@ public class XmppConnection implements Runnable { account.getJid().asBareJid() + ": successfully enabled carbons"); features.carbonsEnabled = true; } - //TODO if both are set mark account ready for pipelining + // TODO if both are set mark account ready for pipelining sendPostBindInitialization(streamManagementEnabled != null, carbonsEnabled != null); } } @@ -1218,10 +1213,30 @@ public class XmppConnection implements Runnable { } private void authenticate(final SaslMechanism.Version version) throws IOException { - final Element element = streamFeatures.findChild("mechanisms"); - final Collection mechanisms = Collections2.transform(element.getChildren(), c -> c == null ? null : c.getContent()); + Log.d(Config.LOGTAG, "stream features: " + this.streamFeatures); + final Element element = + this.streamFeatures.findChild("mechanisms"); // TODO get from correct NS + final Collection mechanisms = + Collections2.transform( + Collections2.filter( + element.getChildren(), + c -> c != null && "mechanism".equals(c.getName())), + c -> c == null ? null : c.getContent()); + final Element cbElement = + this.streamFeatures.findChild("sasl-channel-binding", Namespace.CHANNEL_BINDING); + final Collection channelBindings = + Collections2.filter( + Collections2.transform( + Collections2.filter( + cbElement == null + ? Collections.emptyList() + : cbElement.getChildren(), + c -> c != null && "channel-binding".equals(c.getName())), + c -> c == null ? null : ChannelBinding.of(c.getAttribute("type"))), + Predicates.notNull()); + Log.d(Config.LOGTAG, "channel bindings: " + channelBindings); final SaslMechanism.Factory factory = new SaslMechanism.Factory(account); - this.saslMechanism = factory.of(mechanisms); + this.saslMechanism = factory.of(mechanisms, channelBindings); if (saslMechanism == null) { Log.d( From 5da9f5b3a344001a85f5299e69557d23771bcc9d Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 6 Sep 2022 16:28:28 +0200 Subject: [PATCH 029/101] refactor ScramMechanism to support PLUS --- .../java/eu/siacs/conversations/Config.java | 2 + .../conversations/crypto/sasl/DigestMd5.java | 5 +- .../crypto/sasl/SaslMechanism.java | 13 +++++- .../crypto/sasl/ScramMechanism.java | 46 +++++++++++++++---- .../crypto/sasl/ScramPlusMechanism.java | 22 +++++++++ .../crypto/sasl/ScramSha1Plus.java | 36 +++++++++++++++ .../conversations/xmpp/XmppConnection.java | 19 ++++++-- 7 files changed, 126 insertions(+), 17 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java create mode 100644 src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1Plus.java diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index 377be3ba1caf469b1308b75d256772bbfe89740f..f7c3dd151c65b86b870659eb0eff1707e899a520 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -57,6 +57,8 @@ public final class Config { public static final long CONTACT_SYNC_RETRY_INTERVAL = 1000L * 60 * 5; + public static final boolean SASL_2_ENABLED = false; + //Notification settings public static final boolean HIDE_MESSAGE_TEXT_IN_NOTIFICATION = false; public static final boolean ALWAYS_NOTIFY_BY_DEFAULT = false; diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/DigestMd5.java b/src/main/java/eu/siacs/conversations/crypto/sasl/DigestMd5.java index 7229299effa6e7ecb78ee4c599b1bb10dc46a74a..b75d0883fd4a5bdbf6634a2cddf5054c40c02e26 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/DigestMd5.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/DigestMd5.java @@ -6,6 +6,8 @@ import java.nio.charset.Charset; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import javax.net.ssl.SSLSocket; + import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.utils.CryptoHelper; @@ -29,7 +31,8 @@ public class DigestMd5 extends SaslMechanism { } @Override - public String getResponse(final String challenge) throws AuthenticationException { + public String getResponse(final String challenge, final SSLSocket sslSocket) + throws AuthenticationException { switch (state) { case INITIAL: state = State.RESPONSE_SENT; diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java index 13360a06380a3b0a0e34ce711d748604417af38e..5fafde9e96cddbb3fd7d47b624870c01c544a5ba 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java @@ -4,6 +4,8 @@ import com.google.common.base.Strings; import java.util.Collection; +import javax.net.ssl.SSLSocket; + import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; @@ -31,7 +33,8 @@ public abstract class SaslMechanism { return ""; } - public String getResponse(final String challenge) throws AuthenticationException { + public String getResponse(final String challenge, final SSLSocket sslSocket) + throws AuthenticationException { return ""; } @@ -112,4 +115,12 @@ public abstract class SaslMechanism { } } } + + public static String namespace(final Version version) { + if (version == Version.SASL) { + return Namespace.SASL; + } else { + return Namespace.SASL_2; + } + } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java index 887128a0c1088db9f89fd7aecb0305752136d562..e6bc3a15d94af4338159b09ab6c01236e807a3ce 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java @@ -2,6 +2,7 @@ package eu.siacs.conversations.crypto.sasl; import android.util.Base64; +import com.google.common.base.CaseFormat; import com.google.common.base.Objects; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; @@ -14,18 +15,19 @@ import java.nio.charset.Charset; import java.security.InvalidKeyException; import java.util.concurrent.ExecutionException; +import javax.net.ssl.SSLSocket; + import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.utils.CryptoHelper; abstract class ScramMechanism extends SaslMechanism { - // TODO: When channel binding (SCRAM-SHA1-PLUS) is supported in future, generalize this to - // indicate support and/or usage. - private static final String GS2_HEADER = "n,,"; + private static final byte[] CLIENT_KEY_BYTES = "Client Key".getBytes(); private static final byte[] SERVER_KEY_BYTES = "Server Key".getBytes(); private static final Cache CACHE = CacheBuilder.newBuilder().maximumSize(10).build(); protected final ChannelBinding channelBinding; + private final String gs2Header; private final String clientNonce; protected State state = State.INITIAL; private String clientFirstMessageBare; @@ -34,6 +36,16 @@ abstract class ScramMechanism extends SaslMechanism { ScramMechanism(final Account account, final ChannelBinding channelBinding) { super(account); this.channelBinding = channelBinding; + if (channelBinding == ChannelBinding.NONE) { + this.gs2Header = "n,,"; + } else { + this.gs2Header = + String.format( + "p=%s,,", + CaseFormat.UPPER_UNDERSCORE + .converterTo(CaseFormat.LOWER_HYPHEN) + .convert(channelBinding.toString())); + } // This nonce should be different for each authentication attempt. this.clientNonce = CryptoHelper.random(100); clientFirstMessageBare = ""; @@ -69,7 +81,7 @@ abstract class ScramMechanism extends SaslMechanism { return out; } - public byte[] digest(byte[] bytes) { + public byte[] digest(final byte[] bytes) { final Digest digest = getDigest(); digest.reset(); digest.update(bytes, 0, bytes.length); @@ -107,12 +119,13 @@ abstract class ScramMechanism extends SaslMechanism { state = State.AUTH_TEXT_SENT; } return Base64.encodeToString( - (GS2_HEADER + clientFirstMessageBare).getBytes(Charset.defaultCharset()), + (gs2Header + clientFirstMessageBare).getBytes(Charset.defaultCharset()), Base64.NO_WRAP); } @Override - public String getResponse(final String challenge) throws AuthenticationException { + public String getResponse(final String challenge, final SSLSocket socket) + throws AuthenticationException { switch (state) { case AUTH_TEXT_SENT: if (challenge == null) { @@ -169,11 +182,17 @@ abstract class ScramMechanism extends SaslMechanism { throw new AuthenticationException("Server sent empty salt"); } + final byte[] channelBindingData = getChannelBindingData(socket); + + final int gs2Len = this.gs2Header.getBytes().length; + final byte[] cMessage = new byte[gs2Len + channelBindingData.length]; + System.arraycopy(this.gs2Header.getBytes(), 0, cMessage, 0, gs2Len); + System.arraycopy( + channelBindingData, 0, cMessage, gs2Len, channelBindingData.length); + final String clientFinalMessageWithoutProof = - "c=" - + Base64.encodeToString(GS2_HEADER.getBytes(), Base64.NO_WRAP) - + ",r=" - + nonce; + "c=" + Base64.encodeToString(cMessage, Base64.NO_WRAP) + ",r=" + nonce; + final byte[] authMessage = (clientFirstMessageBare + ',' @@ -239,6 +258,13 @@ abstract class ScramMechanism extends SaslMechanism { } } + protected byte[] getChannelBindingData(final SSLSocket sslSocket) throws AuthenticationException { + if (this.channelBinding == ChannelBinding.NONE) { + return new byte[0]; + } + throw new AssertionError("getChannelBindingData needs to be overwritten"); + } + private static class CacheKey { final String algorithm; final String password; diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java new file mode 100644 index 0000000000000000000000000000000000000000..0067a4237ca25741bc2357c0fe371e284c81dfac --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java @@ -0,0 +1,22 @@ +package eu.siacs.conversations.crypto.sasl; + +import javax.net.ssl.SSLSocket; + +import eu.siacs.conversations.entities.Account; + +abstract class ScramPlusMechanism extends ScramMechanism { + ScramPlusMechanism(Account account, ChannelBinding channelBinding) { + super(account, channelBinding); + } + + @Override + protected byte[] getChannelBindingData(final SSLSocket sslSocket) throws AuthenticationException { + if (this.channelBinding == ChannelBinding.NONE) { + throw new AuthenticationException(String.format("%s is not a valid channel binding", ChannelBinding.NONE)); + } + if (sslSocket == null) { + throw new AuthenticationException("Channel binding attempt on non secure socket"); + } + throw new AssertionError("not yet implemented"); + } +} diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1Plus.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1Plus.java new file mode 100644 index 0000000000000000000000000000000000000000..34d9009fc0ad49bb43fc2cab67085517e6d903e4 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1Plus.java @@ -0,0 +1,36 @@ +package eu.siacs.conversations.crypto.sasl; + +import org.bouncycastle.crypto.Digest; +import org.bouncycastle.crypto.digests.SHA1Digest; +import org.bouncycastle.crypto.macs.HMac; + +import eu.siacs.conversations.entities.Account; + +public class ScramSha1Plus extends ScramPlusMechanism { + + public static final String MECHANISM = "SCRAM-SHA-1-PLUS"; + + public ScramSha1Plus(final Account account, final ChannelBinding channelBinding) { + super(account, channelBinding); + } + + @Override + protected HMac getHMAC() { + return new HMac(new SHA1Digest()); + } + + @Override + protected Digest getDigest() { + return new SHA1Digest(); + } + + @Override + public int getPriority() { + return 35; //higher than SCRAM-SHA512 (30) + } + + @Override + public String getMechanism() { + return MECHANISM; + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index b5d7fd1af01f26cfacd136d7996be9d41982f9b0..a1719dd25b0e838ae61f172dbdb1ffe1bb93a3fb 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -607,7 +607,7 @@ public class XmppConnection implements Runnable { throw new AssertionError("Missing implementation for " + version); } try { - response.setContent(saslMechanism.getResponse(challenge.getContent())); + response.setContent(saslMechanism.getResponse(challenge.getContent(), sslSocketOrNull(socket))); } catch (final SaslMechanism.AuthenticationException e) { // TODO: Send auth abort tag. Log.e(Config.LOGTAG, e.toString()); @@ -707,7 +707,7 @@ public class XmppConnection implements Runnable { throw new AssertionError("Missing implementation for " + version); } try { - saslMechanism.getResponse(challenge); + saslMechanism.getResponse(challenge, sslSocketOrNull(socket)); } catch (final SaslMechanism.AuthenticationException e) { Log.e(Config.LOGTAG, String.valueOf(e)); throw new StateChangingException(Account.State.UNAUTHORIZED); @@ -798,6 +798,14 @@ public class XmppConnection implements Runnable { } } + private static SSLSocket sslSocketOrNull(final Socket socket) { + if (socket instanceof SSLSocket) { + return (SSLSocket) socket; + } else { + return null; + } + } + private void processEnabled(final Element enabled) { final String streamId; if (enabled.getAttributeAsBoolean("resume")) { @@ -1170,7 +1178,8 @@ public class XmppConnection implements Runnable { } else if (!this.streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE) && account.isOptionSet(Account.OPTION_REGISTER)) { throw new StateChangingException(Account.State.REGISTRATION_NOT_SUPPORTED); - } else if (this.streamFeatures.hasChild("mechanisms", Namespace.SASL_2) + } else if (Config.SASL_2_ENABLED + && this.streamFeatures.hasChild("mechanisms", Namespace.SASL_2) && shouldAuthenticate && isSecure) { authenticate(SaslMechanism.Version.SASL_2); @@ -1213,9 +1222,8 @@ public class XmppConnection implements Runnable { } private void authenticate(final SaslMechanism.Version version) throws IOException { - Log.d(Config.LOGTAG, "stream features: " + this.streamFeatures); final Element element = - this.streamFeatures.findChild("mechanisms"); // TODO get from correct NS + this.streamFeatures.findChild("mechanisms", SaslMechanism.namespace(version)); final Collection mechanisms = Collections2.transform( Collections2.filter( @@ -1234,6 +1242,7 @@ public class XmppConnection implements Runnable { c -> c != null && "channel-binding".equals(c.getName())), c -> c == null ? null : ChannelBinding.of(c.getAttribute("type"))), Predicates.notNull()); + Log.d(Config.LOGTAG,"mechanisms: "+mechanisms); Log.d(Config.LOGTAG, "channel bindings: " + channelBindings); final SaslMechanism.Factory factory = new SaslMechanism.Factory(account); this.saslMechanism = factory.of(mechanisms, channelBindings); From 6d3d9dfe26a83fb49cbc12009bc5dfe1bea09704 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 6 Sep 2022 16:43:51 +0200 Subject: [PATCH 030/101] support channel binding with tls-exporter --- .../crypto/sasl/SaslMechanism.java | 19 ++++++++------- .../crypto/sasl/ScramMechanism.java | 3 ++- .../crypto/sasl/ScramPlusMechanism.java | 23 +++++++++++++++---- .../crypto/sasl/ScramSha1Plus.java | 2 +- 4 files changed, 32 insertions(+), 15 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java index 5fafde9e96cddbb3fd7d47b624870c01c544a5ba..4380ad93c34596991c9ad36a1f5e964c803fc516 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java @@ -18,6 +18,14 @@ public abstract class SaslMechanism { this.account = account; } + public static String namespace(final Version version) { + if (version == Version.SASL) { + return Namespace.SASL; + } else { + return Namespace.SASL_2; + } + } + /** * The priority is used to pin the authentication mechanism. If authentication fails, it MAY be * retried with another mechanism of the same priority, but MUST NOT be tried with a mechanism @@ -97,6 +105,9 @@ public abstract class SaslMechanism { final Collection mechanisms, final Collection bindings) { if (mechanisms.contains(External.MECHANISM) && account.getPrivateKeyAlias() != null) { return new External(account); + } else if (mechanisms.contains(ScramSha1Plus.MECHANISM) + && bindings.contains(ChannelBinding.TLS_EXPORTER)) { + return new ScramSha1Plus(account, ChannelBinding.TLS_EXPORTER); } else if (mechanisms.contains(ScramSha512.MECHANISM)) { return new ScramSha512(account); } else if (mechanisms.contains(ScramSha256.MECHANISM)) { @@ -115,12 +126,4 @@ public abstract class SaslMechanism { } } } - - public static String namespace(final Version version) { - if (version == Version.SASL) { - return Namespace.SASL; - } else { - return Namespace.SASL_2; - } - } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java index e6bc3a15d94af4338159b09ab6c01236e807a3ce..62f221b74394f18bcbefa64313707f7cff9b00a0 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java @@ -258,7 +258,8 @@ abstract class ScramMechanism extends SaslMechanism { } } - protected byte[] getChannelBindingData(final SSLSocket sslSocket) throws AuthenticationException { + protected byte[] getChannelBindingData(final SSLSocket sslSocket) + throws AuthenticationException { if (this.channelBinding == ChannelBinding.NONE) { return new byte[0]; } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java index 0067a4237ca25741bc2357c0fe371e284c81dfac..3b0dbb6e18bf6ef3338cc656fb3a0b8d401c2ce4 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java @@ -1,22 +1,35 @@ package eu.siacs.conversations.crypto.sasl; +import org.conscrypt.Conscrypt; + +import javax.net.ssl.SSLException; import javax.net.ssl.SSLSocket; import eu.siacs.conversations.entities.Account; abstract class ScramPlusMechanism extends ScramMechanism { + + private static final String EXPORTER_LABEL = "EXPORTER-Channel-Binding"; + ScramPlusMechanism(Account account, ChannelBinding channelBinding) { super(account, channelBinding); } @Override - protected byte[] getChannelBindingData(final SSLSocket sslSocket) throws AuthenticationException { - if (this.channelBinding == ChannelBinding.NONE) { - throw new AuthenticationException(String.format("%s is not a valid channel binding", ChannelBinding.NONE)); - } + protected byte[] getChannelBindingData(final SSLSocket sslSocket) + throws AuthenticationException { if (sslSocket == null) { throw new AuthenticationException("Channel binding attempt on non secure socket"); } - throw new AssertionError("not yet implemented"); + if (this.channelBinding == ChannelBinding.TLS_EXPORTER) { + try { + return Conscrypt.exportKeyingMaterial(sslSocket, EXPORTER_LABEL, new byte[0], 32); + } catch (final SSLException e) { + throw new AuthenticationException("Could not export keying material"); + } + } else { + throw new AuthenticationException( + String.format("%s is not a valid channel binding", ChannelBinding.NONE)); + } } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1Plus.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1Plus.java index 34d9009fc0ad49bb43fc2cab67085517e6d903e4..d4f2fcb0bc444912f129fea417d5e6106273749d 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1Plus.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1Plus.java @@ -26,7 +26,7 @@ public class ScramSha1Plus extends ScramPlusMechanism { @Override public int getPriority() { - return 35; //higher than SCRAM-SHA512 (30) + return 35; // higher than SCRAM-SHA512 (30) } @Override From 789d1dc2259fa930c3751647c60526841f68abb6 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 6 Sep 2022 17:01:57 +0200 Subject: [PATCH 031/101] support tls-unique for TLSv1.2 --- .../crypto/sasl/ChannelBinding.java | 12 ++++++++++++ .../conversations/crypto/sasl/SaslMechanism.java | 6 +++--- .../crypto/sasl/ScramPlusMechanism.java | 16 +++++++++++++++- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java index 847c50e9d7213fe1696bcf43b6b41eb7bad3efb1..81bd1270527b547a377c47432b8f2e0499976059 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java @@ -4,6 +4,8 @@ import android.util.Log; import com.google.common.base.CaseFormat; +import java.util.Collection; + import eu.siacs.conversations.Config; public enum ChannelBinding { @@ -24,4 +26,14 @@ public enum ChannelBinding { return null; } } + + public static ChannelBinding best(final Collection bindings) { + if (bindings.contains(TLS_EXPORTER)) { + return TLS_EXPORTER; + } else if (bindings.contains(TLS_UNIQUE)) { + return TLS_UNIQUE; + } else { + return null; + } + } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java index 4380ad93c34596991c9ad36a1f5e964c803fc516..829a4e6ea04853f3717a5d50cf78247631c77c82 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java @@ -103,11 +103,11 @@ public abstract class SaslMechanism { public SaslMechanism of( final Collection mechanisms, final Collection bindings) { + final ChannelBinding channelBinding = ChannelBinding.best(bindings); if (mechanisms.contains(External.MECHANISM) && account.getPrivateKeyAlias() != null) { return new External(account); - } else if (mechanisms.contains(ScramSha1Plus.MECHANISM) - && bindings.contains(ChannelBinding.TLS_EXPORTER)) { - return new ScramSha1Plus(account, ChannelBinding.TLS_EXPORTER); + } else if (mechanisms.contains(ScramSha1Plus.MECHANISM) && channelBinding != null) { + return new ScramSha1Plus(account, channelBinding); } else if (mechanisms.contains(ScramSha512.MECHANISM)) { return new ScramSha512(account); } else if (mechanisms.contains(ScramSha256.MECHANISM)) { diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java index 3b0dbb6e18bf6ef3338cc656fb3a0b8d401c2ce4..8f6dec20ef6fdce0402e2315af41a1063fcd0fdb 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java @@ -22,11 +22,25 @@ abstract class ScramPlusMechanism extends ScramMechanism { throw new AuthenticationException("Channel binding attempt on non secure socket"); } if (this.channelBinding == ChannelBinding.TLS_EXPORTER) { + final byte[] keyingMaterial; try { - return Conscrypt.exportKeyingMaterial(sslSocket, EXPORTER_LABEL, new byte[0], 32); + keyingMaterial = + Conscrypt.exportKeyingMaterial(sslSocket, EXPORTER_LABEL, new byte[0], 32); } catch (final SSLException e) { throw new AuthenticationException("Could not export keying material"); } + if (keyingMaterial == null) { + throw new AuthenticationException( + "Could not export keying material. Socket not ready"); + } + return keyingMaterial; + } else if (this.channelBinding == ChannelBinding.TLS_UNIQUE) { + final byte[] unique = Conscrypt.getTlsUnique(sslSocket); + if (unique == null) { + throw new AuthenticationException( + "Could not retrieve tls unique. Socket not ready"); + } + return unique; } else { throw new AuthenticationException( String.format("%s is not a valid channel binding", ChannelBinding.NONE)); From e8bce17940f53669027fd31086cda24204be549e Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 6 Sep 2022 17:39:58 +0200 Subject: [PATCH 032/101] add scram-sha256 and 512 in their plus variants --- .../crypto/sasl/SaslMechanism.java | 4 +++ .../crypto/sasl/ScramMechanism.java | 3 ++ .../crypto/sasl/ScramSha256Plus.java | 36 +++++++++++++++++++ .../crypto/sasl/ScramSha512Plus.java | 36 +++++++++++++++++++ 4 files changed, 79 insertions(+) create mode 100644 src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256Plus.java create mode 100644 src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512Plus.java diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java index 829a4e6ea04853f3717a5d50cf78247631c77c82..aaff4cc8284a5ee5cdfc81a5e659cdd17c7dc952 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java @@ -106,6 +106,10 @@ public abstract class SaslMechanism { final ChannelBinding channelBinding = ChannelBinding.best(bindings); if (mechanisms.contains(External.MECHANISM) && account.getPrivateKeyAlias() != null) { return new External(account); + } else if (mechanisms.contains(ScramSha512Plus.MECHANISM) && channelBinding != null) { + return new ScramSha512Plus(account, channelBinding); + } else if (mechanisms.contains(ScramSha256Plus.MECHANISM) && channelBinding != null) { + return new ScramSha256Plus(account, channelBinding); } else if (mechanisms.contains(ScramSha1Plus.MECHANISM) && channelBinding != null) { return new ScramSha1Plus(account, channelBinding); } else if (mechanisms.contains(ScramSha512.MECHANISM)) { diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java index 62f221b74394f18bcbefa64313707f7cff9b00a0..aba434e3a66d23f7bac64c8138b843dcbfb17a41 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java @@ -37,6 +37,9 @@ abstract class ScramMechanism extends SaslMechanism { super(account); this.channelBinding = channelBinding; if (channelBinding == ChannelBinding.NONE) { + // TODO this needs to be changed to "y,," for the scram internal down grade protection + // but we might risk compatibility issues if the server supports a binding that we don’t + // support this.gs2Header = "n,,"; } else { this.gs2Header = diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256Plus.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256Plus.java new file mode 100644 index 0000000000000000000000000000000000000000..f48a052abf50852160addc63e82fb8c156405b74 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256Plus.java @@ -0,0 +1,36 @@ +package eu.siacs.conversations.crypto.sasl; + +import org.bouncycastle.crypto.Digest; +import org.bouncycastle.crypto.digests.SHA256Digest; +import org.bouncycastle.crypto.macs.HMac; + +import eu.siacs.conversations.entities.Account; + +public class ScramSha256Plus extends ScramPlusMechanism { + + public static final String MECHANISM = "SCRAM-SHA-256-PLUS"; + + public ScramSha256Plus(final Account account, final ChannelBinding channelBinding) { + super(account, channelBinding); + } + + @Override + protected HMac getHMAC() { + return new HMac(new SHA256Digest()); + } + + @Override + protected Digest getDigest() { + return new SHA256Digest(); + } + + @Override + public int getPriority() { + return 40; + } + + @Override + public String getMechanism() { + return MECHANISM; + } +} diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512Plus.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512Plus.java new file mode 100644 index 0000000000000000000000000000000000000000..8cec1f33f7906a86204eb812c27d8fc7feae63a3 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512Plus.java @@ -0,0 +1,36 @@ +package eu.siacs.conversations.crypto.sasl; + +import org.bouncycastle.crypto.Digest; +import org.bouncycastle.crypto.digests.SHA512Digest; +import org.bouncycastle.crypto.macs.HMac; + +import eu.siacs.conversations.entities.Account; + +public class ScramSha512Plus extends ScramPlusMechanism { + + public static final String MECHANISM = "SCRAM-SHA-512-PLUS"; + + public ScramSha512Plus(final Account account, final ChannelBinding channelBinding) { + super(account, channelBinding); + } + + @Override + protected HMac getHMAC() { + return new HMac(new SHA512Digest()); + } + + @Override + protected Digest getDigest() { + return new SHA512Digest(); + } + + @Override + public int getPriority() { + return 45; + } + + @Override + public String getMechanism() { + return MECHANISM; + } +} From d4ec1eaf3878dc1f43f1bf81124731de64537bfd Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 7 Sep 2022 10:31:11 +0200 Subject: [PATCH 033/101] refactor processFailure and processChallange into methods --- .../conversations/xmpp/XmppConnection.java | 111 ++++++++++-------- 1 file changed, 60 insertions(+), 51 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index a1719dd25b0e838ae61f172dbdb1ffe1bb93a3fb..5bcc99f136d65a4ee097e3e37c13e89122044adf 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -559,61 +559,13 @@ public class XmppConnection implements Runnable { throw new StateChangingException(Account.State.TLS_ERROR); } else if (nextTag.isStart("failure")) { final Element failure = tagReader.readElement(nextTag); - final SaslMechanism.Version version; - try { - version = SaslMechanism.Version.of(failure); - } catch (final IllegalArgumentException e) { - throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); - } - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": login failure " + version); - if (failure.hasChild("temporary-auth-failure")) { - throw new StateChangingException(Account.State.TEMPORARY_AUTH_FAILURE); - } else if (failure.hasChild("account-disabled")) { - final String text = failure.findChildContent("text"); - if (Strings.isNullOrEmpty(text)) { - throw new StateChangingException(Account.State.UNAUTHORIZED); - } - final Matcher matcher = Patterns.AUTOLINK_WEB_URL.matcher(text); - if (matcher.find()) { - final HttpUrl url; - try { - url = HttpUrl.get(text.substring(matcher.start(), matcher.end())); - } catch (final IllegalArgumentException e) { - throw new StateChangingException(Account.State.UNAUTHORIZED); - } - if (url.isHttps()) { - this.redirectionUrl = url; - throw new StateChangingException(Account.State.PAYMENT_REQUIRED); - } - } - } - throw new StateChangingException(Account.State.UNAUTHORIZED); + processFailure(failure); } else if (nextTag.isStart("continue", Namespace.SASL_2)) { + // two step sasl2 - we don’t support this yet throw new StateChangingException(Account.State.INCOMPATIBLE_CLIENT); } else if (nextTag.isStart("challenge")) { final Element challenge = tagReader.readElement(nextTag); - final SaslMechanism.Version version; - try { - version = SaslMechanism.Version.of(challenge); - } catch (final IllegalArgumentException e) { - throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); - } - final Element response; - if (version == SaslMechanism.Version.SASL) { - response = new Element("response", Namespace.SASL); - } else if (version == SaslMechanism.Version.SASL_2) { - response = new Element("response", Namespace.SASL_2); - } else { - throw new AssertionError("Missing implementation for " + version); - } - try { - response.setContent(saslMechanism.getResponse(challenge.getContent(), sslSocketOrNull(socket))); - } catch (final SaslMechanism.AuthenticationException e) { - // TODO: Send auth abort tag. - Log.e(Config.LOGTAG, e.toString()); - throw new StateChangingException(Account.State.UNAUTHORIZED); - } - tagWriter.writeElement(response); + processChallenge(challenge); } else if (nextTag.isStart("enabled", Namespace.STREAM_MANAGEMENT)) { final Element enabled = tagReader.readElement(nextTag); processEnabled(enabled); @@ -690,6 +642,31 @@ public class XmppConnection implements Runnable { } } + private void processChallenge(Element challenge) throws IOException { + final SaslMechanism.Version version; + try { + version = SaslMechanism.Version.of(challenge); + } catch (final IllegalArgumentException e) { + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); + } + final Element response; + if (version == SaslMechanism.Version.SASL) { + response = new Element("response", Namespace.SASL); + } else if (version == SaslMechanism.Version.SASL_2) { + response = new Element("response", Namespace.SASL_2); + } else { + throw new AssertionError("Missing implementation for " + version); + } + try { + response.setContent(saslMechanism.getResponse(challenge.getContent(), sslSocketOrNull(socket))); + } catch (final SaslMechanism.AuthenticationException e) { + // TODO: Send auth abort tag. + Log.e(Config.LOGTAG, e.toString()); + throw new StateChangingException(Account.State.UNAUTHORIZED); + } + tagWriter.writeElement(response); + } + private boolean processSuccess(final Element success) throws IOException, XmlPullParserException { final SaslMechanism.Version version; @@ -798,6 +775,38 @@ public class XmppConnection implements Runnable { } } + private void processFailure(final Element failure) throws StateChangingException { + final SaslMechanism.Version version; + try { + version = SaslMechanism.Version.of(failure); + } catch (final IllegalArgumentException e) { + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); + } + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": login failure " + version); + if (failure.hasChild("temporary-auth-failure")) { + throw new StateChangingException(Account.State.TEMPORARY_AUTH_FAILURE); + } else if (failure.hasChild("account-disabled")) { + final String text = failure.findChildContent("text"); + if (Strings.isNullOrEmpty(text)) { + throw new StateChangingException(Account.State.UNAUTHORIZED); + } + final Matcher matcher = Patterns.AUTOLINK_WEB_URL.matcher(text); + if (matcher.find()) { + final HttpUrl url; + try { + url = HttpUrl.get(text.substring(matcher.start(), matcher.end())); + } catch (final IllegalArgumentException e) { + throw new StateChangingException(Account.State.UNAUTHORIZED); + } + if (url.isHttps()) { + this.redirectionUrl = url; + throw new StateChangingException(Account.State.PAYMENT_REQUIRED); + } + } + } + throw new StateChangingException(Account.State.UNAUTHORIZED); + } + private static SSLSocket sslSocketOrNull(final Socket socket) { if (socket instanceof SSLSocket) { return (SSLSocket) socket; From 018e0d9edfd212c866063e04297a59b500b2c393 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 7 Sep 2022 11:08:54 +0200 Subject: [PATCH 034/101] add (inactive) channel binding end-point code --- .../crypto/sasl/ScramPlusMechanism.java | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java index 8f6dec20ef6fdce0402e2315af41a1063fcd0fdb..8de4524f2a3c28b340073911fef18cb6025f6ede 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java @@ -1,11 +1,24 @@ package eu.siacs.conversations.crypto.sasl; +import android.util.Log; + +import org.bouncycastle.jcajce.provider.digest.SHA256; import org.conscrypt.Conscrypt; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; + import javax.net.ssl.SSLException; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocket; +import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.utils.CryptoHelper; abstract class ScramPlusMechanism extends ScramMechanism { @@ -41,9 +54,59 @@ abstract class ScramPlusMechanism extends ScramMechanism { "Could not retrieve tls unique. Socket not ready"); } return unique; + } else if (this.channelBinding == ChannelBinding.TLS_SERVER_END_POINT) { + final byte[] endPoint = getServerEndPointChannelBinding(sslSocket.getSession()); + Log.d(Config.LOGTAG, "retrieved endpoint " + CryptoHelper.bytesToHex(endPoint)); + return endPoint; } else { throw new AuthenticationException( String.format("%s is not a valid channel binding", ChannelBinding.NONE)); } } + + private byte[] getServerEndPointChannelBinding(final SSLSession session) + throws AuthenticationException { + final Certificate[] certificates; + try { + certificates = session.getPeerCertificates(); + } catch (final SSLPeerUnverifiedException e) { + throw new AuthenticationException("Could not verify peer certificates"); + } + if (certificates == null || certificates.length == 0) { + throw new AuthenticationException("Could not retrieve peer certificate"); + } + final X509Certificate certificate; + if (certificates[0] instanceof X509Certificate) { + certificate = (X509Certificate) certificates[0]; + } else { + throw new AuthenticationException("Certificate was not X509"); + } + final String algorithm = certificate.getSigAlgName(); + final int withIndex = algorithm.indexOf("with"); + if (withIndex <= 0) { + throw new AuthenticationException("Unable to parse SigAlgName"); + } + final String hashAlgorithm = algorithm.substring(0, withIndex); + final MessageDigest messageDigest; + // https://www.rfc-editor.org/rfc/rfc5929#section-4.1 + if ("MD5".equalsIgnoreCase(hashAlgorithm) || "SHA1".equalsIgnoreCase(hashAlgorithm)) { + messageDigest = new SHA256.Digest(); + } else { + try { + messageDigest = MessageDigest.getInstance(hashAlgorithm); + } catch (final NoSuchAlgorithmException e) { + throw new AuthenticationException( + "Could not instantiate message digest for " + hashAlgorithm); + } + } + Log.d(Config.LOGTAG, "hashing certificate with " + messageDigest.getAlgorithm()); + final byte[] encodedCertificate; + try { + encodedCertificate = certificate.getEncoded(); + } catch (final CertificateEncodingException e) { + throw new AuthenticationException("Could not encode certificate"); + } + messageDigest.update(encodedCertificate); + return messageDigest.digest(); + } } From ecbfe33e8d4b86603c343f6c92324211e7b76261 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 7 Sep 2022 12:08:50 +0200 Subject: [PATCH 035/101] support end-point channel binding as last choice option --- .../siacs/conversations/crypto/sasl/ChannelBinding.java | 2 ++ .../conversations/crypto/sasl/ScramPlusMechanism.java | 8 +------- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java index 81bd1270527b547a377c47432b8f2e0499976059..c9211c89873900c2295482605aca88d2b1b6a462 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java @@ -32,6 +32,8 @@ public enum ChannelBinding { return TLS_EXPORTER; } else if (bindings.contains(TLS_UNIQUE)) { return TLS_UNIQUE; + } else if (bindings.contains(TLS_SERVER_END_POINT)) { + return TLS_SERVER_END_POINT; } else { return null; } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java index 8de4524f2a3c28b340073911fef18cb6025f6ede..8b23e9c9245e3de5ca7876b987c8560a3f85216c 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java @@ -1,7 +1,5 @@ package eu.siacs.conversations.crypto.sasl; -import android.util.Log; - import org.bouncycastle.jcajce.provider.digest.SHA256; import org.conscrypt.Conscrypt; @@ -16,9 +14,7 @@ import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocket; -import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.utils.CryptoHelper; abstract class ScramPlusMechanism extends ScramMechanism { @@ -56,11 +52,10 @@ abstract class ScramPlusMechanism extends ScramMechanism { return unique; } else if (this.channelBinding == ChannelBinding.TLS_SERVER_END_POINT) { final byte[] endPoint = getServerEndPointChannelBinding(sslSocket.getSession()); - Log.d(Config.LOGTAG, "retrieved endpoint " + CryptoHelper.bytesToHex(endPoint)); return endPoint; } else { throw new AuthenticationException( - String.format("%s is not a valid channel binding", ChannelBinding.NONE)); + String.format("%s is not a valid channel binding", channelBinding)); } } @@ -99,7 +94,6 @@ abstract class ScramPlusMechanism extends ScramMechanism { "Could not instantiate message digest for " + hashAlgorithm); } } - Log.d(Config.LOGTAG, "hashing certificate with " + messageDigest.getAlgorithm()); final byte[] encodedCertificate; try { encodedCertificate = certificate.getEncoded(); From f7996a6c3c7fe23eeb2b005aec56eaf2b6e50397 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 7 Sep 2022 16:29:51 +0200 Subject: [PATCH 036/101] catch illegal state exception when copying file --- .../conversations/persistance/FileBackend.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index 0d1c03fcb2462df754fab7f6c4c556832d31518d..2d5496f2f6b4755ff1e553182763531c71d641fd 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -684,7 +684,7 @@ public class FileBackend { } catch (final FileWriterException e) { cleanup(file); throw new FileCopyException(R.string.error_unable_to_create_temporary_file); - } catch (final SecurityException e) { + } catch (final SecurityException | IllegalStateException e) { cleanup(file); throw new FileCopyException(R.string.error_security_exception); } catch (final IOException e) { @@ -1576,19 +1576,19 @@ public class FileBackend { return 0; } return Integer.parseInt(value); - } catch (final IllegalArgumentException e) { + } catch (final Exception e) { return 0; } } private Dimensions getImageDimensions(File file) { - BitmapFactory.Options options = new BitmapFactory.Options(); + final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeFile(file.getAbsolutePath(), options); - int rotation = getRotation(file); - boolean rotated = rotation == 90 || rotation == 270; - int imageHeight = rotated ? options.outWidth : options.outHeight; - int imageWidth = rotated ? options.outHeight : options.outWidth; + final int rotation = getRotation(file); + final boolean rotated = rotation == 90 || rotation == 270; + final int imageHeight = rotated ? options.outWidth : options.outHeight; + final int imageWidth = rotated ? options.outHeight : options.outWidth; return new Dimensions(imageHeight, imageWidth); } From a95d0fa8d368e12a6b191b20b47541694141210d Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 9 Sep 2022 16:55:47 +0200 Subject: [PATCH 037/101] use resolveActivityInfo to display nagivate to button resolveActivity on the other hand only finds apps that are category_default fixes #4375 --- src/main/AndroidManifest.xml | 3 + .../ui/ShowLocationActivity.java | 403 +++++++++--------- 2 files changed, 213 insertions(+), 193 deletions(-) diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index e37b5ab36d073400739972f10ca98695959944ff..24265da618d211defb27384cb963fc236a4a3a0a 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -64,6 +64,9 @@ + + + diff --git a/src/main/java/eu/siacs/conversations/ui/ShowLocationActivity.java b/src/main/java/eu/siacs/conversations/ui/ShowLocationActivity.java index 43c55de4944dc1e951d5f23d51949ac72e18a9ce..d4b6a2e30e43f14abe06ef436e6c63ba27248a48 100644 --- a/src/main/java/eu/siacs/conversations/ui/ShowLocationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ShowLocationActivity.java @@ -3,8 +3,8 @@ package eu.siacs.conversations.ui; import android.content.ActivityNotFoundException; import android.content.ClipData; import android.content.ClipboardManager; -import android.content.ComponentName; import android.content.Intent; +import android.content.pm.ActivityInfo; import android.location.Location; import android.location.LocationListener; import android.net.Uri; @@ -17,6 +17,7 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.databinding.DataBindingUtil; +import org.jetbrains.annotations.NotNull; import org.osmdroid.util.GeoPoint; import java.util.HashMap; @@ -32,198 +33,214 @@ import eu.siacs.conversations.ui.widget.Marker; import eu.siacs.conversations.ui.widget.MyLocation; import eu.siacs.conversations.utils.LocationProvider; - public class ShowLocationActivity extends LocationActivity implements LocationListener { - private GeoPoint loc = LocationProvider.FALLBACK; - private ActivityShowLocationBinding binding; - - - private Uri createGeoUri() { - return Uri.parse("geo:" + this.loc.getLatitude() + "," + this.loc.getLongitude()); - } - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - this.binding = DataBindingUtil.setContentView(this,R.layout.activity_show_location); - setSupportActionBar(binding.toolbar); - - configureActionBar(getSupportActionBar()); - setupMapView(this.binding.map, this.loc); - - this.binding.fab.setOnClickListener(view -> startNavigation()); - - final Intent intent = getIntent(); - if (intent != null) { - final String action = intent.getAction(); - if (action == null) { - return; - } - switch (action) { - case "eu.siacs.conversations.location.show": - if (intent.hasExtra("longitude") && intent.hasExtra("latitude")) { - final double longitude = intent.getDoubleExtra("longitude", 0); - final double latitude = intent.getDoubleExtra("latitude", 0); - this.loc = new GeoPoint(latitude, longitude); - } - break; - case Intent.ACTION_VIEW: - final Uri geoUri = intent.getData(); - - // Attempt to set zoom level if the geo URI specifies it - if (geoUri != null) { - final HashMap query = UriHelper.parseQueryString(geoUri.getQuery()); - - // Check for zoom level. - final String z = query.get("z"); - if (z != null) { - try { - mapController.setZoom(Double.valueOf(z)); - } catch (final Exception ignored) { - } - } - - // Check for the actual geo query. - boolean posInQuery = false; - final String q = query.get("q"); - if (q != null) { - final Pattern latlng = Pattern.compile("/^([-+]?[0-9]+(\\.[0-9]+)?),([-+]?[0-9]+(\\.[0-9]+)?)(\\(.*\\))?/"); - final Matcher m = latlng.matcher(q); - if (m.matches()) { - try { - this.loc = new GeoPoint(Double.valueOf(m.group(1)), Double.valueOf(m.group(3))); - posInQuery = true; - } catch (final Exception ignored) { - } - } - } - - final String schemeSpecificPart = geoUri.getSchemeSpecificPart(); - if (schemeSpecificPart != null && !schemeSpecificPart.isEmpty()) { - try { - final GeoPoint latlong = LocationHelper.parseLatLong(schemeSpecificPart); - if (latlong != null && !posInQuery) { - this.loc = latlong; - } - } catch (final NumberFormatException ignored) { - } - } - } - - break; - } - updateLocationMarkers(); - } - } - - @Override - protected void gotoLoc(final boolean setZoomLevel) { - if (this.loc != null && mapController != null) { - if (setZoomLevel) { - mapController.setZoom(Config.Map.FINAL_ZOOM_LEVEL); - } - mapController.animateTo(new GeoPoint(this.loc)); - } - } - - @Override - public void onRequestPermissionsResult(final int requestCode, - @NonNull final String[] permissions, - @NonNull final int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - updateUi(); - } - - @Override - protected void setMyLoc(final Location location) { - this.myLoc = location; - } - - @Override - public boolean onCreateOptionsMenu(final Menu menu) { - // Inflate the menu; this adds items to the action bar if it is present. - getMenuInflater().inflate(R.menu.menu_show_location, menu); - updateUi(); - return true; - } - - @Override - protected void updateLocationMarkers() { - super.updateLocationMarkers(); - if (this.myLoc != null) { - this.binding.map.getOverlays().add(new MyLocation(this, null, this.myLoc)); - } - this.binding.map.getOverlays().add(new Marker(this.marker_icon, this.loc)); - } - - @Override - protected void onPause() { - super.onPause(); - } - - @Override - public boolean onOptionsItemSelected(final MenuItem item) { - switch (item.getItemId()) { - case R.id.action_copy_location: - final ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); - if (clipboard != null) { - final ClipData clip = ClipData.newPlainText("location", createGeoUri().toString()); - clipboard.setPrimaryClip(clip); - Toast.makeText(this,R.string.url_copied_to_clipboard,Toast.LENGTH_SHORT).show(); - } - return true; - case R.id.action_share_location: - final Intent shareIntent = new Intent(); - shareIntent.setAction(Intent.ACTION_SEND); - shareIntent.putExtra(Intent.EXTRA_TEXT, createGeoUri().toString()); - shareIntent.setType("text/plain"); - try { - startActivity(Intent.createChooser(shareIntent, getText(R.string.share_with))); - } catch (final ActivityNotFoundException e) { - //This should happen only on faulty androids because normally chooser is always available - Toast.makeText(this, R.string.no_application_found_to_open_file, Toast.LENGTH_SHORT).show(); - } - return true; - } - return super.onOptionsItemSelected(item); - } - - private void startNavigation() { - startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse( - "google.navigation:q=" + - this.loc.getLatitude() + "," + this.loc.getLongitude() - ))); - } - - @Override - protected void updateUi() { - final Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse("google.navigation:q=0,0")); - final ComponentName component = i.resolveActivity(getPackageManager()); - this.binding.fab.setVisibility(component == null ? View.GONE : View.VISIBLE); - } - - @Override - public void onLocationChanged(final Location location) { - if (LocationHelper.isBetterLocation(location, this.myLoc)) { - this.myLoc = location; - updateLocationMarkers(); - } - } - - @Override - public void onStatusChanged(final String provider, final int status, final Bundle extras) { - - } - - @Override - public void onProviderEnabled(final String provider) { - - } - - @Override - public void onProviderDisabled(final String provider) { - - } + private GeoPoint loc = LocationProvider.FALLBACK; + private ActivityShowLocationBinding binding; + + private Uri createGeoUri() { + return Uri.parse("geo:" + this.loc.getLatitude() + "," + this.loc.getLongitude()); + } + + @Override + protected void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + this.binding = DataBindingUtil.setContentView(this, R.layout.activity_show_location); + setSupportActionBar(binding.toolbar); + + configureActionBar(getSupportActionBar()); + setupMapView(this.binding.map, this.loc); + + this.binding.fab.setOnClickListener(view -> startNavigation()); + + final Intent intent = getIntent(); + if (intent != null) { + final String action = intent.getAction(); + if (action == null) { + return; + } + switch (action) { + case "eu.siacs.conversations.location.show": + if (intent.hasExtra("longitude") && intent.hasExtra("latitude")) { + final double longitude = intent.getDoubleExtra("longitude", 0); + final double latitude = intent.getDoubleExtra("latitude", 0); + this.loc = new GeoPoint(latitude, longitude); + } + break; + case Intent.ACTION_VIEW: + final Uri geoUri = intent.getData(); + + // Attempt to set zoom level if the geo URI specifies it + if (geoUri != null) { + final HashMap query = + UriHelper.parseQueryString(geoUri.getQuery()); + + // Check for zoom level. + final String z = query.get("z"); + if (z != null) { + try { + mapController.setZoom(Double.valueOf(z)); + } catch (final Exception ignored) { + } + } + + // Check for the actual geo query. + boolean posInQuery = false; + final String q = query.get("q"); + if (q != null) { + final Pattern latlng = + Pattern.compile( + "/^([-+]?[0-9]+(\\.[0-9]+)?),([-+]?[0-9]+(\\.[0-9]+)?)(\\(.*\\))?/"); + final Matcher m = latlng.matcher(q); + if (m.matches()) { + try { + this.loc = + new GeoPoint( + Double.valueOf(m.group(1)), + Double.valueOf(m.group(3))); + posInQuery = true; + } catch (final Exception ignored) { + } + } + } + + final String schemeSpecificPart = geoUri.getSchemeSpecificPart(); + if (schemeSpecificPart != null && !schemeSpecificPart.isEmpty()) { + try { + final GeoPoint latlong = + LocationHelper.parseLatLong(schemeSpecificPart); + if (latlong != null && !posInQuery) { + this.loc = latlong; + } + } catch (final NumberFormatException ignored) { + } + } + } + + break; + } + updateLocationMarkers(); + } + } + + @Override + protected void gotoLoc(final boolean setZoomLevel) { + if (this.loc != null && mapController != null) { + if (setZoomLevel) { + mapController.setZoom(Config.Map.FINAL_ZOOM_LEVEL); + } + mapController.animateTo(new GeoPoint(this.loc)); + } + } + + @Override + public void onRequestPermissionsResult( + final int requestCode, + @NonNull final String[] permissions, + @NonNull final int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + updateUi(); + } + + @Override + protected void setMyLoc(final Location location) { + this.myLoc = location; + } + + @Override + public boolean onCreateOptionsMenu(@NotNull final Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.menu_show_location, menu); + updateUi(); + return true; + } + + @Override + protected void updateLocationMarkers() { + super.updateLocationMarkers(); + if (this.myLoc != null) { + this.binding.map.getOverlays().add(new MyLocation(this, null, this.myLoc)); + } + this.binding.map.getOverlays().add(new Marker(this.marker_icon, this.loc)); + } + + @Override + protected void onPause() { + super.onPause(); + } + + @Override + public boolean onOptionsItemSelected(final MenuItem item) { + switch (item.getItemId()) { + case R.id.action_copy_location: + final ClipboardManager clipboard = + (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); + if (clipboard != null) { + final ClipData clip = + ClipData.newPlainText("location", createGeoUri().toString()); + clipboard.setPrimaryClip(clip); + Toast.makeText(this, R.string.url_copied_to_clipboard, Toast.LENGTH_SHORT) + .show(); + } + return true; + case R.id.action_share_location: + final Intent shareIntent = new Intent(); + shareIntent.setAction(Intent.ACTION_SEND); + shareIntent.putExtra(Intent.EXTRA_TEXT, createGeoUri().toString()); + shareIntent.setType("text/plain"); + try { + startActivity(Intent.createChooser(shareIntent, getText(R.string.share_with))); + } catch (final ActivityNotFoundException e) { + // This should happen only on faulty androids because normally chooser is always + // available + Toast.makeText( + this, + R.string.no_application_found_to_open_file, + Toast.LENGTH_SHORT) + .show(); + } + return true; + } + return super.onOptionsItemSelected(item); + } + + private void startNavigation() { + final Intent intent = getStartNavigationIntent(); + startActivity(intent); + } + + private Intent getStartNavigationIntent() { + return new Intent( + Intent.ACTION_VIEW, + Uri.parse( + "google.navigation:q=" + + this.loc.getLatitude() + + "," + + this.loc.getLongitude())); + } + + @Override + protected void updateUi() { + final Intent intent = getStartNavigationIntent(); + final ActivityInfo activityInfo = intent.resolveActivityInfo(getPackageManager(), 0); + this.binding.fab.setVisibility(activityInfo == null ? View.GONE : View.VISIBLE); + } + + @Override + public void onLocationChanged(@NotNull final Location location) { + if (LocationHelper.isBetterLocation(location, this.myLoc)) { + this.myLoc = location; + updateLocationMarkers(); + } + } + + @Override + public void onStatusChanged(final String provider, final int status, final Bundle extras) {} + + @Override + public void onProviderEnabled(@NotNull final String provider) {} + + @Override + public void onProviderDisabled(@NotNull final String provider) {} } From 82316d13b09eeb9184456013e62a6a6956cb6f98 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 9 Sep 2022 19:06:37 +0200 Subject: [PATCH 038/101] use weak reference to activity when using threads fixes #4366 --- .../conversations/ui/RecordingActivity.java | 60 +++++++++++-------- .../conversations/ui/RtpSessionActivity.java | 42 +++++++++---- 2 files changed, 66 insertions(+), 36 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java b/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java index bc99723167fc2c07d896929c7ae3af28b0e2bbac..6c58a404d3011f875b380054e066982177a3fb30 100644 --- a/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java @@ -18,6 +18,7 @@ import android.widget.Toast; import androidx.databinding.DataBindingUtil; import java.io.File; +import java.lang.ref.WeakReference; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; @@ -136,30 +137,41 @@ public class RecordingActivity extends Activity implements View.OnClickListener } } if (saveFile) { - new Thread( - () -> { - try { - if (!outputFileWrittenLatch.await(2, TimeUnit.SECONDS)) { - Log.d( - Config.LOGTAG, - "time out waiting for output file to be written"); - } - } catch (InterruptedException e) { - Log.d( - Config.LOGTAG, - "interrupted while waiting for output file to be written", - e); - } - runOnUiThread( - () -> { - setResult( - Activity.RESULT_OK, - new Intent() - .setData(Uri.fromFile(mOutputFile))); - finish(); - }); - }) - .start(); + new Thread(new Finisher(outputFileWrittenLatch, mOutputFile, this)).start(); + } + } + + private static class Finisher implements Runnable { + + private final CountDownLatch latch; + private final File outputFile; + private final WeakReference activityReference; + + private Finisher(CountDownLatch latch, File outputFile, Activity activity) { + this.latch = latch; + this.outputFile = outputFile; + this.activityReference = new WeakReference<>(activity); + } + + @Override + public void run() { + try { + if (!latch.await(5, TimeUnit.SECONDS)) { + Log.d(Config.LOGTAG, "time out waiting for output file to be written"); + } + } catch (final InterruptedException e) { + Log.d(Config.LOGTAG, "interrupted while waiting for output file to be written", e); + } + final Activity activity = activityReference.get(); + if (activity == null) { + return; + } + activity.runOnUiThread( + () -> { + activity.setResult( + Activity.RESULT_OK, new Intent().setData(Uri.fromFile(outputFile))); + activity.finish(); + }); } } diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index cbf00d04b83b10bdb4712eb3bf2e5ddc2f01fa96..e73fdb23c72cb536a0a33481646abfe4b87a9ac2 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -5,6 +5,7 @@ import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied; import android.Manifest; import android.annotation.SuppressLint; +import android.app.Activity; import android.app.PictureInPictureParams; import android.content.ActivityNotFoundException; import android.content.Context; @@ -297,21 +298,38 @@ public class RtpSessionActivity extends XmppActivity } private void checkMicrophoneAvailabilityAsync() { - new Thread(this::checkMicrophoneAvailability).start(); + new Thread(new MicrophoneAvailabilityCheck(this)).start(); } - private void checkMicrophoneAvailability() { - final long start = SystemClock.elapsedRealtime(); - final boolean isMicrophoneAvailable = AppRTCAudioManager.isMicrophoneAvailable(); - final long stop = SystemClock.elapsedRealtime(); - Log.d(Config.LOGTAG, "checking microphone availability took " + (stop - start) + "ms"); - if (isMicrophoneAvailable) { - return; + private static class MicrophoneAvailabilityCheck implements Runnable { + + private final WeakReference activityReference; + + private MicrophoneAvailabilityCheck(final Activity activity) { + this.activityReference = new WeakReference<>(activity); + } + + @Override + public void run() { + final long start = SystemClock.elapsedRealtime(); + final boolean isMicrophoneAvailable = AppRTCAudioManager.isMicrophoneAvailable(); + final long stop = SystemClock.elapsedRealtime(); + Log.d(Config.LOGTAG, "checking microphone availability took " + (stop - start) + "ms"); + if (isMicrophoneAvailable) { + return; + } + final Activity activity = activityReference.get(); + if (activity == null) { + return; + } + activity.runOnUiThread( + () -> + Toast.makeText( + activity, + R.string.microphone_unavailable, + Toast.LENGTH_LONG) + .show()); } - runOnUiThread( - () -> - Toast.makeText(this, R.string.microphone_unavailable, Toast.LENGTH_LONG) - .show()); } private void putScreenInCallMode() { From 6e53ab36949a4b8e7d3307504bb92d4e5d501938 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 10 Sep 2022 12:36:35 +0200 Subject: [PATCH 039/101] allow invite only when muc is online. fixes #4218 --- src/main/java/eu/siacs/conversations/entities/MucOptions.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java index 060b1b6f65ceaf9335ec8439bbdd8dcdfaa26ca3..cc1c358de5288bc69117f81e1c5e67ffe3f84dd2 100644 --- a/src/main/java/eu/siacs/conversations/entities/MucOptions.java +++ b/src/main/java/eu/siacs/conversations/entities/MucOptions.java @@ -156,7 +156,8 @@ public class MucOptions { } public boolean canInvite() { - return !membersOnly() || self.getRole().ranks(Role.MODERATOR) || allowInvites(); + final boolean hasPermission = !membersOnly() || self.getRole().ranks(Role.MODERATOR) || allowInvites(); + return hasPermission && online(); } public boolean allowInvites() { From d0efe6eae2fb79d0f96373ffc51b7a40c2f3ff80 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 14 Sep 2022 12:27:02 +0200 Subject: [PATCH 040/101] bump recording wait for write to 8s --- .../java/eu/siacs/conversations/ui/RecordingActivity.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java b/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java index 6c58a404d3011f875b380054e066982177a3fb30..ad8684b72658039fed302f46a3203875a9caa473 100644 --- a/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java @@ -156,7 +156,7 @@ public class RecordingActivity extends Activity implements View.OnClickListener @Override public void run() { try { - if (!latch.await(5, TimeUnit.SECONDS)) { + if (!latch.await(8, TimeUnit.SECONDS)) { Log.d(Config.LOGTAG, "time out waiting for output file to be written"); } } catch (final InterruptedException e) { @@ -199,7 +199,7 @@ public class RecordingActivity extends Activity implements View.OnClickListener setupFileObserver(parentDirectory); } - private void setupFileObserver(File directory) { + private void setupFileObserver(final File directory) { mFileObserver = new FileObserver(directory.getAbsolutePath()) { @Override @@ -219,7 +219,7 @@ public class RecordingActivity extends Activity implements View.OnClickListener } @Override - public void onClick(View view) { + public void onClick(final View view) { switch (view.getId()) { case R.id.cancel_button: mHandler.removeCallbacks(mTickExecutor); From c1abca35da2fd8887fe0dd85935a63a930accba0 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 14 Sep 2022 12:49:15 +0200 Subject: [PATCH 041/101] copy bookmarks before passing them to other parts of the app for read closes #4381 --- src/main/java/eu/siacs/conversations/Config.java | 2 +- .../java/eu/siacs/conversations/entities/Account.java | 9 ++++++--- .../conversations/services/XmppConnectionService.java | 8 ++++---- .../conversations/ui/StartConversationActivity.java | 4 ++-- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index f7c3dd151c65b86b870659eb0eff1707e899a520..a3eacc9db4b9fc55be92c5f177129085952f14fe 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -57,7 +57,7 @@ public final class Config { public static final long CONTACT_SYNC_RETRY_INTERVAL = 1000L * 60 * 5; - public static final boolean SASL_2_ENABLED = false; + public static final boolean SASL_2_ENABLED = true; //Notification settings public static final boolean HIDE_MESSAGE_TEXT_IN_NOTIFICATION = false; diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index 9220cc192fd054e53e3ea908692182d60c6c3e41..10f6ed8a893dba4a7bdf82ee9562e46f56da84dd 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -6,6 +6,7 @@ import android.os.SystemClock; import android.util.Log; import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; import org.json.JSONException; import org.json.JSONObject; @@ -488,17 +489,19 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable } public Collection getBookmarks() { - return this.bookmarks.values(); + synchronized (this.bookmarks) { + return ImmutableList.copyOf(this.bookmarks.values()); + } } - public void setBookmarks(Map bookmarks) { + public void setBookmarks(final Map bookmarks) { synchronized (this.bookmarks) { this.bookmarks.clear(); this.bookmarks.putAll(bookmarks); } } - public void putBookmark(Bookmark bookmark) { + public void putBookmark(final Bookmark bookmark) { synchronized (this.bookmarks) { this.bookmarks.put(bookmark.getJid(), bookmark); } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 586b717ff79445fdaee1b3b038d9faa5be24962a..e4af37947ffafc567eee0d2552d84f8d403be7c1 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -1862,7 +1862,7 @@ public class XmppConnectionService extends Service { IqPacket iqPacket = new IqPacket(IqPacket.TYPE.SET); Element query = iqPacket.query("jabber:iq:private"); Element storage = query.addChild("storage", "storage:bookmarks"); - for (Bookmark bookmark : account.getBookmarks()) { + for (final Bookmark bookmark : account.getBookmarks()) { storage.addChild(bookmark); } sendIqPacket(account, iqPacket, mDefaultIqHandler); @@ -1870,8 +1870,8 @@ public class XmppConnectionService extends Service { private void pushBookmarksPep(Account account) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": pushing bookmarks via pep"); - Element storage = new Element("storage", "storage:bookmarks"); - for (Bookmark bookmark : account.getBookmarks()) { + final Element storage = new Element("storage", "storage:bookmarks"); + for (final Bookmark bookmark : account.getBookmarks()) { storage.addChild(bookmark); } pushNodeAndEnforcePublishOptions(account, Namespace.BOOKMARKS, storage, "current", PublishOptions.persistentWhitelistAccess()); @@ -4418,7 +4418,7 @@ public class XmppConnectionService extends Service { for (final Account account : accounts) { if (account.getXmppConnection() != null) { mucServers.addAll(account.getXmppConnection().getMucServers()); - for (Bookmark bookmark : account.getBookmarks()) { + for (final Bookmark bookmark : account.getBookmarks()) { final Jid jid = bookmark.getJid(); final String s = jid == null ? null : jid.getDomain().toEscapedString(); if (s != null) { diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java index 99479777963a8aca1551b4717f79d1f804bec0ef..91807295b9360aaef75e335136b13d75353ff484 100644 --- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java @@ -980,9 +980,9 @@ public class StartConversationActivity extends XmppActivity implements XmppConne protected void filterConferences(String needle) { this.conferences.clear(); - for (Account account : xmppConnectionService.getAccounts()) { + for (final Account account : xmppConnectionService.getAccounts()) { if (account.getStatus() != Account.State.DISABLED) { - for (Bookmark bookmark : account.getBookmarks()) { + for (final Bookmark bookmark : account.getBookmarks()) { if (bookmark.match(this, needle)) { this.conferences.add(bookmark); } From 9ae0475413334b33b713dae6b84a070c890d1d76 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Wed, 14 Sep 2022 10:13:17 -0500 Subject: [PATCH 042/101] Show the name of the sender in search results (#4379) Just like a MUC, search results lack the context to be sure who sent a message, so show the name in the result item. --- .../java/eu/siacs/conversations/ui/SearchActivity.java | 2 +- .../eu/siacs/conversations/ui/adapter/MessageAdapter.java | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/SearchActivity.java b/src/main/java/eu/siacs/conversations/ui/SearchActivity.java index f5f4eb175d7f650f8f10008d4d2a8e59b0b06414..ec279f58e44246312b053fd1d3dcb72e87e6a288 100644 --- a/src/main/java/eu/siacs/conversations/ui/SearchActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/SearchActivity.java @@ -97,7 +97,7 @@ public class SearchActivity extends XmppActivity implements TextWatcher, OnSearc this.binding = DataBindingUtil.setContentView(this, R.layout.activity_search); setSupportActionBar(this.binding.toolbar); configureActionBar(getSupportActionBar()); - this.messageListAdapter = new MessageAdapter(this, this.messages); + this.messageListAdapter = new MessageAdapter(this, this.messages, uuid == null); this.messageListAdapter.setOnContactPictureClicked(this); this.binding.searchResults.setAdapter(messageListAdapter); registerForContextMenu(this.binding.searchResults); diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index a5ba05819e1d2fa3bc77a9f91d7665923181c7fe..bb954f45ebe402fbc34045d7d25645eaaf251e15 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -87,6 +87,7 @@ public class MessageAdapter extends ArrayAdapter { private OnContactPictureClicked mOnContactPictureClickedListener; private OnContactPictureLongClicked mOnContactPictureLongClickedListener; private boolean mUseGreenBackground = false; + private boolean mForceNames = false; public MessageAdapter(XmppActivity activity, List messages) { super(activity, 0, messages); @@ -96,6 +97,10 @@ public class MessageAdapter extends ArrayAdapter { updatePreferences(); } + public MessageAdapter(XmppActivity activity, List messages, boolean forceNames) { + this(activity, messages); + mForceNames = forceNames; + } private static void resetClickListener(View... views) { for (View view : views) { @@ -233,7 +238,7 @@ public class MessageAdapter extends ArrayAdapter { error = true; break; default: - if (multiReceived) { + if (mForceNames || multiReceived) { info = UIHelper.getMessageDisplayName(message); } break; From 82efb6f1dbf290f86433416535179000cb084ba9 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 14 Sep 2022 17:51:22 +0200 Subject: [PATCH 043/101] code clean up --- .../siacs/conversations/entities/Account.java | 38 +++++++++---------- .../persistance/DatabaseBackend.java | 29 ++++++-------- .../ui/adapter/MessageAdapter.java | 10 ++--- 3 files changed, 36 insertions(+), 41 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index 10f6ed8a893dba4a7bdf82ee9562e46f56da84dd..1c16ab20bd8d04e2319daeb198c395a7d27deb8b 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -125,31 +125,31 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable public static Account fromCursor(final Cursor cursor) { final Jid jid; try { - String resource = cursor.getString(cursor.getColumnIndex(RESOURCE)); + String resource = cursor.getString(cursor.getColumnIndexOrThrow(RESOURCE)); jid = Jid.of( - cursor.getString(cursor.getColumnIndex(USERNAME)), - cursor.getString(cursor.getColumnIndex(SERVER)), + cursor.getString(cursor.getColumnIndexOrThrow(USERNAME)), + cursor.getString(cursor.getColumnIndexOrThrow(SERVER)), resource == null || resource.trim().isEmpty() ? null : resource); } catch (final IllegalArgumentException ignored) { - Log.d(Config.LOGTAG, cursor.getString(cursor.getColumnIndex(USERNAME)) + "@" + cursor.getString(cursor.getColumnIndex(SERVER))); + Log.d(Config.LOGTAG, cursor.getString(cursor.getColumnIndexOrThrow(USERNAME)) + "@" + cursor.getString(cursor.getColumnIndexOrThrow(SERVER))); throw new AssertionError(ignored); } - return new Account(cursor.getString(cursor.getColumnIndex(UUID)), + return new Account(cursor.getString(cursor.getColumnIndexOrThrow(UUID)), jid, - cursor.getString(cursor.getColumnIndex(PASSWORD)), - cursor.getInt(cursor.getColumnIndex(OPTIONS)), - cursor.getString(cursor.getColumnIndex(ROSTERVERSION)), - cursor.getString(cursor.getColumnIndex(KEYS)), - cursor.getString(cursor.getColumnIndex(AVATAR)), - cursor.getString(cursor.getColumnIndex(DISPLAY_NAME)), - cursor.getString(cursor.getColumnIndex(HOSTNAME)), - cursor.getInt(cursor.getColumnIndex(PORT)), - Presence.Status.fromShowString(cursor.getString(cursor.getColumnIndex(STATUS))), - cursor.getString(cursor.getColumnIndex(STATUS_MESSAGE))); - } - - public boolean httpUploadAvailable(long filesize) { - return xmppConnection != null && xmppConnection.getFeatures().httpUpload(filesize); + cursor.getString(cursor.getColumnIndexOrThrow(PASSWORD)), + cursor.getInt(cursor.getColumnIndexOrThrow(OPTIONS)), + cursor.getString(cursor.getColumnIndexOrThrow(ROSTERVERSION)), + cursor.getString(cursor.getColumnIndexOrThrow(KEYS)), + cursor.getString(cursor.getColumnIndexOrThrow(AVATAR)), + cursor.getString(cursor.getColumnIndexOrThrow(DISPLAY_NAME)), + cursor.getString(cursor.getColumnIndexOrThrow(HOSTNAME)), + cursor.getInt(cursor.getColumnIndexOrThrow(PORT)), + Presence.Status.fromShowString(cursor.getString(cursor.getColumnIndexOrThrow(STATUS))), + cursor.getString(cursor.getColumnIndexOrThrow(STATUS_MESSAGE))); + } + + public boolean httpUploadAvailable(long size) { + return xmppConnection != null && xmppConnection.getFeatures().httpUpload(size); } public boolean httpUploadAvailable() { diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index de9bc0d2ce32e5135bdb7613ea8e24bd4c89b85d..9e4bf9f8ef9ba5dd8dde2392c95253c48447eeb5 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -967,33 +967,28 @@ public class DatabaseBackend extends SQLiteOpenHelper { } public List getAccountJids(final boolean enabledOnly) { - SQLiteDatabase db = this.getReadableDatabase(); + final SQLiteDatabase db = this.getReadableDatabase(); final List jids = new ArrayList<>(); final String[] columns = new String[]{Account.USERNAME, Account.SERVER}; - String where = enabledOnly ? "not options & (1 <<1)" : null; - Cursor cursor = db.query(Account.TABLENAME, columns, where, null, null, null, null); - try { - while (cursor.moveToNext()) { + final String where = enabledOnly ? "not options & (1 <<1)" : null; + try (final Cursor cursor = db.query(Account.TABLENAME, columns, where, null, null, null, null)) { + while (cursor != null && cursor.moveToNext()) { jids.add(Jid.of(cursor.getString(0), cursor.getString(1), null)); } + } catch (final Exception e) { return jids; - } catch (Exception e) { - return jids; - } finally { - if (cursor != null) { - cursor.close(); - } } + return jids; } private List getAccounts(SQLiteDatabase db) { - List list = new ArrayList<>(); - Cursor cursor = db.query(Account.TABLENAME, null, null, null, null, - null, null); - while (cursor.moveToNext()) { - list.add(Account.fromCursor(cursor)); + final List list = new ArrayList<>(); + try (final Cursor cursor = + db.query(Account.TABLENAME, null, null, null, null, null, null)) { + while (cursor != null && cursor.moveToNext()) { + list.add(Account.fromCursor(cursor)); + } } - cursor.close(); return list; } diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index bb954f45ebe402fbc34045d7d25645eaaf251e15..fb7aec14deccafa65e3bffe0069710b31453b626 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -87,19 +87,19 @@ public class MessageAdapter extends ArrayAdapter { private OnContactPictureClicked mOnContactPictureClickedListener; private OnContactPictureLongClicked mOnContactPictureLongClickedListener; private boolean mUseGreenBackground = false; - private boolean mForceNames = false; + private final boolean mForceNames; - public MessageAdapter(XmppActivity activity, List messages) { + public MessageAdapter(final XmppActivity activity, final List messages, final boolean forceNames) { super(activity, 0, messages); this.audioPlayer = new AudioPlayer(this); this.activity = activity; metrics = getContext().getResources().getDisplayMetrics(); updatePreferences(); + this.mForceNames = forceNames; } - public MessageAdapter(XmppActivity activity, List messages, boolean forceNames) { - this(activity, messages); - mForceNames = forceNames; + public MessageAdapter(final XmppActivity activity, final List messages) { + this(activity, messages, false); } private static void resetClickListener(View... views) { From 495f79921dea64268caca789d1ee2e455e5f5d2c Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 15 Sep 2022 12:22:05 +0200 Subject: [PATCH 044/101] store full sasl mechanism (not just priority) --- .../conversations/ui/MagicCreateActivity.java | 2 +- .../crypto/sasl/ChannelBinding.java | 12 ++++ .../crypto/sasl/SaslMechanism.java | 6 ++ .../crypto/sasl/ScramPlusMechanism.java | 9 ++- .../siacs/conversations/entities/Account.java | 72 +++++++++++++++---- .../persistance/DatabaseBackend.java | 46 ++++++------ .../conversations/ui/EditAccountActivity.java | 6 +- .../conversations/xmpp/XmppConnection.java | 7 +- 8 files changed, 117 insertions(+), 43 deletions(-) diff --git a/src/conversations/java/eu/siacs/conversations/ui/MagicCreateActivity.java b/src/conversations/java/eu/siacs/conversations/ui/MagicCreateActivity.java index 6f03866729689457a6130c7bb524f3651fdb6e51..38761befdfdb02c6e23629c87ea51e67b90caff9 100644 --- a/src/conversations/java/eu/siacs/conversations/ui/MagicCreateActivity.java +++ b/src/conversations/java/eu/siacs/conversations/ui/MagicCreateActivity.java @@ -100,7 +100,7 @@ public class MagicCreateActivity extends XmppActivity implements TextWatcher { account.setOption(Account.OPTION_MAGIC_CREATE, true); account.setOption(Account.OPTION_FIXED_USERNAME, fixedUsername); if (this.preAuth != null) { - account.setKey(Account.PRE_AUTH_REGISTRATION_TOKEN, this.preAuth); + account.setKey(Account.KEY_PRE_AUTH_REGISTRATION_TOKEN, this.preAuth); } xmppConnectionService.createAccount(account); } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java index c9211c89873900c2295482605aca88d2b1b6a462..d8307a76d3eb057230e363ec6cff826a41a4a125 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java @@ -3,6 +3,7 @@ package eu.siacs.conversations.crypto.sasl; import android.util.Log; import com.google.common.base.CaseFormat; +import com.google.common.base.Strings; import java.util.Collection; @@ -27,6 +28,17 @@ public enum ChannelBinding { } } + public static ChannelBinding get(final String name) { + if (Strings.isNullOrEmpty(name)) { + return NONE; + } + try { + return valueOf(name); + } catch (final IllegalArgumentException e) { + return NONE; + } + } + public static ChannelBinding best(final Collection bindings) { if (bindings.contains(TLS_EXPORTER)) { return TLS_EXPORTER; diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java index aaff4cc8284a5ee5cdfc81a5e659cdd17c7dc952..e5b940b87330d38b1947e703a2430133b19be53e 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java @@ -3,6 +3,7 @@ package eu.siacs.conversations.crypto.sasl; import com.google.common.base.Strings; import java.util.Collection; +import java.util.Collections; import javax.net.ssl.SSLSocket; @@ -129,5 +130,10 @@ public abstract class SaslMechanism { return null; } } + + public SaslMechanism of(final String mechanism, final ChannelBinding channelBinding) { + return of(Collections.singleton(mechanism), Collections.singleton(channelBinding)); + } + } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java index 8b23e9c9245e3de5ca7876b987c8560a3f85216c..707883d734d26a936776e36648c9dbf9766723bb 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java @@ -16,7 +16,7 @@ import javax.net.ssl.SSLSocket; import eu.siacs.conversations.entities.Account; -abstract class ScramPlusMechanism extends ScramMechanism { +public abstract class ScramPlusMechanism extends ScramMechanism { private static final String EXPORTER_LABEL = "EXPORTER-Channel-Binding"; @@ -51,8 +51,7 @@ abstract class ScramPlusMechanism extends ScramMechanism { } return unique; } else if (this.channelBinding == ChannelBinding.TLS_SERVER_END_POINT) { - final byte[] endPoint = getServerEndPointChannelBinding(sslSocket.getSession()); - return endPoint; + return getServerEndPointChannelBinding(sslSocket.getSession()); } else { throw new AuthenticationException( String.format("%s is not a valid channel binding", channelBinding)); @@ -103,4 +102,8 @@ abstract class ScramPlusMechanism extends ScramMechanism { messageDigest.update(encodedCertificate); return messageDigest.digest(); } + + public ChannelBinding getChannelBinding() { + return this.channelBinding; + } } diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index 1c16ab20bd8d04e2319daeb198c395a7d27deb8b..8446abbbd50496e4b55e8e10fd20c47d782e92da 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -25,6 +25,9 @@ import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.PgpDecryptionService; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession; +import eu.siacs.conversations.crypto.sasl.ChannelBinding; +import eu.siacs.conversations.crypto.sasl.SaslMechanism; +import eu.siacs.conversations.crypto.sasl.ScramPlusMechanism; import eu.siacs.conversations.services.AvatarService; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.UIHelper; @@ -50,9 +53,9 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable public static final String STATUS = "status"; public static final String STATUS_MESSAGE = "status_message"; public static final String RESOURCE = "resource"; + public static final String PINNED_MECHANISM = "pinned_mechanism"; + public static final String PINNED_CHANNEL_BINDING = "pinned_channel_binding"; - public static final String PINNED_MECHANISM_KEY = "pinned_mechanism"; - public static final String PRE_AUTH_REGISTRATION_TOKEN = "pre_auth_registration"; public static final int OPTION_USETLS = 0; public static final int OPTION_DISABLED = 1; @@ -64,8 +67,13 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable public static final int OPTION_HTTP_UPLOAD_AVAILABLE = 7; public static final int OPTION_UNVERIFIED = 8; public static final int OPTION_FIXED_USERNAME = 9; + private static final String KEY_PGP_SIGNATURE = "pgp_signature"; private static final String KEY_PGP_ID = "pgp_id"; + private static final String KEY_PINNED_MECHANISM = "pinned_mechanism"; + public static final String KEY_PRE_AUTH_REGISTRATION_TOKEN = "pre_auth_registration"; + + protected final JSONObject keys; private final Roster roster = new Roster(this); private final Collection blocklist = new CopyOnWriteArraySet<>(); @@ -90,18 +98,20 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable private XmppConnection xmppConnection = null; private long mEndGracePeriod = 0L; private final Map bookmarks = new HashMap<>(); - private Presence.Status presenceStatus = Presence.Status.ONLINE; - private String presenceStatusMessage = null; + private Presence.Status presenceStatus; + private String presenceStatusMessage; + private String pinnedMechanism; + private String pinnedChannelBinding; public Account(final Jid jid, final String password) { this(java.util.UUID.randomUUID().toString(), jid, - password, 0, null, "", null, null, null, 5222, Presence.Status.ONLINE, null); + password, 0, null, "", null, null, null, 5222, Presence.Status.ONLINE, null, null, null); } private Account(final String uuid, final Jid jid, final String password, final int options, final String rosterVersion, final String keys, final String avatar, String displayName, String hostname, int port, - final Presence.Status status, String statusMessage) { + final Presence.Status status, String statusMessage, final String pinnedMechanism, final String pinnedChannelBinding) { this.uuid = uuid; this.jid = jid; this.password = password; @@ -120,19 +130,21 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable this.port = port; this.presenceStatus = status; this.presenceStatusMessage = statusMessage; + this.pinnedMechanism = pinnedMechanism; + this.pinnedChannelBinding = pinnedChannelBinding; } public static Account fromCursor(final Cursor cursor) { final Jid jid; try { - String resource = cursor.getString(cursor.getColumnIndexOrThrow(RESOURCE)); + final String resource = cursor.getString(cursor.getColumnIndexOrThrow(RESOURCE)); jid = Jid.of( cursor.getString(cursor.getColumnIndexOrThrow(USERNAME)), cursor.getString(cursor.getColumnIndexOrThrow(SERVER)), resource == null || resource.trim().isEmpty() ? null : resource); - } catch (final IllegalArgumentException ignored) { + } catch (final IllegalArgumentException e) { Log.d(Config.LOGTAG, cursor.getString(cursor.getColumnIndexOrThrow(USERNAME)) + "@" + cursor.getString(cursor.getColumnIndexOrThrow(SERVER))); - throw new AssertionError(ignored); + throw new AssertionError(e); } return new Account(cursor.getString(cursor.getColumnIndexOrThrow(UUID)), jid, @@ -145,7 +157,9 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable cursor.getString(cursor.getColumnIndexOrThrow(HOSTNAME)), cursor.getInt(cursor.getColumnIndexOrThrow(PORT)), Presence.Status.fromShowString(cursor.getString(cursor.getColumnIndexOrThrow(STATUS))), - cursor.getString(cursor.getColumnIndexOrThrow(STATUS_MESSAGE))); + cursor.getString(cursor.getColumnIndexOrThrow(STATUS_MESSAGE)), + cursor.getString(cursor.getColumnIndexOrThrow(PINNED_MECHANISM)), + cursor.getString(cursor.getColumnIndexOrThrow(PINNED_CHANNEL_BINDING))); } public boolean httpUploadAvailable(long size) { @@ -289,6 +303,38 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable } } + public void setPinnedMechanism(final SaslMechanism mechanism) { + this.pinnedMechanism = mechanism.getMechanism(); + if (mechanism instanceof ScramPlusMechanism) { + this.pinnedChannelBinding = ((ScramPlusMechanism) mechanism).getChannelBinding().toString(); + } + } + + public void resetPinnedMechanism() { + this.pinnedMechanism = null; + this.pinnedChannelBinding = null; + setKey(Account.KEY_PINNED_MECHANISM, String.valueOf(-1)); + } + + public int getPinnedMechanismPriority() { + final int fallback = getKeyAsInt(KEY_PINNED_MECHANISM, -1); + if (Strings.isNullOrEmpty(this.pinnedMechanism)) { + return fallback; + } + final SaslMechanism saslMechanism = getPinnedMechanism(); + if (saslMechanism == null) { + return fallback; + } else { + return saslMechanism.getPriority(); + } + } + + public SaslMechanism getPinnedMechanism() { + final String mechanism = Strings.nullToEmpty(this.pinnedMechanism); + final ChannelBinding channelBinding = ChannelBinding.get(this.pinnedChannelBinding); + return new SaslMechanism.Factory(this).of(mechanism, channelBinding); + } + public State getTrueStatus() { return this.status; } @@ -361,8 +407,8 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable } } - public boolean setPrivateKeyAlias(String alias) { - return setKey("private_key_alias", alias); + public void setPrivateKeyAlias(final String alias) { + setKey("private_key_alias", alias); } public String getPrivateKeyAlias() { @@ -388,6 +434,8 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable values.put(STATUS, presenceStatus.toShowString()); values.put(STATUS_MESSAGE, presenceStatusMessage); values.put(RESOURCE, jid.getResource()); + values.put(PINNED_MECHANISM, pinnedMechanism); + values.put(PINNED_CHANNEL_BINDING, pinnedChannelBinding); return values; } diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index 9e4bf9f8ef9ba5dd8dde2392c95253c48447eeb5..49de553ebc27190480530e527064e91a9d084e15 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -64,7 +64,7 @@ import eu.siacs.conversations.xmpp.mam.MamReference; public class DatabaseBackend extends SQLiteOpenHelper { private static final String DATABASE_NAME = "history"; - private static final int DATABASE_VERSION = 49; + private static final int DATABASE_VERSION = 50; private static boolean requiresMessageIndexRebuild = false; private static DatabaseBackend instance = null; @@ -230,6 +230,8 @@ public class DatabaseBackend extends SQLiteOpenHelper { + Account.KEYS + " TEXT, " + Account.HOSTNAME + " TEXT, " + Account.RESOURCE + " TEXT," + + Account.PINNED_MECHANISM + " TEXT," + + Account.PINNED_CHANNEL_BINDING + " TEXT," + Account.PORT + " NUMBER DEFAULT 5222)"); db.execSQL("create table " + Conversation.TABLENAME + " (" + Conversation.UUID + " TEXT PRIMARY KEY, " + Conversation.NAME @@ -589,6 +591,11 @@ public class DatabaseBackend extends SQLiteOpenHelper { db.endTransaction(); requiresMessageIndexRebuild = true; } + if (oldVersion < 50 && newVersion >= 50) { + db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.PINNED_MECHANISM + " TEXT"); + db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.PINNED_CHANNEL_BINDING + " TEXT"); + + } } private void canonicalizeJids(SQLiteDatabase db) { @@ -938,20 +945,19 @@ public class DatabaseBackend extends SQLiteOpenHelper { contactJid.asBareJid().toString() + "/%", contactJid.asBareJid().toString() }; - Cursor cursor = db.query(Conversation.TABLENAME, null, + try(final Cursor cursor = db.query(Conversation.TABLENAME, null, Conversation.ACCOUNT + "=? AND (" + Conversation.CONTACTJID - + " like ? OR " + Conversation.CONTACTJID + "=?)", selectionArgs, null, null, null); - if (cursor.getCount() == 0) { - cursor.close(); - return null; - } - cursor.moveToFirst(); - Conversation conversation = Conversation.fromCursor(cursor); - cursor.close(); - if (conversation.getJid() instanceof InvalidJid) { - return null; + + " like ? OR " + Conversation.CONTACTJID + "=?)", selectionArgs, null, null, null)) { + if (cursor.getCount() == 0) { + return null; + } + cursor.moveToFirst(); + final Conversation conversation = Conversation.fromCursor(cursor); + if (conversation.getJid() instanceof InvalidJid) { + return null; + } + return conversation; } - return conversation; } public void updateConversation(final Conversation conversation) { @@ -1024,14 +1030,14 @@ public class DatabaseBackend extends SQLiteOpenHelper { } public void readRoster(Roster roster) { - SQLiteDatabase db = this.getReadableDatabase(); - Cursor cursor; - String[] args = {roster.getAccount().getUuid()}; - cursor = db.query(Contact.TABLENAME, null, Contact.ACCOUNT + "=?", args, null, null, null); - while (cursor.moveToNext()) { - roster.initContact(Contact.fromCursor(cursor)); + final SQLiteDatabase db = this.getReadableDatabase(); + final String[] args = {roster.getAccount().getUuid()}; + try (final Cursor cursor = + db.query(Contact.TABLENAME, null, Contact.ACCOUNT + "=?", args, null, null, null)) { + while (cursor.moveToNext()) { + roster.initContact(Contact.fromCursor(cursor)); + } } - cursor.close(); } public void writeRoster(final Roster roster) { diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index 19424ee2bdf81aa753528bd2b8bf88e205af9a7e..8eee27627eaa0d590f167736e5df87268545835e 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -181,7 +181,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat } if (inNeedOfSaslAccept()) { - mAccount.setKey(Account.PINNED_MECHANISM_KEY, String.valueOf(-1)); + mAccount.resetPinnedMechanism(); if (!xmppConnectionService.updateAccount(mAccount)) { Toast.makeText(EditAccountActivity.this, R.string.unable_to_update_account, Toast.LENGTH_SHORT).show(); } @@ -421,7 +421,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat } else { preset = jid.getDomain(); } - final Intent intent = SignupUtils.getTokenRegistrationIntent(this, preset, mAccount.getKey(Account.PRE_AUTH_REGISTRATION_TOKEN)); + final Intent intent = SignupUtils.getTokenRegistrationIntent(this, preset, mAccount.getKey(Account.KEY_PRE_AUTH_REGISTRATION_TOKEN)); StartConversationActivity.addInviteUri(intent, getIntent()); startActivity(intent); return; @@ -892,7 +892,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat } private boolean inNeedOfSaslAccept() { - return mAccount != null && mAccount.getLastErrorStatus() == Account.State.DOWNGRADE_ATTACK && mAccount.getKeyAsInt(Account.PINNED_MECHANISM_KEY, -1) >= 0 && !accountInfoEdited(); + return mAccount != null && mAccount.getLastErrorStatus() == Account.State.DOWNGRADE_ATTACK && mAccount.getPinnedMechanismPriority() >= 0 && !accountInfoEdited(); } private void shareBarcode() { diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 5bcc99f136d65a4ee097e3e37c13e89122044adf..3bcec5a5a4bf8edfeae3b5b5c81286ce21051f59 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -692,8 +692,7 @@ public class XmppConnection implements Runnable { Log.d( Config.LOGTAG, account.getJid().asBareJid().toString() + ": logged in (using " + version + ")"); - // TODO store mechanism name - account.setKey(Account.PINNED_MECHANISM_KEY, String.valueOf(saslMechanism.getPriority())); + account.setPinnedMechanism(saslMechanism); if (version == SaslMechanism.Version.SASL_2) { final String authorizationIdentifier = success.findChildContent("authorization-identifier"); @@ -1264,7 +1263,7 @@ public class XmppConnection implements Runnable { + mechanisms); throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } - final int pinnedMechanism = account.getKeyAsInt(Account.PINNED_MECHANISM_KEY, -1); + final int pinnedMechanism = account.getPinnedMechanismPriority(); if (pinnedMechanism > saslMechanism.getPriority()) { Log.e( Config.LOGTAG, @@ -1345,7 +1344,7 @@ public class XmppConnection implements Runnable { } private void register() { - final String preAuth = account.getKey(Account.PRE_AUTH_REGISTRATION_TOKEN); + final String preAuth = account.getKey(Account.KEY_PRE_AUTH_REGISTRATION_TOKEN); if (preAuth != null && features.invite()) { final IqPacket preAuthRequest = new IqPacket(IqPacket.TYPE.SET); preAuthRequest.addChild("preauth", Namespace.PARS).setAttribute("token", preAuth); From bf15070fef52edb494c3496e2f75921091e69802 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 15 Sep 2022 13:10:15 +0200 Subject: [PATCH 045/101] bump sasl2 namespace --- .../java/eu/siacs/conversations/xml/Namespace.java | 2 +- .../eu/siacs/conversations/xmpp/XmppConnection.java | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index e9f9639ec1fbdd2fd52f1403436e2e3b5da8a17f..819e5fb2124233c458020cbd110e144ed025826b 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -16,7 +16,7 @@ public final class Namespace { public static final String DATA = "jabber:x:data"; public static final String OOB = "jabber:x:oob"; public static final String SASL = "urn:ietf:params:xml:ns:xmpp-sasl"; - public static final String SASL_2 = "urn:xmpp:sasl:1"; + public static final String SASL_2 = "urn:xmpp:sasl:2"; public static final String CHANNEL_BINDING = "urn:xmpp:sasl-cb:0"; public static final String TLS = "urn:ietf:params:xml:ns:xmpp-tls"; public static final String PUBSUB = "http://jabber.org/protocol/pubsub"; diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 3bcec5a5a4bf8edfeae3b5b5c81286ce21051f59..d0ea1349f1cca6a95b81d965992974536da7f93b 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -1187,7 +1187,7 @@ public class XmppConnection implements Runnable { && account.isOptionSet(Account.OPTION_REGISTER)) { throw new StateChangingException(Account.State.REGISTRATION_NOT_SUPPORTED); } else if (Config.SASL_2_ENABLED - && this.streamFeatures.hasChild("mechanisms", Namespace.SASL_2) + && this.streamFeatures.hasChild("authentication", Namespace.SASL_2) && shouldAuthenticate && isSecure) { authenticate(SaslMechanism.Version.SASL_2); @@ -1230,8 +1230,12 @@ public class XmppConnection implements Runnable { } private void authenticate(final SaslMechanism.Version version) throws IOException { - final Element element = - this.streamFeatures.findChild("mechanisms", SaslMechanism.namespace(version)); + final Element element; + if (version == SaslMechanism.Version.SASL) { + element = this.streamFeatures.findChild("mechanisms", Namespace.SASL); + } else { + element = this.streamFeatures.findChild("authentication", Namespace.SASL_2); + } final Collection mechanisms = Collections2.transform( Collections2.filter( From 5a3cca9554367ba0d4b9cb48990a57d5f69dcef1 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 15 Sep 2022 14:28:51 +0200 Subject: [PATCH 046/101] use bind 2 tag and sasl 2 user-agent --- .../conversations/utils/PhoneHelper.java | 68 +++++++++++++------ .../conversations/xmpp/XmppConnection.java | 19 ++++-- 2 files changed, 58 insertions(+), 29 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java b/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java index a894cab677c5bee769d5b1927b3eeccb9c3ce19b..9ff4925780945aec300c4edb6a298c9d1220fa93 100644 --- a/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java @@ -12,27 +12,51 @@ import android.provider.Settings; public class PhoneHelper { - @SuppressLint("HardwareIds") - public static String getAndroidId(Context context) { - return Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID); - } + @SuppressLint("HardwareIds") + public static String getAndroidId(Context context) { + return Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID); + } - public static Uri getProfilePictureUri(Context context) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && context.checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) { - return null; - } - final String[] projection = new String[]{Profile._ID, Profile.PHOTO_URI}; - final Cursor cursor; - try { - cursor = context.getContentResolver().query(Profile.CONTENT_URI, projection, null, null, null); - } catch (Throwable e) { - return null; - } - if (cursor == null) { - return null; - } - final String uri = cursor.moveToFirst() ? cursor.getString(1) : null; - cursor.close(); - return uri == null ? null : Uri.parse(uri); - } + public static Uri getProfilePictureUri(Context context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M + && context.checkSelfPermission(Manifest.permission.READ_CONTACTS) + != PackageManager.PERMISSION_GRANTED) { + return null; + } + final String[] projection = new String[] {Profile._ID, Profile.PHOTO_URI}; + final Cursor cursor; + try { + cursor = + context.getContentResolver() + .query(Profile.CONTENT_URI, projection, null, null, null); + } catch (Throwable e) { + return null; + } + if (cursor == null) { + return null; + } + final String uri = cursor.moveToFirst() ? cursor.getString(1) : null; + cursor.close(); + return uri == null ? null : Uri.parse(uri); + } + + public static boolean isEmulator() { + return (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")) + || Build.FINGERPRINT.startsWith("generic") + || Build.FINGERPRINT.startsWith("unknown") + || Build.HARDWARE.contains("goldfish") + || Build.HARDWARE.contains("ranchu") + || Build.MODEL.contains("google_sdk") + || Build.MODEL.contains("Emulator") + || Build.MODEL.contains("Android SDK built for x86") + || Build.MANUFACTURER.contains("Genymotion") + || Build.PRODUCT.contains("sdk_google") + || Build.PRODUCT.contains("google_sdk") + || Build.PRODUCT.contains("sdk") + || Build.PRODUCT.contains("sdk_x86") + || Build.PRODUCT.contains("sdk_gphone64_arm64") + || Build.PRODUCT.contains("vbox86p") + || Build.PRODUCT.contains("emulator") + || Build.PRODUCT.contains("simulator"); + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index d0ea1349f1cca6a95b81d965992974536da7f93b..d4335db50f488fe150e5b4f3c150704bffd7ae65 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -5,6 +5,7 @@ import static eu.siacs.conversations.utils.Random.SECURE_RANDOM; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; +import android.os.Build; import android.os.SystemClock; import android.security.KeyChain; import android.util.Base64; @@ -59,6 +60,7 @@ import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.X509KeyManager; import javax.net.ssl.X509TrustManager; +import eu.siacs.conversations.BuildConfig; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.XmppDomainVerifier; @@ -77,6 +79,7 @@ import eu.siacs.conversations.services.NotificationService; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.Patterns; +import eu.siacs.conversations.utils.PhoneHelper; import eu.siacs.conversations.utils.Resolver; import eu.siacs.conversations.utils.SSLSocketHelper; import eu.siacs.conversations.utils.SocksSocketFactory; @@ -1292,6 +1295,14 @@ public class XmppConnection implements Runnable { if (!Strings.isNullOrEmpty(firstMessage)) { authenticate.addChild("initial-response").setContent(firstMessage); } + final Element userAgent = authenticate.addChild("user-agent"); + userAgent.setAttribute("id", account.getUuid()); + userAgent.addChild("software").setContent(mXmppConnectionService.getString(R.string.app_name)); + if (!PhoneHelper.isEmulator()) { + userAgent + .addChild("device") + .setContent(String.format("%s %s", Build.MANUFACTURER, Build.MODEL)); + } final Element inline = this.streamFeatures.findChild("inline", Namespace.SASL_2); final boolean inlineStreamManagement = inline != null && inline.hasChild("sm", "urn:xmpp:sm:3"); @@ -1330,9 +1341,7 @@ public class XmppConnection implements Runnable { private Element generateBindRequest(final Collection bindFeatures) { Log.d(Config.LOGTAG, "inline bind features: " + bindFeatures); final Element bind = new Element("bind", Namespace.BIND2); - final Element clientId = bind.addChild("client-id"); - clientId.setAttribute("tag", mXmppConnectionService.getString(R.string.app_name)); - clientId.setContent(account.getUuid()); + bind.addChild("tag").setContent(mXmppConnectionService.getString(R.string.app_name)); final Element features = bind.addChild("features"); if (bindFeatures.contains(Namespace.CARBONS)) { features.addChild("enable", Namespace.CARBONS); @@ -1343,10 +1352,6 @@ public class XmppConnection implements Runnable { return bind; } - private static Collection extractMechanisms(final Element stream) { - return Collections2.transform(stream.getChildren(), c -> c == null ? null : c.getContent()); - } - private void register() { final String preAuth = account.getKey(Account.KEY_PRE_AUTH_REGISTRATION_TOKEN); if (preAuth != null && features.invite()) { From 9f5da6753929a1a368dabcb67f91c2cf461997f8 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 24 Sep 2022 11:59:53 +0200 Subject: [PATCH 047/101] use bind:0 namespace --- .../conversations/parser/MessageParser.java | 2 +- .../eu/siacs/conversations/xml/Namespace.java | 2 +- .../conversations/xmpp/XmppConnection.java | 32 +++++++++++-------- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 76945c472d7573ff22145de8dc25f5c4793b7a9b..46355354ab7a3843a8c0aba6731421abeb5c8ffe 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -233,7 +233,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece Element item = items.findChild("item"); Set deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received PEP device list " + deviceIds + " update from " + from + ", processing... "); - AxolotlService axolotlService = account.getAxolotlService(); + final AxolotlService axolotlService = account.getAxolotlService(); axolotlService.registerDevices(from, deviceIds); } else if (Namespace.BOOKMARKS.equals(node) && account.getJid().asBareJid().equals(from)) { if (account.getXmppConnection().getFeatures().bookmarksConversion()) { diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index 819e5fb2124233c458020cbd110e144ed025826b..7c7edde6eb98e39b51bf77736e524395cb74422c 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -26,7 +26,7 @@ public final class Namespace { public static final String NICK = "http://jabber.org/protocol/nick"; public static final String FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL = "http://jabber.org/protocol/offline"; public static final String BIND = "urn:ietf:params:xml:ns:xmpp-bind"; - public static final String BIND2 = "urn:xmpp:bind2:1"; + public static final String BIND2 = "urn:xmpp:bind:0"; public static final String STREAM_MANAGEMENT = "urn:xmpp:sm:3"; public static final String CSI = "urn:xmpp:csi:0"; public static final String CARBONS = "urn:xmpp:carbons:2"; diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index d4335db50f488fe150e5b4f3c150704bffd7ae65..5e4a6c0ed31c00fa57235003da386d889222d7b6 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -60,7 +60,6 @@ import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.X509KeyManager; import javax.net.ssl.X509TrustManager; -import eu.siacs.conversations.BuildConfig; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.XmppDomainVerifier; @@ -1233,16 +1232,16 @@ public class XmppConnection implements Runnable { } private void authenticate(final SaslMechanism.Version version) throws IOException { - final Element element; + final Element authElement; if (version == SaslMechanism.Version.SASL) { - element = this.streamFeatures.findChild("mechanisms", Namespace.SASL); + authElement = this.streamFeatures.findChild("mechanisms", Namespace.SASL); } else { - element = this.streamFeatures.findChild("authentication", Namespace.SASL_2); + authElement = this.streamFeatures.findChild("authentication", Namespace.SASL_2); } final Collection mechanisms = Collections2.transform( Collections2.filter( - element.getChildren(), + authElement.getChildren(), c -> c != null && "mechanism".equals(c.getName())), c -> c == null ? null : c.getContent()); final Element cbElement = @@ -1297,24 +1296,29 @@ public class XmppConnection implements Runnable { } final Element userAgent = authenticate.addChild("user-agent"); userAgent.setAttribute("id", account.getUuid()); - userAgent.addChild("software").setContent(mXmppConnectionService.getString(R.string.app_name)); + userAgent + .addChild("software") + .setContent(mXmppConnectionService.getString(R.string.app_name)); if (!PhoneHelper.isEmulator()) { userAgent .addChild("device") .setContent(String.format("%s %s", Build.MANUFACTURER, Build.MODEL)); } - final Element inline = this.streamFeatures.findChild("inline", Namespace.SASL_2); + final Element inline = authElement.findChild("inline", Namespace.SASL_2); final boolean inlineStreamManagement = inline != null && inline.hasChild("sm", "urn:xmpp:sm:3"); - final boolean inlineBind2 = inline != null && inline.hasChild("bind", Namespace.BIND2); - final Element inlineBindFeatures = - this.streamFeatures.findChild("inline", Namespace.BIND2); - if (inlineBind2 && inlineBindFeatures != null) { + final Element inlineBind2 = + inline != null ? inline.findChild("bind", Namespace.BIND2) : null; + final Element inlineBind2Inline = + inlineBind2 != null ? inlineBind2.findChild("inline", Namespace.BIND2) : null; + if (inlineBind2 != null) { final Element bind = generateBindRequest( - Collections2.transform( - inlineBindFeatures.getChildren(), - c -> c == null ? null : c.getAttribute("var"))); + inlineBind2Inline == null + ? Collections.emptyList() + : Collections2.transform( + inlineBind2Inline.getChildren(), + c -> c == null ? null : c.getAttribute("var"))); authenticate.addChild(bind); } if (inlineStreamManagement && streamId != null) { From 126e8ef08cb1146976da157b20947466ed3e2303 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 24 Sep 2022 14:58:49 +0200 Subject: [PATCH 048/101] refactor sasl 2 authentication code --- .../conversations/xmpp/XmppConnection.java | 84 +++++++++++-------- 1 file changed, 48 insertions(+), 36 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 5e4a6c0ed31c00fa57235003da386d889222d7b6..2ff1fc4060d44f3d12cd1f2cb05199911efd5bb3 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -1290,43 +1290,10 @@ public class XmppConnection implements Runnable { authenticate.setContent(firstMessage); } } else if (version == SaslMechanism.Version.SASL_2) { - authenticate = new Element("authenticate", Namespace.SASL_2); - if (!Strings.isNullOrEmpty(firstMessage)) { - authenticate.addChild("initial-response").setContent(firstMessage); - } - final Element userAgent = authenticate.addChild("user-agent"); - userAgent.setAttribute("id", account.getUuid()); - userAgent - .addChild("software") - .setContent(mXmppConnectionService.getString(R.string.app_name)); - if (!PhoneHelper.isEmulator()) { - userAgent - .addChild("device") - .setContent(String.format("%s %s", Build.MANUFACTURER, Build.MODEL)); - } final Element inline = authElement.findChild("inline", Namespace.SASL_2); - final boolean inlineStreamManagement = - inline != null && inline.hasChild("sm", "urn:xmpp:sm:3"); - final Element inlineBind2 = - inline != null ? inline.findChild("bind", Namespace.BIND2) : null; - final Element inlineBind2Inline = - inlineBind2 != null ? inlineBind2.findChild("inline", Namespace.BIND2) : null; - if (inlineBind2 != null) { - final Element bind = - generateBindRequest( - inlineBind2Inline == null - ? Collections.emptyList() - : Collections2.transform( - inlineBind2Inline.getChildren(), - c -> c == null ? null : c.getAttribute("var"))); - authenticate.addChild(bind); - } - if (inlineStreamManagement && streamId != null) { - final ResumePacket resume = new ResumePacket(this.streamId, stanzasReceived); - this.mSmCatchupMessageCounter.set(0); - this.mWaitingForSmCatchup.set(true); - authenticate.addChild(resume); - } + final boolean sm = inline != null && inline.hasChild("sm", "urn:xmpp:sm:3"); + final Collection bindFeatures = bindFeatures(inline); + authenticate = generateAuthenticationRequest(firstMessage, bindFeatures, sm); } else { throw new AssertionError("Missing implementation for " + version); } @@ -1342,6 +1309,51 @@ public class XmppConnection implements Runnable { tagWriter.writeElement(authenticate); } + private static Collection bindFeatures(final Element inline) { + final Element inlineBind2 = + inline != null ? inline.findChild("bind", Namespace.BIND2) : null; + final Element inlineBind2Inline = + inlineBind2 != null ? inlineBind2.findChild("inline", Namespace.BIND2) : null; + if (inlineBind2 == null) { + return null; + } + if (inlineBind2Inline == null) { + return Collections.emptyList(); + } + return Collections2.transform( + inlineBind2Inline.getChildren(), c -> c == null ? null : c.getAttribute("var")); + } + + private Element generateAuthenticationRequest( + final String firstMessage, + final Collection bind, + final boolean inlineStreamManagement) { + final Element authenticate = new Element("authenticate", Namespace.SASL_2); + if (!Strings.isNullOrEmpty(firstMessage)) { + authenticate.addChild("initial-response").setContent(firstMessage); + } + final Element userAgent = authenticate.addChild("user-agent"); + userAgent.setAttribute("id", account.getUuid()); + userAgent + .addChild("software") + .setContent(mXmppConnectionService.getString(R.string.app_name)); + if (!PhoneHelper.isEmulator()) { + userAgent + .addChild("device") + .setContent(String.format("%s %s", Build.MANUFACTURER, Build.MODEL)); + } + if (bind != null) { + authenticate.addChild(generateBindRequest(bind)); + } + if (inlineStreamManagement && streamId != null) { + final ResumePacket resume = new ResumePacket(this.streamId, stanzasReceived); + this.mSmCatchupMessageCounter.set(0); + this.mWaitingForSmCatchup.set(true); + authenticate.addChild(resume); + } + return authenticate; + } + private Element generateBindRequest(final Collection bindFeatures) { Log.d(Config.LOGTAG, "inline bind features: " + bindFeatures); final Element bind = new Element("bind", Namespace.BIND2); From 32f9a58d9ab5b2b88d433687d45bc8c9ecc0d43d Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 25 Sep 2022 14:13:04 +0200 Subject: [PATCH 049/101] pipeline sasl2 directly after stream start --- .../siacs/conversations/entities/Account.java | 4 +- .../conversations/ui/EditAccountActivity.java | 2 - .../eu/siacs/conversations/xml/Namespace.java | 1 + .../eu/siacs/conversations/xml/TagWriter.java | 11 +- .../conversations/xmpp/XmppConnection.java | 112 ++++++++++++------ .../siacs/conversations/xmpp/bind/Bind2.java | 33 ++++++ 6 files changed, 121 insertions(+), 42 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/xmpp/bind/Bind2.java diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index 8446abbbd50496e4b55e8e10fd20c47d782e92da..bbfacf420927efd9db4f01f2c78a7a3e4329c65f 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -57,16 +57,14 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable public static final String PINNED_CHANNEL_BINDING = "pinned_channel_binding"; - public static final int OPTION_USETLS = 0; public static final int OPTION_DISABLED = 1; public static final int OPTION_REGISTER = 2; - public static final int OPTION_USECOMPRESSION = 3; public static final int OPTION_MAGIC_CREATE = 4; public static final int OPTION_REQUIRES_ACCESS_MODE_CHANGE = 5; public static final int OPTION_LOGGED_IN_SUCCESSFULLY = 6; public static final int OPTION_HTTP_UPLOAD_AVAILABLE = 7; - public static final int OPTION_UNVERIFIED = 8; public static final int OPTION_FIXED_USERNAME = 9; + public static final int OPTION_QUICKSTART_AVAILABLE = 10; private static final String KEY_PGP_SIGNATURE = "pgp_signature"; private static final String KEY_PGP_ID = "pgp_id"; diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index 8eee27627eaa0d590f167736e5df87268545835e..8959d4c3897294c0839fd0d63d667fc2570a5468 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -286,8 +286,6 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat mAccount = new Account(jid.asBareJid(), password); mAccount.setPort(numericPort); mAccount.setHostname(hostname); - mAccount.setOption(Account.OPTION_USETLS, true); - mAccount.setOption(Account.OPTION_USECOMPRESSION, true); mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount); xmppConnectionService.createAccount(mAccount); } diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index 7c7edde6eb98e39b51bf77736e524395cb74422c..d17891ff2b138f8de3b2ca58fc6251f1d3d305f9 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -1,6 +1,7 @@ package eu.siacs.conversations.xml; public final class Namespace { + public static final String STREAMS = "http://etherx.jabber.org/streams"; public static final String DISCO_ITEMS = "http://jabber.org/protocol/disco#items"; public static final String DISCO_INFO = "http://jabber.org/protocol/disco#info"; public static final String EXTERNAL_SERVICE_DISCOVERY = "urn:xmpp:extdisco:2"; diff --git a/src/main/java/eu/siacs/conversations/xml/TagWriter.java b/src/main/java/eu/siacs/conversations/xml/TagWriter.java index 4f429377a166b3cf7d31075cb58ed0937a0a9c0c..5a9f3317c4bfae527ca498f915466bcd947fad97 100644 --- a/src/main/java/eu/siacs/conversations/xml/TagWriter.java +++ b/src/main/java/eu/siacs/conversations/xml/TagWriter.java @@ -58,15 +58,20 @@ public class TagWriter { throw new IOException("output stream was null"); } outputStream.write(""); - outputStream.flush(); } - public synchronized void writeTag(Tag tag) throws IOException { + public void writeTag(final Tag tag) throws IOException { + writeTag(tag, true); + } + + public synchronized void writeTag(final Tag tag, final boolean flush) throws IOException { if (outputStream == null) { throw new IOException("output stream was null"); } outputStream.write(tag.toString()); - outputStream.flush(); + if (flush) { + outputStream.flush(); + } } public synchronized void writeElement(Element element) throws IOException { diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 2ff1fc4060d44f3d12cd1f2cb05199911efd5bb3..911b226868a9b758d7c06988f95f87983d5b2975 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -89,6 +89,7 @@ import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xml.Tag; import eu.siacs.conversations.xml.TagWriter; import eu.siacs.conversations.xml.XmlReader; +import eu.siacs.conversations.xmpp.bind.Bind2; import eu.siacs.conversations.xmpp.forms.Data; import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived; import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; @@ -155,6 +156,7 @@ public class XmppConnection implements Runnable { private TagWriter tagWriter = new TagWriter(); private boolean shouldAuthenticate = true; private boolean inSmacksSession = false; + private boolean quickStartInProgress = false; private boolean isBound = false; private Element streamFeatures; private String streamId = null; @@ -270,11 +272,11 @@ public class XmppConnection implements Runnable { } Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": connecting"); features.encryptionEnabled = false; - inSmacksSession = false; - isBound = false; + this.inSmacksSession = false; + this.quickStartInProgress = false; + this.isBound = false; this.attempt++; - this.verifiedHostname = - null; // will be set if user entered hostname is being used or hostname was verified + this.verifiedHostname = null; // will be set if user entered hostname is being used or hostname was verified // with dnssec try { Socket localSocket; @@ -310,14 +312,14 @@ public class XmppConnection implements Runnable { try { startXmpp(localSocket); - } catch (InterruptedException e) { + } catch (final InterruptedException e) { Log.d( Config.LOGTAG, account.getJid().asBareJid() + ": thread was interrupted before beginning stream"); return; - } catch (Exception e) { - throw new IOException(e.getMessage()); + } catch (final Exception e) { + throw new IOException("Could not start stream", e); } } else { final String domain = account.getServer(); @@ -477,7 +479,7 @@ public class XmppConnection implements Runnable { * * @return true if server returns with valid xmpp, false otherwise */ - private boolean startXmpp(Socket socket) throws Exception { + private boolean startXmpp(final Socket socket) throws Exception { if (Thread.currentThread().isInterrupted()) { throw new InterruptedException(); } @@ -490,15 +492,22 @@ public class XmppConnection implements Runnable { tagWriter.setOutputStream(socket.getOutputStream()); tagReader.setInputStream(socket.getInputStream()); tagWriter.beginDocument(); - sendStartStream(); + final boolean quickStart; + if (socket instanceof SSLSocket) { + SSLSocketHelper.log(account, (SSLSocket) socket); + quickStart = establishStream(true); + } else { + quickStart = establishStream(false); + } final Tag tag = tagReader.readTag(); if (Thread.currentThread().isInterrupted()) { throw new InterruptedException(); } - if (socket instanceof SSLSocket) { - SSLSocketHelper.log(account, (SSLSocket) socket); + final boolean success = tag != null && tag.isStart("stream", Namespace.STREAMS); + if (success && quickStart) { + this.quickStartInProgress = true; } - return tag != null && tag.isStart("stream"); + return success; } private SSLSocketFactory getSSLSocketFactory() @@ -761,11 +770,12 @@ public class XmppConnection implements Runnable { sendPostBindInitialization(streamManagementEnabled != null, carbonsEnabled != null); } } + this.quickStartInProgress = false; if (version == SaslMechanism.Version.SASL) { tagReader.reset(); - sendStartStream(); + sendStartStream(true); final Tag tag = tagReader.readTag(); - if (tag != null && tag.isStart("stream")) { + if (tag != null && tag.isStart("stream", Namespace.STREAMS)) { processStream(); return true; } else { @@ -1119,11 +1129,14 @@ public class XmppConnection implements Runnable { final SSLSocket sslSocket = upgradeSocketToTls(socket); tagReader.setInputStream(sslSocket.getInputStream()); tagWriter.setOutputStream(sslSocket.getOutputStream()); - sendStartStream(); Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": TLS connection established"); + final boolean quickStart = establishStream(true); + if (quickStart) { + this.quickStartInProgress = true; + } features.encryptionEnabled = true; final Tag tag = tagReader.readTag(); - if (tag != null && tag.isStart("stream")) { + if (tag != null && tag.isStart("stream", Namespace.STREAMS)) { SSLSocketHelper.log(account, sslSocket); processStream(); } else { @@ -1170,7 +1183,13 @@ public class XmppConnection implements Runnable { final boolean isSecure = features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS || account.isOnion(); final boolean needsBinding = !isBound && !account.isOptionSet(Account.OPTION_REGISTER); - if (this.streamFeatures.hasChild("starttls", Namespace.TLS) + if (this.quickStartInProgress) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": quick start in progress. ignoring features: " + + XmlHelper.printElementNames(this.streamFeatures)); + } else if (this.streamFeatures.hasChild("starttls", Namespace.TLS) && !features.encryptionEnabled) { sendStartTLS(); } else if (this.streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE) @@ -1238,6 +1257,7 @@ public class XmppConnection implements Runnable { } else { authElement = this.streamFeatures.findChild("authentication", Namespace.SASL_2); } + //TODO externalize final Collection mechanisms = Collections2.transform( Collections2.filter( @@ -1261,6 +1281,8 @@ public class XmppConnection implements Runnable { final SaslMechanism.Factory factory = new SaslMechanism.Factory(account); this.saslMechanism = factory.of(mechanisms, channelBindings); + //TODO externalize checks + if (saslMechanism == null) { Log.d( Config.LOGTAG, @@ -1282,6 +1304,7 @@ public class XmppConnection implements Runnable { + "). Possible downgrade attack?"); throw new StateChangingException(Account.State.DOWNGRADE_ATTACK); } + final boolean quickStartAvailable; final String firstMessage = saslMechanism.getClientFirstMessage(); final Element authenticate; if (version == SaslMechanism.Version.SASL) { @@ -1289,15 +1312,24 @@ public class XmppConnection implements Runnable { if (!Strings.isNullOrEmpty(firstMessage)) { authenticate.setContent(firstMessage); } + quickStartAvailable = false; } else if (version == SaslMechanism.Version.SASL_2) { final Element inline = authElement.findChild("inline", Namespace.SASL_2); final boolean sm = inline != null && inline.hasChild("sm", "urn:xmpp:sm:3"); - final Collection bindFeatures = bindFeatures(inline); + final Collection bindFeatures = Bind2.features(inline); + quickStartAvailable = + sm + && bindFeatures != null + && bindFeatures.containsAll(Bind2.QUICKSTART_FEATURES); authenticate = generateAuthenticationRequest(firstMessage, bindFeatures, sm); } else { throw new AssertionError("Missing implementation for " + version); } + if (account.setOption(Account.OPTION_QUICKSTART_AVAILABLE, quickStartAvailable)) { + mXmppConnectionService.updateAccount(account); + } + Log.d( Config.LOGTAG, account.getJid().toString() @@ -1309,19 +1341,8 @@ public class XmppConnection implements Runnable { tagWriter.writeElement(authenticate); } - private static Collection bindFeatures(final Element inline) { - final Element inlineBind2 = - inline != null ? inline.findChild("bind", Namespace.BIND2) : null; - final Element inlineBind2Inline = - inlineBind2 != null ? inlineBind2.findChild("inline", Namespace.BIND2) : null; - if (inlineBind2 == null) { - return null; - } - if (inlineBind2Inline == null) { - return Collections.emptyList(); - } - return Collections2.transform( - inlineBind2Inline.getChildren(), c -> c == null ? null : c.getAttribute("var")); + private Element generateAuthenticationRequest(final String firstMessage) { + return generateAuthenticationRequest(firstMessage, Bind2.QUICKSTART_FEATURES, true); } private Element generateAuthenticationRequest( @@ -1988,14 +2009,37 @@ public class XmppConnection implements Runnable { } } - private void sendStartStream() throws IOException { + private boolean establishStream(final boolean secureConnection) throws IOException { + final SaslMechanism saslMechanism = account.getPinnedMechanism(); + if (secureConnection + && saslMechanism != null + && account.isOptionSet(Account.OPTION_QUICKSTART_AVAILABLE)) { + this.saslMechanism = saslMechanism; + final Element authenticate = + generateAuthenticationRequest(saslMechanism.getClientFirstMessage()); + authenticate.setAttribute("mechanism", saslMechanism.getMechanism()); + sendStartStream(false); + tagWriter.writeElement(authenticate); + Log.d( + Config.LOGTAG, + account.getJid().toString() + + ": quick start with " + + saslMechanism.getMechanism()); + return true; + } else { + sendStartStream(true); + return false; + } + } + + private void sendStartStream(final boolean flush) throws IOException { final Tag stream = Tag.start("stream:stream"); stream.setAttribute("to", account.getServer()); stream.setAttribute("version", "1.0"); stream.setAttribute("xml:lang", LocalizedContent.STREAM_LANGUAGE); stream.setAttribute("xmlns", "jabber:client"); - stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams"); - tagWriter.writeTag(stream); + stream.setAttribute("xmlns:stream", Namespace.STREAMS); + tagWriter.writeTag(stream, flush); } private String createNewResource() { diff --git a/src/main/java/eu/siacs/conversations/xmpp/bind/Bind2.java b/src/main/java/eu/siacs/conversations/xmpp/bind/Bind2.java new file mode 100644 index 0000000000000000000000000000000000000000..21c957a0f46e6a5c4ae526412d6dc1eb2d1b3e22 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/bind/Bind2.java @@ -0,0 +1,33 @@ +package eu.siacs.conversations.xmpp.bind; + +import com.google.common.collect.Collections2; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xml.Namespace; + +public class Bind2 { + + public static final Collection QUICKSTART_FEATURES = Arrays.asList( + Namespace.CARBONS, + Namespace.STREAM_MANAGEMENT + ); + + public static Collection features(final Element inline) { + final Element inlineBind2 = + inline != null ? inline.findChild("bind", Namespace.BIND2) : null; + final Element inlineBind2Inline = + inlineBind2 != null ? inlineBind2.findChild("inline", Namespace.BIND2) : null; + if (inlineBind2 == null) { + return null; + } + if (inlineBind2Inline == null) { + return Collections.emptyList(); + } + return Collections2.transform( + inlineBind2Inline.getChildren(), c -> c == null ? null : c.getAttribute("var")); + } +} From 717aeddb82e25fed0b4dc4591985f762cb81536d Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 25 Sep 2022 15:18:45 +0200 Subject: [PATCH 050/101] fix last commit. bring back option required by quicksy --- src/main/java/eu/siacs/conversations/entities/Account.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index bbfacf420927efd9db4f01f2c78a7a3e4329c65f..1817c24bb7ae3b84dfd4fb151e6b02b717fd14da 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -63,6 +63,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable public static final int OPTION_REQUIRES_ACCESS_MODE_CHANGE = 5; public static final int OPTION_LOGGED_IN_SUCCESSFULLY = 6; public static final int OPTION_HTTP_UPLOAD_AVAILABLE = 7; + public static final int OPTION_UNVERIFIED = 8; public static final int OPTION_FIXED_USERNAME = 9; public static final int OPTION_QUICKSTART_AVAILABLE = 10; From 3d56d01826e53ce5bb5b6450d99a195957a40626 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 26 Sep 2022 07:53:48 +0200 Subject: [PATCH 051/101] handle case when server loses support for quick start --- .../conversations/xmpp/XmppConnection.java | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 911b226868a9b758d7c06988f95f87983d5b2975..e753e4b39c928935bb1f41e24983132093f51ab9 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -898,6 +898,10 @@ public class XmppConnection implements Runnable { } sendPacket(packet); } + changeStatusToOnline(); + } + + private void changeStatusToOnline() { Log.d( Config.LOGTAG, account.getJid().asBareJid() + ": online with resource " + account.getResource()); @@ -1184,12 +1188,20 @@ public class XmppConnection implements Runnable { features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS || account.isOnion(); final boolean needsBinding = !isBound && !account.isOptionSet(Account.OPTION_REGISTER); if (this.quickStartInProgress) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": quick start in progress. ignoring features: " - + XmlHelper.printElementNames(this.streamFeatures)); - } else if (this.streamFeatures.hasChild("starttls", Namespace.TLS) + if (this.streamFeatures.hasChild("authentication", Namespace.SASL_2)) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": quick start in progress. ignoring features: " + + XmlHelper.printElementNames(this.streamFeatures)); + return; + } + Log.d(Config.LOGTAG,account.getJid().asBareJid()+": server lost support for SASL 2. quick start not possible"); + this.account.setOption(Account.OPTION_QUICKSTART_AVAILABLE, false); + mXmppConnectionService.updateAccount(account); + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); + } + if (this.streamFeatures.hasChild("starttls", Namespace.TLS) && !features.encryptionEnabled) { sendStartTLS(); } else if (this.streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE) @@ -1878,13 +1890,10 @@ public class XmppConnection implements Runnable { } private void finalizeBind() { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() + ": online with resource " + account.getResource()); if (bindListener != null) { bindListener.onBind(account); } - changeStatus(Account.State.ONLINE); + changeStatusToOnline(); } private void enableAdvancedStreamFeatures() { @@ -2012,6 +2021,7 @@ public class XmppConnection implements Runnable { private boolean establishStream(final boolean secureConnection) throws IOException { final SaslMechanism saslMechanism = account.getPinnedMechanism(); if (secureConnection + && Config.SASL_2_ENABLED && saslMechanism != null && account.isOptionSet(Account.OPTION_QUICKSTART_AVAILABLE)) { this.saslMechanism = saslMechanism; From cb775ece992a8ded54c4cccae484475edc548803 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 26 Sep 2022 09:47:53 +0200 Subject: [PATCH 052/101] wait for DB restore before bind --- .../services/XmppConnectionService.java | 12 +++++----- .../conversations/xmpp/XmppConnection.java | 23 ++++++++++++++++--- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index e4af37947ffafc567eee0d2552d84f8d403be7c1..483016364ef5ec8e2d56133be1b3f58f3533208a 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -1939,7 +1939,7 @@ public class XmppConnectionService extends Service { databaseBackend.expireOldMessages(deletionDate); } Log.d(Config.LOGTAG, "restoring roster..."); - for (Account account : accounts) { + for (final Account account : accounts) { databaseBackend.readRoster(account.getRoster()); account.initAccountServices(XmppConnectionService.this); //roster needs to be loaded at this stage } @@ -1977,11 +1977,11 @@ public class XmppConnectionService extends Service { public void loadPhoneContacts() { mContactMergerExecutor.execute(() -> { - Map contacts = JabberIdContact.load(this); + final Map contacts = JabberIdContact.load(this); Log.d(Config.LOGTAG, "start merging phone contacts with roster"); - for (Account account : accounts) { - List withSystemAccounts = account.getRoster().getWithSystemAccounts(JabberIdContact.class); - for (JabberIdContact jidContact : contacts.values()) { + for (final Account account : accounts) { + final List withSystemAccounts = account.getRoster().getWithSystemAccounts(JabberIdContact.class); + for (final JabberIdContact jidContact : contacts.values()) { final Contact contact = account.getRoster().getContact(jidContact.getJid()); boolean needsCacheClean = contact.setPhoneContact(jidContact); if (needsCacheClean) { @@ -1989,7 +1989,7 @@ public class XmppConnectionService extends Service { } withSystemAccounts.remove(contact); } - for (Contact contact : withSystemAccounts) { + for (final Contact contact : withSystemAccounts) { boolean needsCacheClean = contact.unsetPhoneContact(JabberIdContact.class); if (needsCacheClean) { getAvatarService().clear(contact); diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index e753e4b39c928935bb1f41e24983132093f51ab9..2ba2daea0e187b6555fe867853dd5fb102df6949 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -753,6 +753,7 @@ public class XmppConnection implements Runnable { processFailed(failed, false); // wait for new stream features } if (bound != null) { + clearIqCallbacks(); this.isBound = true; final Element streamManagementEnabled = bound.findChild("enabled", Namespace.STREAM_MANAGEMENT); @@ -1134,7 +1135,12 @@ public class XmppConnection implements Runnable { tagReader.setInputStream(sslSocket.getInputStream()); tagWriter.setOutputStream(sslSocket.getOutputStream()); Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": TLS connection established"); - final boolean quickStart = establishStream(true); + final boolean quickStart; + try { + quickStart = establishStream(true); + } catch (final InterruptedException e) { + return; + } if (quickStart) { this.quickStartInProgress = true; } @@ -1333,6 +1339,17 @@ public class XmppConnection implements Runnable { sm && bindFeatures != null && bindFeatures.containsAll(Bind2.QUICKSTART_FEATURES); + if (bindFeatures != null) { + try { + mXmppConnectionService.restoredFromDatabaseLatch.await(); + } catch (final InterruptedException e) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": interrupted while waiting for DB restore during SASL2 bind"); + return; + } + } authenticate = generateAuthenticationRequest(firstMessage, bindFeatures, sm); } else { throw new AssertionError("Missing implementation for " + version); @@ -1876,7 +1893,6 @@ public class XmppConnection implements Runnable { } } } - Log.d(Config.LOGTAG, commands.toString()); synchronized (this.commands) { this.commands.clear(); this.commands.putAll(commands); @@ -2018,12 +2034,13 @@ public class XmppConnection implements Runnable { } } - private boolean establishStream(final boolean secureConnection) throws IOException { + private boolean establishStream(final boolean secureConnection) throws IOException, InterruptedException { final SaslMechanism saslMechanism = account.getPinnedMechanism(); if (secureConnection && Config.SASL_2_ENABLED && saslMechanism != null && account.isOptionSet(Account.OPTION_QUICKSTART_AVAILABLE)) { + mXmppConnectionService.restoredFromDatabaseLatch.await(); this.saslMechanism = saslMechanism; final Element authenticate = generateAuthenticationRequest(saslMechanism.getClientFirstMessage()); From 10f30faf551e630315cdc9f9717be9b85929b15a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 1 Oct 2022 09:21:38 +0200 Subject: [PATCH 053/101] revert transcoder to 0.9.1 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 063dac0ce53af7e3699455ca595d124fb745aabd..1817807b7361aa25e2dc76b45cc2ebae0655a66a 100644 --- a/build.gradle +++ b/build.gradle @@ -60,7 +60,7 @@ dependencies { implementation 'org.whispersystems:signal-protocol-java:2.6.2' implementation 'com.makeramen:roundedimageview:2.3.0' implementation "com.wefika:flowlayout:0.4.1" - implementation 'com.otaliastudios:transcoder:0.10.4' + implementation 'com.otaliastudios:transcoder:0.9.1' implementation 'org.jxmpp:jxmpp-jid:1.0.3' implementation 'org.osmdroid:osmdroid-android:6.1.11' From 64b853f3acefc7151ecb8b74856ab7c9a8d55271 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 1 Oct 2022 09:25:41 +0200 Subject: [PATCH 054/101] bump various dependencies --- build.gradle | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 1817807b7361aa25e2dc76b45cc2ebae0655a66a..27e620963716608769cfecb9169c2bfca27f0dae 100644 --- a/build.gradle +++ b/build.gradle @@ -33,7 +33,7 @@ configurations { dependencies { implementation 'androidx.viewpager:viewpager:1.0.0' - playstoreImplementation('com.google.firebase:firebase-messaging:23.0.7') { + playstoreImplementation('com.google.firebase:firebase-messaging:23.0.8') { exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' @@ -42,11 +42,11 @@ dependencies { quicksyPlaystoreImplementation 'com.google.android.gms:play-services-auth-api-phone:18.0.1' implementation 'org.sufficientlysecure:openpgp-api:10.0' implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0' - implementation 'androidx.appcompat:appcompat:1.5.0' + implementation 'androidx.appcompat:appcompat:1.5.1' implementation 'androidx.exifinterface:exifinterface:1.3.3' implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' - implementation 'com.google.android.material:material:1.4.0' + implementation 'com.google.android.material:material:1.6.1' implementation "androidx.emoji2:emoji2:1.2.0" freeImplementation "androidx.emoji2:emoji2-bundled:1.2.0" @@ -60,6 +60,7 @@ dependencies { implementation 'org.whispersystems:signal-protocol-java:2.6.2' implementation 'com.makeramen:roundedimageview:2.3.0' implementation "com.wefika:flowlayout:0.4.1" + //noinspection GradleDependency implementation 'com.otaliastudios:transcoder:0.9.1' implementation 'org.jxmpp:jxmpp-jid:1.0.3' From 5735bca517a798a1ef6ffa87dc5882b69888cbdd Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 1 Oct 2022 09:26:07 +0200 Subject: [PATCH 055/101] minor code clean up --- .../java/eu/siacs/conversations/persistance/FileBackend.java | 5 ++--- .../java/eu/siacs/conversations/ui/RtpSessionActivity.java | 3 +++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index 2d5496f2f6b4755ff1e553182763531c71d641fd..8519e5dbddd4358f829f1a9a05b293eb170f4d15 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -1602,7 +1602,6 @@ public class FileBackend { return getVideoDimensions(metadataRetriever); } - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) private Dimensions getPdfDocumentDimensions(final File file) { final ParcelFileDescriptor fileDescriptor; try { @@ -1610,7 +1609,7 @@ public class FileBackend { if (fileDescriptor == null) { return new Dimensions(0, 0); } - } catch (FileNotFoundException e) { + } catch (final FileNotFoundException e) { return new Dimensions(0, 0); } try { @@ -1621,7 +1620,7 @@ public class FileBackend { page.close(); pdfRenderer.close(); return scalePdfDimensions(new Dimensions(height, width)); - } catch (IOException | SecurityException e) { + } catch (final IOException | SecurityException e) { Log.d(Config.LOGTAG, "unable to get dimensions for pdf document", e); return new Dimensions(0, 0); } diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index e73fdb23c72cb536a0a33481646abfe4b87a9ac2..302fbf81d710c777b19cea79d340567b52187afc 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -521,6 +521,9 @@ public class RtpSessionActivity extends XmppActivity @StringRes int res; final String firstDenied = getFirstDenied(permissionResult.grantResults, permissionResult.permissions); + if (firstDenied == null) { + return; + } if (Manifest.permission.RECORD_AUDIO.equals(firstDenied)) { res = R.string.no_microphone_permission; } else if (Manifest.permission.CAMERA.equals(firstDenied)) { From d435c1f2aef1454141d4f5099224b5a03d579dba Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 1 Oct 2022 11:26:52 +0200 Subject: [PATCH 056/101] let omemoOnly config overwrite OmemoSetting --- .../java/eu/siacs/conversations/Config.java | 7 +++- .../conversations/crypto/OmemoSetting.java | 10 ++++- .../conversations/ui/SettingsActivity.java | 39 +++++++++++-------- src/main/res/xml/preferences.xml | 3 +- 4 files changed, 39 insertions(+), 20 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index a3eacc9db4b9fc55be92c5f177129085952f14fe..47226eb6ebdf6a16468cc8a7779144ca51798601 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -15,10 +15,9 @@ import eu.siacs.conversations.xmpp.chatstate.ChatState; public final class Config { private static final int UNENCRYPTED = 1; private static final int OPENPGP = 2; - private static final int OTR = 4; private static final int OMEMO = 8; - private static final int ENCRYPTION_MASK = UNENCRYPTED | OPENPGP | OTR | OMEMO; + private static final int ENCRYPTION_MASK = UNENCRYPTED | OPENPGP | OMEMO; public static boolean supportUnencrypted() { return (ENCRYPTION_MASK & UNENCRYPTED) != 0; @@ -32,6 +31,10 @@ public final class Config { return (ENCRYPTION_MASK & OMEMO) != 0; } + public static boolean omemoOnly() { + return !multipleEncryptionChoices() && supportOmemo(); + } + public static boolean multipleEncryptionChoices() { return (ENCRYPTION_MASK & (ENCRYPTION_MASK - 1)) != 0; } diff --git a/src/main/java/eu/siacs/conversations/crypto/OmemoSetting.java b/src/main/java/eu/siacs/conversations/crypto/OmemoSetting.java index 5326ecae04d38fc1bc16fd68bf4ebb29d926c73e..a531c39f3ea5968367d35b8bdde8bd9f70867014 100644 --- a/src/main/java/eu/siacs/conversations/crypto/OmemoSetting.java +++ b/src/main/java/eu/siacs/conversations/crypto/OmemoSetting.java @@ -34,6 +34,9 @@ import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; +import com.google.common.base.Strings; + +import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.ui.SettingsActivity; @@ -52,8 +55,13 @@ public class OmemoSetting { } public static void load(final Context context, final SharedPreferences sharedPreferences) { + if (Config.omemoOnly()) { + always = true; + encryption = Message.ENCRYPTION_AXOLOTL; + return; + } final String value = sharedPreferences.getString(SettingsActivity.OMEMO_SETTING, context.getResources().getString(R.string.omemo_setting_default)); - switch (value) { + switch (Strings.nullToEmpty(value)) { case "always": always = true; encryption = Message.ENCRYPTION_AXOLOTL; diff --git a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java index 21d2b956c84a4a17ea1b04554f744e9840dd75d5..5e21e0b262ae0f86f0ca677518f0043dfc26ed3d 100644 --- a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java @@ -298,26 +298,33 @@ public class SettingsActivity extends XmppActivity implements OnSharedPreference deleteOmemoPreference.setOnPreferenceClickListener( preference -> deleteOmemoIdentities()); } + if (Config.omemoOnly()) { + final PreferenceCategory privacyCategory = + (PreferenceCategory) mSettingsFragment.findPreference("privacy"); + final Preference omemoPreference =mSettingsFragment.findPreference(OMEMO_SETTING); + if (omemoPreference != null) { + privacyCategory.removePreference(omemoPreference); + } + } } private void changeOmemoSettingSummary() { - ListPreference omemoPreference = + final ListPreference omemoPreference = (ListPreference) mSettingsFragment.findPreference(OMEMO_SETTING); - if (omemoPreference != null) { - String value = omemoPreference.getValue(); - switch (value) { - case "always": - omemoPreference.setSummary(R.string.pref_omemo_setting_summary_always); - break; - case "default_on": - omemoPreference.setSummary(R.string.pref_omemo_setting_summary_default_on); - break; - case "default_off": - omemoPreference.setSummary(R.string.pref_omemo_setting_summary_default_off); - break; - } - } else { - Log.d(Config.LOGTAG, "unable to find preference named " + OMEMO_SETTING); + if (omemoPreference == null) { + return; + } + final String value = omemoPreference.getValue(); + switch (value) { + case "always": + omemoPreference.setSummary(R.string.pref_omemo_setting_summary_always); + break; + case "default_on": + omemoPreference.setSummary(R.string.pref_omemo_setting_summary_default_on); + break; + case "default_off": + omemoPreference.setSummary(R.string.pref_omemo_setting_summary_default_off); + break; } } diff --git a/src/main/res/xml/preferences.xml b/src/main/res/xml/preferences.xml index 91b07210c55fd00bce03a02562b94dc77714ec76..b461558364e17a73999bce7a22008292f6275cc0 100644 --- a/src/main/res/xml/preferences.xml +++ b/src/main/res/xml/preferences.xml @@ -15,7 +15,8 @@ android:targetPackage="com.huawei.systemmanager" /> - + Date: Wed, 12 Oct 2022 11:53:57 +0200 Subject: [PATCH 057/101] only run account options through int conversion. fixes #4390 --- .../siacs/conversations/services/ExportBackupService.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/ExportBackupService.java b/src/main/java/eu/siacs/conversations/services/ExportBackupService.java index 4e144f223cfde44518bba71333b3f57aba3b8a96..9826ecbc24dcf66514379f83c219df920a02b1e0 100644 --- a/src/main/java/eu/siacs/conversations/services/ExportBackupService.java +++ b/src/main/java/eu/siacs/conversations/services/ExportBackupService.java @@ -110,11 +110,9 @@ public class ExportBackupService extends Service { final String value = accountCursor.getString(i); if (value == null || Account.ROSTERVERSION.equals(accountCursor.getColumnName(i))) { builder.append("NULL"); - } else if (value.matches("\\d+")) { + } else if (Account.OPTIONS.equals(accountCursor.getColumnName(i)) && value.matches("\\d+")) { int intValue = Integer.parseInt(value); - if (Account.OPTIONS.equals(accountCursor.getColumnName(i))) { - intValue |= 1 << Account.OPTION_DISABLED; - } + intValue |= 1 << Account.OPTION_DISABLED; builder.append(intValue); } else { appendEscapedSQLString(builder, value); From ab0ea7096e57cdcc3f1056c7f8191680017ddbfa Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 12 Oct 2022 14:47:02 +0200 Subject: [PATCH 058/101] make it easier to disable muclumbus in Config --- .../services/ChannelDiscoveryService.java | 208 +++++++++++------- .../ui/ChannelDiscoveryActivity.java | 5 + .../conversations/ui/SettingsActivity.java | 18 +- 3 files changed, 142 insertions(+), 89 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java b/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java index c46568c3e254e9dada3b02aaf30f1196a39f27d4..2f9553bfce714bda47f3e438da7fa96c76e65ebf 100644 --- a/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java +++ b/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java @@ -4,6 +4,7 @@ import android.util.Log; import androidx.annotation.NonNull; +import com.google.common.base.Strings; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; @@ -39,7 +40,6 @@ public class ChannelDiscoveryService { private final XmppConnectionService service; - private MuclumbusService muclumbusService; private final Cache> cache; @@ -50,16 +50,21 @@ public class ChannelDiscoveryService { } void initializeMuclumbusService() { + if (Strings.isNullOrEmpty(Config.CHANNEL_DISCOVERY)) { + this.muclumbusService = null; + return; + } final OkHttpClient.Builder builder = HttpConnectionManager.OK_HTTP_CLIENT.newBuilder(); if (service.useTorToConnect()) { builder.proxy(HttpConnectionManager.getProxy()); } - Retrofit retrofit = new Retrofit.Builder() - .client(builder.build()) - .baseUrl(Config.CHANNEL_DISCOVERY) - .addConverterFactory(GsonConverterFactory.create()) - .callbackExecutor(Executors.newSingleThreadExecutor()) - .build(); + final Retrofit retrofit = + new Retrofit.Builder() + .client(builder.build()) + .baseUrl(Config.CHANNEL_DISCOVERY) + .addConverterFactory(GsonConverterFactory.create()) + .callbackExecutor(Executors.newSingleThreadExecutor()) + .build(); this.muclumbusService = retrofit.create(MuclumbusService.class); } @@ -67,7 +72,10 @@ public class ChannelDiscoveryService { cache.invalidateAll(); } - void discover(@NonNull final String query, Method method, OnChannelSearchResultsFound onChannelSearchResultsFound) { + void discover( + @NonNull final String query, + Method method, + OnChannelSearchResultsFound onChannelSearchResultsFound) { final List result = cache.getIfPresent(key(method, query)); if (result != null) { onChannelSearchResultsFound.onChannelSearchResultsFound(result); @@ -84,59 +92,82 @@ public class ChannelDiscoveryService { } } - private void discoverChannelsJabberNetwork(OnChannelSearchResultsFound listener) { - Call call = muclumbusService.getRooms(1); - try { - call.enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - final MuclumbusService.Rooms body = response.body(); - if (body == null) { - listener.onChannelSearchResultsFound(Collections.emptyList()); - logError(response); - return; + private void discoverChannelsJabberNetwork(final OnChannelSearchResultsFound listener) { + if (muclumbusService == null) { + listener.onChannelSearchResultsFound(Collections.emptyList()); + return; + } + final Call call = muclumbusService.getRooms(1); + call.enqueue( + new Callback() { + @Override + public void onResponse( + @NonNull Call call, + @NonNull Response response) { + final MuclumbusService.Rooms body = response.body(); + if (body == null) { + listener.onChannelSearchResultsFound(Collections.emptyList()); + logError(response); + return; + } + cache.put(key(Method.JABBER_NETWORK, ""), body.items); + listener.onChannelSearchResultsFound(body.items); } - cache.put(key(Method.JABBER_NETWORK, ""), body.items); - listener.onChannelSearchResultsFound(body.items); - } - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable throwable) { - Log.d(Config.LOGTAG, "Unable to query muclumbus on " + Config.CHANNEL_DISCOVERY, throwable); - listener.onChannelSearchResultsFound(Collections.emptyList()); - } - }); - } catch (Exception e) { - e.printStackTrace(); - } + @Override + public void onFailure( + @NonNull Call call, + @NonNull Throwable throwable) { + Log.d( + Config.LOGTAG, + "Unable to query muclumbus on " + Config.CHANNEL_DISCOVERY, + throwable); + listener.onChannelSearchResultsFound(Collections.emptyList()); + } + }); } - private void discoverChannelsJabberNetwork(final String query, OnChannelSearchResultsFound listener) { - MuclumbusService.SearchRequest searchRequest = new MuclumbusService.SearchRequest(query); - Call searchResultCall = muclumbusService.search(searchRequest); - - searchResultCall.enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - final MuclumbusService.SearchResult body = response.body(); - if (body == null) { - listener.onChannelSearchResultsFound(Collections.emptyList()); - logError(response); - return; - } - cache.put(key(Method.JABBER_NETWORK, query), body.result.items); - listener.onChannelSearchResultsFound(body.result.items); - } + private void discoverChannelsJabberNetwork( + final String query, final OnChannelSearchResultsFound listener) { + if (muclumbusService == null) { + listener.onChannelSearchResultsFound(Collections.emptyList()); + return; + } + final MuclumbusService.SearchRequest searchRequest = + new MuclumbusService.SearchRequest(query); + final Call searchResultCall = + muclumbusService.search(searchRequest); + searchResultCall.enqueue( + new Callback() { + @Override + public void onResponse( + @NonNull Call call, + @NonNull Response response) { + final MuclumbusService.SearchResult body = response.body(); + if (body == null) { + listener.onChannelSearchResultsFound(Collections.emptyList()); + logError(response); + return; + } + cache.put(key(Method.JABBER_NETWORK, query), body.result.items); + listener.onChannelSearchResultsFound(body.result.items); + } - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable throwable) { - Log.d(Config.LOGTAG, "Unable to query muclumbus on " + Config.CHANNEL_DISCOVERY, throwable); - listener.onChannelSearchResultsFound(Collections.emptyList()); - } - }); + @Override + public void onFailure( + @NonNull Call call, + @NonNull Throwable throwable) { + Log.d( + Config.LOGTAG, + "Unable to query muclumbus on " + Config.CHANNEL_DISCOVERY, + throwable); + listener.onChannelSearchResultsFound(Collections.emptyList()); + } + }); } - private void discoverChannelsLocalServers(final String query, final OnChannelSearchResultsFound listener) { + private void discoverChannelsLocalServers( + final String query, final OnChannelSearchResultsFound listener) { final Map localMucService = getLocalMucServices(); Log.d(Config.LOGTAG, "checking with " + localMucService.size() + " muc services"); if (localMucService.size() == 0) { @@ -156,38 +187,49 @@ public class ChannelDiscoveryService { for (Map.Entry entry : localMucService.entrySet()) { IqPacket itemsRequest = service.getIqGenerator().queryDiscoItems(entry.getKey()); queriesInFlight.incrementAndGet(); - service.sendIqPacket(entry.getValue(), itemsRequest, (account, itemsResponse) -> { - if (itemsResponse.getType() == IqPacket.TYPE.RESULT) { - final List items = IqParser.items(itemsResponse); - for (Jid item : items) { - IqPacket infoRequest = service.getIqGenerator().queryDiscoInfo(item); - queriesInFlight.incrementAndGet(); - service.sendIqPacket(account, infoRequest, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket infoResponse) { - if (infoResponse.getType() == IqPacket.TYPE.RESULT) { - final Room room = IqParser.parseRoom(infoResponse); - if (room != null) { - rooms.add(room); - } - if (queriesInFlight.decrementAndGet() <= 0) { - finishDiscoSearch(rooms, query, listener); - } - } else { - queriesInFlight.decrementAndGet(); - } + service.sendIqPacket( + entry.getValue(), + itemsRequest, + (account, itemsResponse) -> { + if (itemsResponse.getType() == IqPacket.TYPE.RESULT) { + final List items = IqParser.items(itemsResponse); + for (Jid item : items) { + IqPacket infoRequest = + service.getIqGenerator().queryDiscoInfo(item); + queriesInFlight.incrementAndGet(); + service.sendIqPacket( + account, + infoRequest, + new OnIqPacketReceived() { + @Override + public void onIqPacketReceived( + Account account, IqPacket infoResponse) { + if (infoResponse.getType() + == IqPacket.TYPE.RESULT) { + final Room room = + IqParser.parseRoom(infoResponse); + if (room != null) { + rooms.add(room); + } + if (queriesInFlight.decrementAndGet() <= 0) { + finishDiscoSearch(rooms, query, listener); + } + } else { + queriesInFlight.decrementAndGet(); + } + } + }); } - }); - } - } - if (queriesInFlight.decrementAndGet() <= 0) { - finishDiscoSearch(rooms, query, listener); - } - }); + } + if (queriesInFlight.decrementAndGet() <= 0) { + finishDiscoSearch(rooms, query, listener); + } + }); } } - private void finishDiscoSearch(List rooms, String query, OnChannelSearchResultsFound listener) { + private void finishDiscoSearch( + List rooms, String query, OnChannelSearchResultsFound listener) { Collections.sort(rooms); cache.put(key(Method.LOCAL_SERVER, ""), rooms); if (query.isEmpty()) { @@ -241,7 +283,7 @@ public class ChannelDiscoveryService { try { Log.d(Config.LOGTAG, "error body=" + errorBody.string()); } catch (IOException e) { - //ignored + // ignored } } diff --git a/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java b/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java index 4687e5f631af44e17549c3c8c04bb3660954881e..5cf9417e9f1b130ffc654beeeeefa42374bad168 100644 --- a/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java @@ -20,6 +20,8 @@ import android.widget.Toast; import androidx.databinding.DataBindingUtil; +import com.google.common.base.Strings; + import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicReference; @@ -90,6 +92,9 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O } private static ChannelDiscoveryService.Method getMethod(final Context c) { + if ( Strings.isNullOrEmpty(Config.CHANNEL_DISCOVERY)) { + return ChannelDiscoveryService.Method.LOCAL_SERVER; + } if (QuickConversationsService.isQuicksy()) { return ChannelDiscoveryService.Method.JABBER_NETWORK; } diff --git a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java index 5e21e0b262ae0f86f0ca677518f0043dfc26ed3d..07c8a55db1b6bba085d94cb1909929b76fdc9d72 100644 --- a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java @@ -23,6 +23,8 @@ import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.core.content.ContextCompat; +import com.google.common.base.Strings; + import java.io.File; import java.security.KeyStoreException; import java.util.ArrayList; @@ -96,20 +98,24 @@ public class SettingsActivity extends XmppActivity implements OnSharedPreference changeOmemoSettingSummary(); - if (QuickConversationsService.isQuicksy()) { - final PreferenceCategory connectionOptions = - (PreferenceCategory) mSettingsFragment.findPreference("connection_options"); + if (QuickConversationsService.isQuicksy() + || Strings.isNullOrEmpty(Config.CHANNEL_DISCOVERY)) { final PreferenceCategory groupChats = (PreferenceCategory) mSettingsFragment.findPreference("group_chats"); final Preference channelDiscoveryMethod = mSettingsFragment.findPreference("channel_discovery_method"); + if (groupChats != null && channelDiscoveryMethod != null) { + groupChats.removePreference(channelDiscoveryMethod); + } + } + + if (QuickConversationsService.isQuicksy()) { + final PreferenceCategory connectionOptions = + (PreferenceCategory) mSettingsFragment.findPreference("connection_options"); PreferenceScreen expert = (PreferenceScreen) mSettingsFragment.findPreference("expert"); if (connectionOptions != null) { expert.removePreference(connectionOptions); } - if (groupChats != null && channelDiscoveryMethod != null) { - groupChats.removePreference(channelDiscoveryMethod); - } } PreferenceScreen mainPreferenceScreen = From 90048e92bb39b2124fcbdd74944e8d1421c0e557 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 12 Oct 2022 18:43:05 +0200 Subject: [PATCH 059/101] use url span method to show context menu. fixes #4393 --- .../ui/ConversationFragment.java | 6 +- .../conversations/ui/util/MyLinkify.java | 38 +++++++++ .../conversations/ui/util/ShareUtil.java | 77 ++++++++++--------- 3 files changed, 81 insertions(+), 40 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 3b923adce54738d6e3f8603c7b080b1629ef48de..f6626c3a12ff7218bc9e680ff044c6cf9fa9a44c 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -1332,11 +1332,11 @@ public class ConversationFragment extends XmppFragment && t == null) { copyMessage.setVisible(true); quoteMessage.setVisible(!showError && MessageUtils.prepareQuote(m).length() > 0); - String body = m.getMergedBody().toString(); - if (ShareUtil.containsXmppUri(body)) { + final String scheme = ShareUtil.getLinkScheme(m.getMergedBody()); + if ("xmpp".equals(scheme)) { copyLink.setTitle(R.string.copy_jabber_id); copyLink.setVisible(true); - } else if (Patterns.AUTOLINK_WEB_URL.matcher(body).find()) { + } else if (scheme != null) { copyLink.setVisible(true); } } diff --git a/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java b/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java index b72c5aa864ab9dc9422cbcf0f29555a3a8be51a4..d5d3a82da519d79309822751581796de549b01ae 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java +++ b/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java @@ -31,9 +31,18 @@ package eu.siacs.conversations.ui.util; import android.os.Build; import android.text.Editable; +import android.text.style.URLSpan; import android.text.util.Linkify; +import com.google.common.collect.Collections2; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; import java.util.Locale; +import java.util.Objects; import eu.siacs.conversations.ui.text.FixedURLSpan; import eu.siacs.conversations.utils.GeoHelper; @@ -118,4 +127,33 @@ public class MyLinkify { } FixedURLSpan.fix(body); } + + public static List extractLinks(final Editable body) { + MyLinkify.addLinks(body, false); + final Collection spans = + Arrays.asList(body.getSpans(0, body.length() - 1, URLSpan.class)); + final Collection urlWrappers = + Collections2.filter( + Collections2.transform( + spans, + s -> + s == null + ? null + : new UrlWrapper(body.getSpanStart(s), s.getURL())), + uw -> uw != null); + List sorted = ImmutableList.sortedCopyOf( + (a, b) -> Integer.compare(a.position, b.position), urlWrappers); + return Lists.transform(sorted, uw -> uw.url); + + } + + private static class UrlWrapper { + private final int position; + private final String url; + + private UrlWrapper(int position, String url) { + this.position = position; + this.url = url; + } + } } diff --git a/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java b/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java index 8ff81a203eac33f72c04bca72ce960baddc9eb7a..007575307c7b02903e2dc485dd14e585a18c26be 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java +++ b/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java @@ -33,18 +33,14 @@ import android.content.ActivityNotFoundException; import android.content.Intent; import android.net.Uri; import android.text.SpannableStringBuilder; -import android.text.style.URLSpan; import android.widget.Toast; -import java.util.regex.Matcher; - import eu.siacs.conversations.R; import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.ui.ConversationsActivity; import eu.siacs.conversations.ui.XmppActivity; -import eu.siacs.conversations.utils.Patterns; import eu.siacs.conversations.utils.XmppUri; import eu.siacs.conversations.xmpp.Jid; @@ -108,38 +104,45 @@ public class ShareUtil { } } - public static void copyLinkToClipboard(final XmppActivity activity, final Message message) { - final SpannableStringBuilder body = message.getMergedBody(); - MyLinkify.addLinks(body, true); - for (final URLSpan urlspan : body.getSpans(0, body.length() - 1, URLSpan.class)) { - final Uri uri = Uri.parse(urlspan.getURL()); - if ("xmpp".equals(uri.getScheme())) { - try { - final Jid jid = new XmppUri(uri).getJid(); - if (activity.copyTextToClipboard(jid.asBareJid().toString(), R.string.account_settings_jabber_id)) { - Toast.makeText(activity,R.string.jabber_id_copied_to_clipboard, Toast.LENGTH_SHORT).show(); - } - return; - } catch (final Exception e) { - return; - } - } else { - if (activity.copyTextToClipboard(urlspan.getURL(),R.string.web_address)) { - Toast.makeText(activity,R.string.url_copied_to_clipboard, Toast.LENGTH_SHORT).show(); - } - } - } - } + public static void copyLinkToClipboard(final XmppActivity activity, final Message message) { + final SpannableStringBuilder body = message.getMergedBody(); + for (final String url : MyLinkify.extractLinks(body)) { + final Uri uri = Uri.parse(url); + if ("xmpp".equals(uri.getScheme())) { + try { + final Jid jid = new XmppUri(uri).getJid(); + if (activity.copyTextToClipboard( + jid.asBareJid().toString(), R.string.account_settings_jabber_id)) { + Toast.makeText( + activity, + R.string.jabber_id_copied_to_clipboard, + Toast.LENGTH_SHORT) + .show(); + } + return; + } catch (final Exception e) { + return; + } + } else { + if (activity.copyTextToClipboard(url, R.string.web_address)) { + Toast.makeText(activity, R.string.url_copied_to_clipboard, Toast.LENGTH_SHORT) + .show(); + } + return; + } + } + } - public static boolean containsXmppUri(String body) { - Matcher xmppPatternMatcher = Patterns.XMPP_PATTERN.matcher(body); - if (xmppPatternMatcher.find()) { - try { - return new XmppUri(body.substring(xmppPatternMatcher.start(), xmppPatternMatcher.end())).isValidJid(); - } catch (Exception e) { - return false; - } - } - return false; - } + public static String getLinkScheme(final SpannableStringBuilder body) { + MyLinkify.addLinks(body, false); + for (final String url : MyLinkify.extractLinks(body)) { + final Uri uri = Uri.parse(url); + if ("xmpp".equals(uri.getScheme())) { + return uri.getScheme(); + } else { + return "http"; + } + } + return null; + } } From 3d6c7bbf1c291640790710e7516efdbadd568214 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 13 Oct 2022 09:51:56 +0200 Subject: [PATCH 060/101] fix display glitch in username mode --- src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index 8959d4c3897294c0839fd0d63d667fc2570a5468..c0c43dda7e83af96b066dac6b1cfa4931ec361d8 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -822,7 +822,6 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat } if (mUsernameMode) { this.binding.accountJidLayout.setHint(getString(R.string.username_hint)); - this.binding.accountJid.setHint(R.string.username_hint); } else { final KnownHostsAdapter mKnownHostsAdapter = new KnownHostsAdapter(this, R.layout.simple_list_item, From 9a0c90f066c57c86fc81eed53f4bbc1c4e6a62c9 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 14 Oct 2022 13:13:21 +0200 Subject: [PATCH 061/101] read new stream features directly after success --- .../conversations/xmpp/XmppConnection.java | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 2ba2daea0e187b6555fe867853dd5fb102df6949..aaf40edf78ef61db685b0cb5c0e02f55949eb7cd 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -556,7 +556,7 @@ public class XmppConnection implements Runnable { while (nextTag != null && !nextTag.isEnd("stream")) { if (nextTag.isStart("error")) { processStreamError(nextTag); - } else if (nextTag.isStart("features")) { + } else if (nextTag.isStart("features", Namespace.STREAMS)) { processStreamFeatures(nextTag); } else if (nextTag.isStart("proceed", Namespace.TLS)) { switchOverToTls(); @@ -705,6 +705,22 @@ public class XmppConnection implements Runnable { account.getJid().asBareJid().toString() + ": logged in (using " + version + ")"); account.setPinnedMechanism(saslMechanism); if (version == SaslMechanism.Version.SASL_2) { + final Tag tag = tagReader.readTag(); + if (tag != null && tag.isStart("features", Namespace.STREAMS)) { + this.streamFeatures = tagReader.readElement(tag); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": processed NOP stream features after success " + + XmlHelper.printElementNames(this.streamFeatures)); + } else { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": server did not send stream features after SASL2 success"); + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); + } + Log.d(Config.LOGTAG, "success: " + success); final String authorizationIdentifier = success.findChildContent("authorization-identifier"); final Jid authorizationJid; @@ -746,7 +762,13 @@ public class XmppConnection implements Runnable { final Element bound = success.findChild("bound", Namespace.BIND2); final Element resumed = success.findChild("resumed", "urn:xmpp:sm:3"); final Element failed = success.findChild("failed", "urn:xmpp:sm:3"); - // TODO check if resumed and bound exist and throw bind failure + if (bound != null && resumed != null) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": server sent bound and resumed in SASL2 success"); + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); + } if (resumed != null && streamId != null) { processResumed(resumed); } else if (failed != null) { @@ -767,6 +789,7 @@ public class XmppConnection implements Runnable { account.getJid().asBareJid() + ": successfully enabled carbons"); features.carbonsEnabled = true; } + // TODO if we didn’t enable stream managment in bind do it now // TODO if both are set mark account ready for pipelining sendPostBindInitialization(streamManagementEnabled != null, carbonsEnabled != null); } @@ -1761,7 +1784,7 @@ public class XmppConnection implements Runnable { lastDiscoStarted = SystemClock.elapsedRealtime(); mXmppConnectionService.scheduleWakeUpCall( Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode()); - Element caps = streamFeatures.findChild("c"); + final Element caps = streamFeatures.findChild("c"); final String hash = caps == null ? null : caps.getAttribute("hash"); final String ver = caps == null ? null : caps.getAttribute("ver"); ServiceDiscoveryResult discoveryResult = null; From 7eb160386d0c1a76084fab004c41d596009fc678 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 14 Oct 2022 13:29:59 +0200 Subject: [PATCH 062/101] =?UTF-8?q?enable=20SM=20if=20it=20wasn=E2=80=99t?= =?UTF-8?q?=20enabled=20in=20bind=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/eu/siacs/conversations/xmpp/XmppConnection.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index aaf40edf78ef61db685b0cb5c0e02f55949eb7cd..b467d8dc708b3206c57aaa7845ba4e3ff5f87d37 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -780,8 +780,13 @@ public class XmppConnection implements Runnable { final Element streamManagementEnabled = bound.findChild("enabled", Namespace.STREAM_MANAGEMENT); final Element carbonsEnabled = bound.findChild("enabled", Namespace.CARBONS); + final boolean waitForDisco; if (streamManagementEnabled != null) { processEnabled(streamManagementEnabled); + waitForDisco = true; + } else { + //if we didn’t enable stream managment in bind do it now + waitForDisco = enableStreamManagement(); } if (carbonsEnabled != null) { Log.d( @@ -789,9 +794,7 @@ public class XmppConnection implements Runnable { account.getJid().asBareJid() + ": successfully enabled carbons"); features.carbonsEnabled = true; } - // TODO if we didn’t enable stream managment in bind do it now - // TODO if both are set mark account ready for pipelining - sendPostBindInitialization(streamManagementEnabled != null, carbonsEnabled != null); + sendPostBindInitialization(waitForDisco, carbonsEnabled != null); } } this.quickStartInProgress = false; From 0cd416298da9ce3844c099486a482bd4f5fa1a8d Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 14 Oct 2022 20:00:36 +0200 Subject: [PATCH 063/101] ensure we only select channel binding methods available for tls version --- .../java/eu/siacs/conversations/Config.java | 2 +- .../crypto/sasl/ChannelBinding.java | 48 ++++++++-- .../crypto/sasl/SaslMechanism.java | 58 ++++++++++-- .../{SSLSocketHelper.java => SSLSockets.java} | 46 +++++++++- .../conversations/utils/TLSSocketFactory.java | 4 +- .../eu/siacs/conversations/xml/Namespace.java | 1 + .../conversations/xmpp/XmppConnection.java | 88 ++++++++----------- 7 files changed, 181 insertions(+), 66 deletions(-) rename src/main/java/eu/siacs/conversations/utils/{SSLSocketHelper.java => SSLSockets.java} (76%) diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index 47226eb6ebdf6a16468cc8a7779144ca51798601..812f6ae1048ba5700afcd08bc6d2854a911affef 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -60,7 +60,7 @@ public final class Config { public static final long CONTACT_SYNC_RETRY_INTERVAL = 1000L * 60 * 5; - public static final boolean SASL_2_ENABLED = true; + public static final boolean QUICKSTART_ENABLED = true; //Notification settings public static final boolean HIDE_MESSAGE_TEXT_IN_NOTIFICATION = false; diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java index d8307a76d3eb057230e363ec6cff826a41a4a125..fb12555664dff0e58387dfbb786e79a02fc87f80 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java @@ -3,11 +3,19 @@ package eu.siacs.conversations.crypto.sasl; import android.util.Log; import com.google.common.base.CaseFormat; +import com.google.common.base.Preconditions; +import com.google.common.base.Predicates; import com.google.common.base.Strings; +import com.google.common.collect.Collections2; +import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import eu.siacs.conversations.Config; +import eu.siacs.conversations.utils.SSLSockets; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xml.Namespace; public enum ChannelBinding { NONE, @@ -15,7 +23,24 @@ public enum ChannelBinding { TLS_SERVER_END_POINT, TLS_UNIQUE; - public static ChannelBinding of(final String type) { + public static Collection of(final Element channelBinding) { + Preconditions.checkArgument( + channelBinding == null + || ("sasl-channel-binding".equals(channelBinding.getName()) + && Namespace.CHANNEL_BINDING.equals(channelBinding.getNamespace())), + "pass null or a valid channel binding stream feature"); + return Collections2.filter( + Collections2.transform( + Collections2.filter( + channelBinding == null + ? Collections.emptyList() + : channelBinding.getChildren(), + c -> c != null && "channel-binding".equals(c.getName())), + c -> c == null ? null : ChannelBinding.of(c.getAttribute("type"))), + Predicates.notNull()); + } + + private static ChannelBinding of(final String type) { if (type == null) { return null; } @@ -39,15 +64,28 @@ public enum ChannelBinding { } } - public static ChannelBinding best(final Collection bindings) { - if (bindings.contains(TLS_EXPORTER)) { + public static ChannelBinding best( + final Collection bindings, final SSLSockets.Version sslVersion) { + if (sslVersion == SSLSockets.Version.NONE) { + return NONE; + } + if (bindings.contains(TLS_EXPORTER) && sslVersion == SSLSockets.Version.TLS_1_3) { return TLS_EXPORTER; - } else if (bindings.contains(TLS_UNIQUE)) { + } else if (bindings.contains(TLS_UNIQUE) + && Arrays.asList( + SSLSockets.Version.TLS_1_0, + SSLSockets.Version.TLS_1_1, + SSLSockets.Version.TLS_1_2) + .contains(sslVersion)) { return TLS_UNIQUE; } else if (bindings.contains(TLS_SERVER_END_POINT)) { return TLS_SERVER_END_POINT; } else { - return null; + return NONE; } } + + public static boolean ensureBest(final ChannelBinding channelBinding, final SSLSockets.Version sslVersion) { + return ChannelBinding.best(Collections.singleton(channelBinding), sslVersion) == channelBinding; + } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java index e5b940b87330d38b1947e703a2430133b19be53e..e0df3a2d4326475b725095a1e4c744635b1392e0 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java @@ -1,13 +1,19 @@ package eu.siacs.conversations.crypto.sasl; +import android.util.Log; + +import com.google.common.base.Preconditions; import com.google.common.base.Strings; +import com.google.common.collect.Collections2; import java.util.Collection; import java.util.Collections; import javax.net.ssl.SSLSocket; +import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.utils.SSLSockets; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; @@ -47,6 +53,17 @@ public abstract class SaslMechanism { return ""; } + public static Collection mechanisms(final Element authElement) { + if (authElement == null) { + return Collections.emptyList(); + } + return Collections2.transform( + Collections2.filter( + authElement.getChildren(), + c -> c != null && "mechanism".equals(c.getName())), + c -> c == null ? null : c.getContent()); + } + protected enum State { INITIAL, AUTH_TEXT_SENT, @@ -102,16 +119,19 @@ public abstract class SaslMechanism { this.account = account; } - public SaslMechanism of( - final Collection mechanisms, final Collection bindings) { - final ChannelBinding channelBinding = ChannelBinding.best(bindings); + private SaslMechanism of( + final Collection mechanisms, final ChannelBinding channelBinding) { + Preconditions.checkNotNull(channelBinding, "Use ChannelBinding.NONE instead of null"); if (mechanisms.contains(External.MECHANISM) && account.getPrivateKeyAlias() != null) { return new External(account); - } else if (mechanisms.contains(ScramSha512Plus.MECHANISM) && channelBinding != null) { + } else if (mechanisms.contains(ScramSha512Plus.MECHANISM) + && channelBinding != ChannelBinding.NONE) { return new ScramSha512Plus(account, channelBinding); - } else if (mechanisms.contains(ScramSha256Plus.MECHANISM) && channelBinding != null) { + } else if (mechanisms.contains(ScramSha256Plus.MECHANISM) + && channelBinding != ChannelBinding.NONE) { return new ScramSha256Plus(account, channelBinding); - } else if (mechanisms.contains(ScramSha1Plus.MECHANISM) && channelBinding != null) { + } else if (mechanisms.contains(ScramSha1Plus.MECHANISM) + && channelBinding != ChannelBinding.NONE) { return new ScramSha1Plus(account, channelBinding); } else if (mechanisms.contains(ScramSha512.MECHANISM)) { return new ScramSha512(account); @@ -131,9 +151,33 @@ public abstract class SaslMechanism { } } + public SaslMechanism of( + final Collection mechanisms, + final Collection bindings, + final SSLSockets.Version sslVersion) { + final ChannelBinding channelBinding = ChannelBinding.best(bindings, sslVersion); + return of(mechanisms, channelBinding); + } + public SaslMechanism of(final String mechanism, final ChannelBinding channelBinding) { - return of(Collections.singleton(mechanism), Collections.singleton(channelBinding)); + return of(Collections.singleton(mechanism), channelBinding); } + } + public static SaslMechanism ensureAvailable( + final SaslMechanism mechanism, final SSLSockets.Version sslVersion) { + if (mechanism instanceof ScramPlusMechanism) { + final ChannelBinding cb = ((ScramPlusMechanism) mechanism).getChannelBinding(); + if (ChannelBinding.ensureBest(cb, sslVersion)) { + return mechanism; + } else { + Log.d( + Config.LOGTAG, + "pinned channel binding method " + cb + " no longer available"); + return null; + } + } else { + return mechanism; + } } } diff --git a/src/main/java/eu/siacs/conversations/utils/SSLSocketHelper.java b/src/main/java/eu/siacs/conversations/utils/SSLSockets.java similarity index 76% rename from src/main/java/eu/siacs/conversations/utils/SSLSocketHelper.java rename to src/main/java/eu/siacs/conversations/utils/SSLSockets.java index 53d4f4169a302575751072794d0a73027d843442..ae853bea8a4914ff03bab3a71554659b32119c5e 100644 --- a/src/main/java/eu/siacs/conversations/utils/SSLSocketHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/SSLSockets.java @@ -5,9 +5,12 @@ import android.util.Log; import androidx.annotation.RequiresApi; +import com.google.common.base.Strings; + import org.conscrypt.Conscrypt; import java.lang.reflect.Method; +import java.net.Socket; import java.nio.charset.StandardCharsets; import java.security.NoSuchAlgorithmException; import java.util.Arrays; @@ -24,7 +27,7 @@ import javax.net.ssl.SSLSocket; import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; -public class SSLSocketHelper { +public class SSLSockets { public static void setSecurity(final SSLSocket sslSocket) { final String[] supportProtocols; @@ -100,6 +103,45 @@ public class SSLSocketHelper { public static void log(Account account, SSLSocket socket) { SSLSession session = socket.getSession(); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": protocol=" + session.getProtocol() + " cipher=" + session.getCipherSuite()); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": protocol=" + + session.getProtocol() + + " cipher=" + + session.getCipherSuite()); + } + + public static Version version(final Socket socket) { + if (socket instanceof SSLSocket) { + final SSLSocket sslSocket = (SSLSocket) socket; + return Version.of(sslSocket.getSession().getProtocol()); + } else { + return Version.NONE; + } + } + + public enum Version { + TLS_1_0, + TLS_1_1, + TLS_1_2, + TLS_1_3, + UNKNOWN, + NONE; + + private static Version of(final String protocol) { + switch (Strings.nullToEmpty(protocol)) { + case "TLSv1": + return TLS_1_0; + case "TLSv1.1": + return TLS_1_1; + case "TLSv1.2": + return TLS_1_2; + case "TLSv1.3": + return TLS_1_3; + default: + return UNKNOWN; + } + } } } diff --git a/src/main/java/eu/siacs/conversations/utils/TLSSocketFactory.java b/src/main/java/eu/siacs/conversations/utils/TLSSocketFactory.java index 8bd737c7e21f2a6e0ba5e1ddeec334f801b8b76d..6d5ce97aa05d4ce0d33cb56b3cfe6feb563652f9 100644 --- a/src/main/java/eu/siacs/conversations/utils/TLSSocketFactory.java +++ b/src/main/java/eu/siacs/conversations/utils/TLSSocketFactory.java @@ -17,7 +17,7 @@ public class TLSSocketFactory extends SSLSocketFactory { private final SSLSocketFactory internalSSLSocketFactory; public TLSSocketFactory(X509TrustManager[] trustManager, SecureRandom random) throws KeyManagementException, NoSuchAlgorithmException { - SSLContext context = SSLSocketHelper.getSSLContext(); + SSLContext context = SSLSockets.getSSLContext(); context.init(null, trustManager, random); this.internalSSLSocketFactory = context.getSocketFactory(); } @@ -59,7 +59,7 @@ public class TLSSocketFactory extends SSLSocketFactory { private static Socket enableTLSOnSocket(Socket socket) { if(socket instanceof SSLSocket) { - SSLSocketHelper.setSecurity((SSLSocket) socket); + SSLSockets.setSecurity((SSLSocket) socket); } return socket; } diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index d17891ff2b138f8de3b2ca58fc6251f1d3d305f9..55f45c6b552d2d20e2a94dde99023cd069f47e56 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -19,6 +19,7 @@ public final class Namespace { public static final String SASL = "urn:ietf:params:xml:ns:xmpp-sasl"; public static final String SASL_2 = "urn:xmpp:sasl:2"; public static final String CHANNEL_BINDING = "urn:xmpp:sasl-cb:0"; + public static final String FAST = "urn:xmpp:fast:0"; public static final String TLS = "urn:ietf:params:xml:ns:xmpp-tls"; public static final String PUBSUB = "http://jabber.org/protocol/pubsub"; public static final String PUBSUB_PUBLISH_OPTIONS = PUBSUB + "#publish-options"; diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index b467d8dc708b3206c57aaa7845ba4e3ff5f87d37..e693b2afa5fd55851304d1322d10ceee3f488108 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -15,9 +15,7 @@ import android.util.SparseArray; import androidx.annotation.NonNull; -import com.google.common.base.Predicates; import com.google.common.base.Strings; -import com.google.common.collect.Collections2; import org.xmlpull.v1.XmlPullParserException; @@ -80,7 +78,7 @@ import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.Patterns; import eu.siacs.conversations.utils.PhoneHelper; import eu.siacs.conversations.utils.Resolver; -import eu.siacs.conversations.utils.SSLSocketHelper; +import eu.siacs.conversations.utils.SSLSockets; import eu.siacs.conversations.utils.SocksSocketFactory; import eu.siacs.conversations.utils.XmlHelper; import eu.siacs.conversations.xml.Element; @@ -494,10 +492,11 @@ public class XmppConnection implements Runnable { tagWriter.beginDocument(); final boolean quickStart; if (socket instanceof SSLSocket) { - SSLSocketHelper.log(account, (SSLSocket) socket); - quickStart = establishStream(true); + final SSLSocket sslSocket = (SSLSocket) socket; + SSLSockets.log(account, sslSocket); + quickStart = establishStream(SSLSockets.version(sslSocket)); } else { - quickStart = establishStream(false); + quickStart = establishStream(SSLSockets.Version.NONE); } final Tag tag = tagReader.readTag(); if (Thread.currentThread().isInterrupted()) { @@ -512,7 +511,7 @@ public class XmppConnection implements Runnable { private SSLSocketFactory getSSLSocketFactory() throws NoSuchAlgorithmException, KeyManagementException { - final SSLContext sc = SSLSocketHelper.getSSLContext(); + final SSLContext sc = SSLSockets.getSSLContext(); final MemorizingTrustManager trustManager = this.mXmppConnectionService.getMemorizingTrustManager(); final KeyManager[] keyManager; @@ -720,7 +719,6 @@ public class XmppConnection implements Runnable { + ": server did not send stream features after SASL2 success"); throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } - Log.d(Config.LOGTAG, "success: " + success); final String authorizationIdentifier = success.findChildContent("authorization-identifier"); final Jid authorizationJid; @@ -785,7 +783,7 @@ public class XmppConnection implements Runnable { processEnabled(streamManagementEnabled); waitForDisco = true; } else { - //if we didn’t enable stream managment in bind do it now + //if we did not enable stream management in bind do it now waitForDisco = enableStreamManagement(); } if (carbonsEnabled != null) { @@ -800,7 +798,7 @@ public class XmppConnection implements Runnable { this.quickStartInProgress = false; if (version == SaslMechanism.Version.SASL) { tagReader.reset(); - sendStartStream(true); + sendStartStream(false, true); final Tag tag = tagReader.readTag(); if (tag != null && tag.isStart("stream", Namespace.STREAMS)) { processStream(); @@ -1163,7 +1161,7 @@ public class XmppConnection implements Runnable { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": TLS connection established"); final boolean quickStart; try { - quickStart = establishStream(true); + quickStart = establishStream(SSLSockets.version(sslSocket)); } catch (final InterruptedException e) { return; } @@ -1173,7 +1171,7 @@ public class XmppConnection implements Runnable { features.encryptionEnabled = true; final Tag tag = tagReader.readTag(); if (tag != null && tag.isStart("stream", Namespace.STREAMS)) { - SSLSocketHelper.log(account, sslSocket); + SSLSockets.log(account, sslSocket); processStream(); } else { throw new StateChangingException(Account.State.STREAM_OPENING_ERROR); @@ -1193,9 +1191,9 @@ public class XmppConnection implements Runnable { (SSLSocket) sslSocketFactory.createSocket( socket, address.getHostAddress(), socket.getPort(), true); - SSLSocketHelper.setSecurity(sslSocket); - SSLSocketHelper.setHostname(sslSocket, IDN.toASCII(account.getServer())); - SSLSocketHelper.setApplicationProtocol(sslSocket, "xmpp-client"); + SSLSockets.setSecurity(sslSocket); + SSLSockets.setHostname(sslSocket, IDN.toASCII(account.getServer())); + SSLSockets.setApplicationProtocol(sslSocket, "xmpp-client"); final XmppDomainVerifier xmppDomainVerifier = new XmppDomainVerifier(); try { if (!xmppDomainVerifier.verify( @@ -1251,8 +1249,7 @@ public class XmppConnection implements Runnable { } else if (!this.streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE) && account.isOptionSet(Account.OPTION_REGISTER)) { throw new StateChangingException(Account.State.REGISTRATION_NOT_SUPPORTED); - } else if (Config.SASL_2_ENABLED - && this.streamFeatures.hasChild("authentication", Namespace.SASL_2) + } else if (this.streamFeatures.hasChild("authentication", Namespace.SASL_2) && shouldAuthenticate && isSecure) { authenticate(SaslMechanism.Version.SASL_2); @@ -1301,29 +1298,14 @@ public class XmppConnection implements Runnable { } else { authElement = this.streamFeatures.findChild("authentication", Namespace.SASL_2); } - //TODO externalize - final Collection mechanisms = - Collections2.transform( - Collections2.filter( - authElement.getChildren(), - c -> c != null && "mechanism".equals(c.getName())), - c -> c == null ? null : c.getContent()); + final Collection mechanisms = SaslMechanism.mechanisms(authElement); final Element cbElement = this.streamFeatures.findChild("sasl-channel-binding", Namespace.CHANNEL_BINDING); - final Collection channelBindings = - Collections2.filter( - Collections2.transform( - Collections2.filter( - cbElement == null - ? Collections.emptyList() - : cbElement.getChildren(), - c -> c != null && "channel-binding".equals(c.getName())), - c -> c == null ? null : ChannelBinding.of(c.getAttribute("type"))), - Predicates.notNull()); + final Collection channelBindings = ChannelBinding.of(cbElement); Log.d(Config.LOGTAG,"mechanisms: "+mechanisms); Log.d(Config.LOGTAG, "channel bindings: " + channelBindings); final SaslMechanism.Factory factory = new SaslMechanism.Factory(account); - this.saslMechanism = factory.of(mechanisms, channelBindings); + this.saslMechanism = factory.of(mechanisms, channelBindings, SSLSockets.version(this.socket)); //TODO externalize checks @@ -1360,6 +1342,9 @@ public class XmppConnection implements Runnable { } else if (version == SaslMechanism.Version.SASL_2) { final Element inline = authElement.findChild("inline", Namespace.SASL_2); final boolean sm = inline != null && inline.hasChild("sm", "urn:xmpp:sm:3"); + final Element fast = inline == null ? null : inline.findChild("fast", Namespace.FAST); + final Collection fastMechanisms = SaslMechanism.mechanisms(fast); + Log.d(Config.LOGTAG,"fast mechanisms: "+fastMechanisms); final Collection bindFeatures = Bind2.features(inline); quickStartAvailable = sm @@ -1434,12 +1419,11 @@ public class XmppConnection implements Runnable { Log.d(Config.LOGTAG, "inline bind features: " + bindFeatures); final Element bind = new Element("bind", Namespace.BIND2); bind.addChild("tag").setContent(mXmppConnectionService.getString(R.string.app_name)); - final Element features = bind.addChild("features"); if (bindFeatures.contains(Namespace.CARBONS)) { - features.addChild("enable", Namespace.CARBONS); + bind.addChild("enable", Namespace.CARBONS); } if (bindFeatures.contains(Namespace.STREAM_MANAGEMENT)) { - features.addChild(new EnablePacket()); + bind.addChild(new EnablePacket()); } return bind; } @@ -2060,34 +2044,40 @@ public class XmppConnection implements Runnable { } } - private boolean establishStream(final boolean secureConnection) throws IOException, InterruptedException { - final SaslMechanism saslMechanism = account.getPinnedMechanism(); + private boolean establishStream(final SSLSockets.Version sslVersion) + throws IOException, InterruptedException { + final SaslMechanism pinnedMechanism = + SaslMechanism.ensureAvailable(account.getPinnedMechanism(), sslVersion); + final boolean secureConnection = sslVersion != SSLSockets.Version.NONE; if (secureConnection - && Config.SASL_2_ENABLED - && saslMechanism != null + && Config.QUICKSTART_ENABLED + && pinnedMechanism != null && account.isOptionSet(Account.OPTION_QUICKSTART_AVAILABLE)) { mXmppConnectionService.restoredFromDatabaseLatch.await(); - this.saslMechanism = saslMechanism; + this.saslMechanism = pinnedMechanism; final Element authenticate = - generateAuthenticationRequest(saslMechanism.getClientFirstMessage()); - authenticate.setAttribute("mechanism", saslMechanism.getMechanism()); - sendStartStream(false); + generateAuthenticationRequest(pinnedMechanism.getClientFirstMessage()); + authenticate.setAttribute("mechanism", pinnedMechanism.getMechanism()); + sendStartStream(true, false); tagWriter.writeElement(authenticate); Log.d( Config.LOGTAG, account.getJid().toString() + ": quick start with " - + saslMechanism.getMechanism()); + + pinnedMechanism.getMechanism()); return true; } else { - sendStartStream(true); + sendStartStream(secureConnection, true); return false; } } - private void sendStartStream(final boolean flush) throws IOException { + private void sendStartStream(final boolean from, final boolean flush) throws IOException { final Tag stream = Tag.start("stream:stream"); stream.setAttribute("to", account.getServer()); + if (from) { + stream.setAttribute("from", account.getJid().asBareJid().toEscapedString()); + } stream.setAttribute("version", "1.0"); stream.setAttribute("xml:lang", LocalizedContent.STREAM_LANGUAGE); stream.setAttribute("xmlns", "jabber:client"); From 3378447f606024d89fc9f13ac5111c2cb0176ce9 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 15 Oct 2022 00:09:29 +0200 Subject: [PATCH 064/101] parse hash token names --- .../java/eu/siacs/conversations/Config.java | 2 +- .../crypto/sasl/ChannelBinding.java | 33 ++++- .../crypto/sasl/HashedToken.java | 114 ++++++++++++++++++ .../crypto/sasl/HashedTokenSha256.java | 24 ++++ .../conversations/xmpp/XmppConnection.java | 3 +- 5 files changed, 172 insertions(+), 4 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/crypto/sasl/HashedToken.java create mode 100644 src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha256.java diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index 812f6ae1048ba5700afcd08bc6d2854a911affef..27d1f1097d9ad731d0f402081d663d91dcf86f36 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -60,7 +60,7 @@ public final class Config { public static final long CONTACT_SYNC_RETRY_INTERVAL = 1000L * 60 * 5; - public static final boolean QUICKSTART_ENABLED = true; + public static final boolean QUICKSTART_ENABLED = false; //Notification settings public static final boolean HIDE_MESSAGE_TEXT_IN_NOTIFICATION = false; diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java index fb12555664dff0e58387dfbb786e79a02fc87f80..26f9c9da02883c04f11928e933c9471f7edfb9fd 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java @@ -6,7 +6,9 @@ import com.google.common.base.CaseFormat; import com.google.common.base.Preconditions; import com.google.common.base.Predicates; import com.google.common.base.Strings; +import com.google.common.collect.BiMap; import com.google.common.collect.Collections2; +import com.google.common.collect.ImmutableBiMap; import java.util.Arrays; import java.util.Collection; @@ -23,6 +25,16 @@ public enum ChannelBinding { TLS_SERVER_END_POINT, TLS_UNIQUE; + public static final BiMap SHORT_NAMES; + + static { + final ImmutableBiMap.Builder builder = ImmutableBiMap.builder(); + for (final ChannelBinding cb : values()) { + builder.put(cb, shortName(cb)); + } + SHORT_NAMES = builder.build(); + } + public static Collection of(final Element channelBinding) { Preconditions.checkArgument( channelBinding == null @@ -85,7 +97,24 @@ public enum ChannelBinding { } } - public static boolean ensureBest(final ChannelBinding channelBinding, final SSLSockets.Version sslVersion) { - return ChannelBinding.best(Collections.singleton(channelBinding), sslVersion) == channelBinding; + public static boolean ensureBest( + final ChannelBinding channelBinding, final SSLSockets.Version sslVersion) { + return ChannelBinding.best(Collections.singleton(channelBinding), sslVersion) + == channelBinding; + } + + private static String shortName(final ChannelBinding channelBinding) { + switch (channelBinding) { + case TLS_UNIQUE: + return "UNIQ"; + case TLS_EXPORTER: + return "EXPR"; + case TLS_SERVER_END_POINT: + return "ENDP"; + case NONE: + return "NONE"; + default: + throw new AssertionError("Missing short name for " + channelBinding); + } } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/HashedToken.java b/src/main/java/eu/siacs/conversations/crypto/sasl/HashedToken.java new file mode 100644 index 0000000000000000000000000000000000000000..f973c8377071d0a7b691799d7e90fd8abbfa6bf3 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/HashedToken.java @@ -0,0 +1,114 @@ +package eu.siacs.conversations.crypto.sasl; + +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.Multimap; +import com.google.common.hash.HashFunction; + +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import javax.net.ssl.SSLSocket; + +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.utils.SSLSockets; + +public abstract class HashedToken extends SaslMechanism { + + private static List HASH_FUNCTIONS = Arrays.asList("SHA-512", "SHA-256"); + + protected final ChannelBinding channelBinding; + + protected HashedToken(final Account account, final ChannelBinding channelBinding) { + super(account); + this.channelBinding = channelBinding; + } + + @Override + public int getPriority() { + throw new UnsupportedOperationException(); + } + + @Override + public String getClientFirstMessage() { + return null; // HMAC(token, "Initiator" || cb-data) + } + + @Override + public String getResponse(final String challenge, final SSLSocket socket) + throws AuthenticationException { + // todo verify that challenge matches HMAC(token, "Responder" || cb-data) + return null; + } + + protected abstract HashFunction getHashFunction(final byte[] key); + + public static final class Mechanism { + public final String hashFunction; + public final ChannelBinding channelBinding; + + public Mechanism(String hashFunction, ChannelBinding channelBinding) { + this.hashFunction = hashFunction; + this.channelBinding = channelBinding; + } + + public static Mechanism of(final String mechanism) { + final int first = mechanism.indexOf('-'); + final int last = mechanism.lastIndexOf('-'); + if (last <= first || mechanism.length() <= last) { + throw new IllegalArgumentException("Not a valid HashedToken name"); + } + if (mechanism.substring(0, first).equals("HT")) { + final String hashFunction = mechanism.substring(first + 1, last); + final String cbShortName = mechanism.substring(last + 1); + final ChannelBinding channelBinding = + ChannelBinding.SHORT_NAMES.inverse().get(cbShortName); + if (channelBinding == null) { + throw new IllegalArgumentException("Unknown channel binding " + cbShortName); + } + return new Mechanism(hashFunction, channelBinding); + } else { + throw new IllegalArgumentException("HashedToken name does not start with HT"); + } + } + + public static Multimap of(final Collection mechanisms) { + final ImmutableMultimap.Builder builder = + ImmutableMultimap.builder(); + for (final String name : mechanisms) { + try { + final Mechanism mechanism = Mechanism.of(name); + builder.put(mechanism.hashFunction, mechanism.channelBinding); + } catch (final IllegalArgumentException ignored) { + } + } + return builder.build(); + } + + public static Mechanism best( + final Collection mechanisms, final SSLSockets.Version sslVersion) { + final Multimap multimap = of(mechanisms); + for (final String hashFunction : HASH_FUNCTIONS) { + final Collection channelBindings = multimap.get(hashFunction); + if (channelBindings.isEmpty()) { + continue; + } + final ChannelBinding cb = ChannelBinding.best(channelBindings, sslVersion); + return new Mechanism(hashFunction, cb); + } + return null; + } + + @NotNull + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("hashFunction", hashFunction) + .add("channelBinding", channelBinding) + .toString(); + } + } +} diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha256.java b/src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha256.java new file mode 100644 index 0000000000000000000000000000000000000000..fae756485850db6e29501947357d80f432727322 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha256.java @@ -0,0 +1,24 @@ +package eu.siacs.conversations.crypto.sasl; + +import com.google.common.hash.HashFunction; +import com.google.common.hash.Hashing; + +import eu.siacs.conversations.entities.Account; + +public class HashedTokenSha256 extends HashedToken { + + public HashedTokenSha256(final Account account, final ChannelBinding channelBinding) { + super(account, channelBinding); + } + + @Override + protected HashFunction getHashFunction(final byte[] key) { + return Hashing.hmacSha256(key); + } + + @Override + public String getMechanism() { + final String cbShortName = ChannelBinding.SHORT_NAMES.get(this.channelBinding); + return String.format("HT-SHA-256-%s", cbShortName); + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index e693b2afa5fd55851304d1322d10ceee3f488108..f2b4c932a02dd8f9e267b0ac4894ad4e189f7f5b 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -63,6 +63,7 @@ import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.XmppDomainVerifier; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.sasl.ChannelBinding; +import eu.siacs.conversations.crypto.sasl.HashedToken; import eu.siacs.conversations.crypto.sasl.SaslMechanism; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Message; @@ -1344,7 +1345,7 @@ public class XmppConnection implements Runnable { final boolean sm = inline != null && inline.hasChild("sm", "urn:xmpp:sm:3"); final Element fast = inline == null ? null : inline.findChild("fast", Namespace.FAST); final Collection fastMechanisms = SaslMechanism.mechanisms(fast); - Log.d(Config.LOGTAG,"fast mechanisms: "+fastMechanisms); + Log.d(Config.LOGTAG,"fast mechanism: "+ HashedToken.Mechanism.best(fastMechanisms, SSLSockets.version(this.socket))); final Collection bindFeatures = Bind2.features(inline); quickStartAvailable = sm From c13787873c0324a1e40ae8c5c4700f297bd4a31f Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 15 Oct 2022 12:27:38 +0200 Subject: [PATCH 065/101] request fast token --- .../crypto/sasl/HashedToken.java | 12 ++++++++-- .../crypto/sasl/HashedTokenSha512.java | 24 +++++++++++++++++++ .../conversations/xmpp/XmppConnection.java | 18 +++++++++++--- 3 files changed, 49 insertions(+), 5 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha512.java diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/HashedToken.java b/src/main/java/eu/siacs/conversations/crypto/sasl/HashedToken.java index f973c8377071d0a7b691799d7e90fd8abbfa6bf3..b54864fea57646a0c8f3fb604d73f34d03b535df 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/HashedToken.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/HashedToken.java @@ -18,7 +18,9 @@ import eu.siacs.conversations.utils.SSLSockets; public abstract class HashedToken extends SaslMechanism { - private static List HASH_FUNCTIONS = Arrays.asList("SHA-512", "SHA-256"); + private static final String PREFIX = "HT"; + + private static final List HASH_FUNCTIONS = Arrays.asList("SHA-512", "SHA-256"); protected final ChannelBinding channelBinding; @@ -61,7 +63,7 @@ public abstract class HashedToken extends SaslMechanism { if (last <= first || mechanism.length() <= last) { throw new IllegalArgumentException("Not a valid HashedToken name"); } - if (mechanism.substring(0, first).equals("HT")) { + if (mechanism.substring(0, first).equals(PREFIX)) { final String hashFunction = mechanism.substring(first + 1, last); final String cbShortName = mechanism.substring(last + 1); final ChannelBinding channelBinding = @@ -110,5 +112,11 @@ public abstract class HashedToken extends SaslMechanism { .add("channelBinding", channelBinding) .toString(); } + + public String name() { + return String.format( + "%s-%s-%s", + PREFIX, hashFunction, ChannelBinding.SHORT_NAMES.get(channelBinding)); + } } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha512.java b/src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha512.java new file mode 100644 index 0000000000000000000000000000000000000000..fd1b7be51bc8d94e693426f8ce61abfe1ff4105e --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha512.java @@ -0,0 +1,24 @@ +package eu.siacs.conversations.crypto.sasl; + +import com.google.common.hash.HashFunction; +import com.google.common.hash.Hashing; + +import eu.siacs.conversations.entities.Account; + +public class HashedTokenSha512 extends HashedToken { + + public HashedTokenSha512(final Account account, final ChannelBinding channelBinding) { + super(account, channelBinding); + } + + @Override + protected HashFunction getHashFunction(final byte[] key) { + return Hashing.hmacSha512(key); + } + + @Override + public String getMechanism() { + final String cbShortName = ChannelBinding.SHORT_NAMES.get(this.channelBinding); + return String.format("HT-SHA-512-%s", cbShortName); + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index f2b4c932a02dd8f9e267b0ac4894ad4e189f7f5b..2fa8d6df3135534d5923f6f17ac1d2f90c0281ed 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -181,6 +181,7 @@ public class XmppConnection implements Runnable { private OnBindListener bindListener = null; private OnMessageAcknowledged acknowledgedListener = null; private SaslMechanism saslMechanism; + private HashedToken.Mechanism hashTokenRequest; private HttpUrl redirectionUrl = null; private String verifiedHostname = null; private volatile Thread mThread; @@ -761,6 +762,8 @@ public class XmppConnection implements Runnable { final Element bound = success.findChild("bound", Namespace.BIND2); final Element resumed = success.findChild("resumed", "urn:xmpp:sm:3"); final Element failed = success.findChild("failed", "urn:xmpp:sm:3"); + final Element tokenWrapper = success.findChild("token", Namespace.FAST); + final String token = tokenWrapper == null ? null : tokenWrapper.getAttribute("token"); if (bound != null && resumed != null) { Log.d( Config.LOGTAG, @@ -795,6 +798,10 @@ public class XmppConnection implements Runnable { } sendPostBindInitialization(waitForDisco, carbonsEnabled != null); } + //TODO figure out name either by the existence of hashTokenRequest or if scramMechanism is of instance HashedToken + if (this.hashTokenRequest != null && !Strings.isNullOrEmpty(token)) { + Log.d(Config.LOGTAG,account.getJid().asBareJid()+": storing hashed token "+this.hashTokenRequest.name()+ " "+token); + } } this.quickStartInProgress = false; if (version == SaslMechanism.Version.SASL) { @@ -1345,7 +1352,7 @@ public class XmppConnection implements Runnable { final boolean sm = inline != null && inline.hasChild("sm", "urn:xmpp:sm:3"); final Element fast = inline == null ? null : inline.findChild("fast", Namespace.FAST); final Collection fastMechanisms = SaslMechanism.mechanisms(fast); - Log.d(Config.LOGTAG,"fast mechanism: "+ HashedToken.Mechanism.best(fastMechanisms, SSLSockets.version(this.socket))); + final HashedToken.Mechanism hashTokenRequest = HashedToken.Mechanism.best(fastMechanisms, SSLSockets.version(this.socket)); final Collection bindFeatures = Bind2.features(inline); quickStartAvailable = sm @@ -1362,7 +1369,8 @@ public class XmppConnection implements Runnable { return; } } - authenticate = generateAuthenticationRequest(firstMessage, bindFeatures, sm); + this.hashTokenRequest = hashTokenRequest; + authenticate = generateAuthenticationRequest(firstMessage, hashTokenRequest, bindFeatures, sm); } else { throw new AssertionError("Missing implementation for " + version); } @@ -1383,11 +1391,12 @@ public class XmppConnection implements Runnable { } private Element generateAuthenticationRequest(final String firstMessage) { - return generateAuthenticationRequest(firstMessage, Bind2.QUICKSTART_FEATURES, true); + return generateAuthenticationRequest(firstMessage, null, Bind2.QUICKSTART_FEATURES, true); } private Element generateAuthenticationRequest( final String firstMessage, + final HashedToken.Mechanism hashedTokenRequest, final Collection bind, final boolean inlineStreamManagement) { final Element authenticate = new Element("authenticate", Namespace.SASL_2); @@ -1413,6 +1422,9 @@ public class XmppConnection implements Runnable { this.mWaitingForSmCatchup.set(true); authenticate.addChild(resume); } + if (hashedTokenRequest != null) { + authenticate.addChild("request-token", Namespace.FAST).setAttribute("mechanism", hashedTokenRequest.name()); + } return authenticate; } From 24badda4c9d99f597cb12f24ad8bd236195c7ce2 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 15 Oct 2022 18:56:31 +0200 Subject: [PATCH 066/101] do quick start with HT-SHA-256-NONE --- .../java/eu/siacs/conversations/Config.java | 2 +- .../crypto/sasl/ChannelBinding.java | 2 +- .../crypto/sasl/ChannelBindingMechanism.java | 6 + .../crypto/sasl/HashedToken.java | 57 ++++++- .../crypto/sasl/HashedTokenSha256.java | 5 +- .../crypto/sasl/HashedTokenSha512.java | 5 +- .../crypto/sasl/SaslMechanism.java | 6 +- .../crypto/sasl/ScramPlusMechanism.java | 3 +- .../siacs/conversations/entities/Account.java | 144 ++++++++++++++---- .../persistance/DatabaseBackend.java | 9 +- .../conversations/xmpp/XmppConnection.java | 60 +++++--- 11 files changed, 237 insertions(+), 62 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBindingMechanism.java diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index 27d1f1097d9ad731d0f402081d663d91dcf86f36..812f6ae1048ba5700afcd08bc6d2854a911affef 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -60,7 +60,7 @@ public final class Config { public static final long CONTACT_SYNC_RETRY_INTERVAL = 1000L * 60 * 5; - public static final boolean QUICKSTART_ENABLED = false; + public static final boolean QUICKSTART_ENABLED = true; //Notification settings public static final boolean HIDE_MESSAGE_TEXT_IN_NOTIFICATION = false; diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java index 26f9c9da02883c04f11928e933c9471f7edfb9fd..216f3d7f81f834b02d5e8640e5b1ba57ff6e62cb 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java @@ -97,7 +97,7 @@ public enum ChannelBinding { } } - public static boolean ensureBest( + public static boolean isAvailable( final ChannelBinding channelBinding, final SSLSockets.Version sslVersion) { return ChannelBinding.best(Collections.singleton(channelBinding), sslVersion) == channelBinding; diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBindingMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBindingMechanism.java new file mode 100644 index 0000000000000000000000000000000000000000..d4e34ba59230320500fbb37bcba9b08084e212e0 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBindingMechanism.java @@ -0,0 +1,6 @@ +package eu.siacs.conversations.crypto.sasl; + +public interface ChannelBindingMechanism { + + ChannelBinding getChannelBinding(); +} diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/HashedToken.java b/src/main/java/eu/siacs/conversations/crypto/sasl/HashedToken.java index b54864fea57646a0c8f3fb604d73f34d03b535df..1d8aeac69cb20091b657ccc295ba080007cf4c51 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/HashedToken.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/HashedToken.java @@ -1,12 +1,17 @@ package eu.siacs.conversations.crypto.sasl; +import android.util.Base64; + import com.google.common.base.MoreObjects; +import com.google.common.base.Strings; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.Multimap; import com.google.common.hash.HashFunction; +import com.google.common.primitives.Bytes; import org.jetbrains.annotations.NotNull; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -16,11 +21,13 @@ import javax.net.ssl.SSLSocket; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.utils.SSLSockets; -public abstract class HashedToken extends SaslMechanism { +public abstract class HashedToken extends SaslMechanism implements ChannelBindingMechanism { private static final String PREFIX = "HT"; private static final List HASH_FUNCTIONS = Arrays.asList("SHA-512", "SHA-256"); + private static final byte[] INITIATOR = "Initiator".getBytes(StandardCharsets.UTF_8); + private static final byte[] RESPONDER = "Responder".getBytes(StandardCharsets.UTF_8); protected final ChannelBinding channelBinding; @@ -36,18 +43,48 @@ public abstract class HashedToken extends SaslMechanism { @Override public String getClientFirstMessage() { - return null; // HMAC(token, "Initiator" || cb-data) + final String token = Strings.nullToEmpty(this.account.getFastToken()); + final HashFunction hashing = getHashFunction(token.getBytes(StandardCharsets.UTF_8)); + final byte[] cbData = new byte[0]; + final byte[] initiatorHashedToken = + hashing.hashBytes(Bytes.concat(INITIATOR, cbData)).asBytes(); + final byte[] firstMessage = + Bytes.concat( + account.getUsername().getBytes(StandardCharsets.UTF_8), + new byte[] {0x00}, + initiatorHashedToken); + return Base64.encodeToString(firstMessage, Base64.NO_WRAP); } @Override public String getResponse(final String challenge, final SSLSocket socket) throws AuthenticationException { - // todo verify that challenge matches HMAC(token, "Responder" || cb-data) - return null; + final byte[] responderMessage; + try { + responderMessage = Base64.decode(challenge, Base64.NO_WRAP); + } catch (final Exception e) { + throw new AuthenticationException("Unable to decode responder message", e); + } + final String token = Strings.nullToEmpty(this.account.getFastToken()); + final HashFunction hashing = getHashFunction(token.getBytes(StandardCharsets.UTF_8)); + final byte[] cbData = new byte[0]; + final byte[] expectedResponderMessage = + hashing.hashBytes(Bytes.concat(RESPONDER, cbData)).asBytes(); + if (Arrays.equals(responderMessage, expectedResponderMessage)) { + return null; + } + throw new AuthenticationException("Responder message did not match"); } protected abstract HashFunction getHashFunction(final byte[] key); + public abstract Mechanism getTokenMechanism(); + + @Override + public String getMechanism() { + return getTokenMechanism().name(); + } + public static final class Mechanism { public final String hashFunction; public final ChannelBinding channelBinding; @@ -77,6 +114,14 @@ public abstract class HashedToken extends SaslMechanism { } } + public static Mechanism ofOrNull(final String mechanism) { + try { + return mechanism == null ? null : of(mechanism); + } catch (final IllegalArgumentException e) { + return null; + } + } + public static Multimap of(final Collection mechanisms) { final ImmutableMultimap.Builder builder = ImmutableMultimap.builder(); @@ -119,4 +164,8 @@ public abstract class HashedToken extends SaslMechanism { PREFIX, hashFunction, ChannelBinding.SHORT_NAMES.get(channelBinding)); } } + + public ChannelBinding getChannelBinding() { + return this.channelBinding; + } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha256.java b/src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha256.java index fae756485850db6e29501947357d80f432727322..aef19d72a95155909b973c0cd0d8d96b230b3fa7 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha256.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha256.java @@ -17,8 +17,7 @@ public class HashedTokenSha256 extends HashedToken { } @Override - public String getMechanism() { - final String cbShortName = ChannelBinding.SHORT_NAMES.get(this.channelBinding); - return String.format("HT-SHA-256-%s", cbShortName); + public Mechanism getTokenMechanism() { + return new Mechanism("SHA-256", channelBinding); } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha512.java b/src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha512.java index fd1b7be51bc8d94e693426f8ce61abfe1ff4105e..6f48b5444fc6ec6e7dd3193ac2d48e53e0baa1f3 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha512.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha512.java @@ -17,8 +17,7 @@ public class HashedTokenSha512 extends HashedToken { } @Override - public String getMechanism() { - final String cbShortName = ChannelBinding.SHORT_NAMES.get(this.channelBinding); - return String.format("HT-SHA-512-%s", cbShortName); + public Mechanism getTokenMechanism() { + return new Mechanism("SHA-512", this.channelBinding); } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java index e0df3a2d4326475b725095a1e4c744635b1392e0..48835f9df727356f63ebcbba99598ca20d932841 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java @@ -166,9 +166,9 @@ public abstract class SaslMechanism { public static SaslMechanism ensureAvailable( final SaslMechanism mechanism, final SSLSockets.Version sslVersion) { - if (mechanism instanceof ScramPlusMechanism) { - final ChannelBinding cb = ((ScramPlusMechanism) mechanism).getChannelBinding(); - if (ChannelBinding.ensureBest(cb, sslVersion)) { + if (mechanism instanceof ChannelBindingMechanism) { + final ChannelBinding cb = ((ChannelBindingMechanism) mechanism).getChannelBinding(); + if (ChannelBinding.isAvailable(cb, sslVersion)) { return mechanism; } else { Log.d( diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java index 707883d734d26a936776e36648c9dbf9766723bb..c6a63ddbdaa329ab64f472b1290c52370fe56046 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java @@ -16,7 +16,7 @@ import javax.net.ssl.SSLSocket; import eu.siacs.conversations.entities.Account; -public abstract class ScramPlusMechanism extends ScramMechanism { +public abstract class ScramPlusMechanism extends ScramMechanism implements ChannelBindingMechanism { private static final String EXPORTER_LABEL = "EXPORTER-Channel-Binding"; @@ -103,6 +103,7 @@ public abstract class ScramPlusMechanism extends ScramMechanism { return messageDigest.digest(); } + @Override public ChannelBinding getChannelBinding() { return this.channelBinding; } diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index 1817c24bb7ae3b84dfd4fb151e6b02b717fd14da..d570cbec355de9c13d603eeb6fceb63e6b5688b3 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -26,6 +26,9 @@ import eu.siacs.conversations.crypto.PgpDecryptionService; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession; import eu.siacs.conversations.crypto.sasl.ChannelBinding; +import eu.siacs.conversations.crypto.sasl.HashedToken; +import eu.siacs.conversations.crypto.sasl.HashedTokenSha256; +import eu.siacs.conversations.crypto.sasl.HashedTokenSha512; import eu.siacs.conversations.crypto.sasl.SaslMechanism; import eu.siacs.conversations.crypto.sasl.ScramPlusMechanism; import eu.siacs.conversations.services.AvatarService; @@ -55,7 +58,8 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable public static final String RESOURCE = "resource"; public static final String PINNED_MECHANISM = "pinned_mechanism"; public static final String PINNED_CHANNEL_BINDING = "pinned_channel_binding"; - + public static final String FAST_MECHANISM = "fast_mechanism"; + public static final String FAST_TOKEN = "fast_token"; public static final int OPTION_DISABLED = 1; public static final int OPTION_REGISTER = 2; @@ -72,7 +76,6 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable private static final String KEY_PINNED_MECHANISM = "pinned_mechanism"; public static final String KEY_PRE_AUTH_REGISTRATION_TOKEN = "pre_auth_registration"; - protected final JSONObject keys; private final Roster roster = new Roster(this); private final Collection blocklist = new CopyOnWriteArraySet<>(); @@ -101,16 +104,46 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable private String presenceStatusMessage; private String pinnedMechanism; private String pinnedChannelBinding; + private String fastMechanism; + private String fastToken; public Account(final Jid jid, final String password) { - this(java.util.UUID.randomUUID().toString(), jid, - password, 0, null, "", null, null, null, 5222, Presence.Status.ONLINE, null, null, null); - } - - private Account(final String uuid, final Jid jid, - final String password, final int options, final String rosterVersion, final String keys, - final String avatar, String displayName, String hostname, int port, - final Presence.Status status, String statusMessage, final String pinnedMechanism, final String pinnedChannelBinding) { + this( + java.util.UUID.randomUUID().toString(), + jid, + password, + 0, + null, + "", + null, + null, + null, + 5222, + Presence.Status.ONLINE, + null, + null, + null, + null, + null); + } + + private Account( + final String uuid, + final Jid jid, + final String password, + final int options, + final String rosterVersion, + final String keys, + final String avatar, + String displayName, + String hostname, + int port, + final Presence.Status status, + String statusMessage, + final String pinnedMechanism, + final String pinnedChannelBinding, + final String fastMechanism, + final String fastToken) { this.uuid = uuid; this.jid = jid; this.password = password; @@ -131,21 +164,29 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable this.presenceStatusMessage = statusMessage; this.pinnedMechanism = pinnedMechanism; this.pinnedChannelBinding = pinnedChannelBinding; + this.fastMechanism = fastMechanism; + this.fastToken = fastToken; } public static Account fromCursor(final Cursor cursor) { final Jid jid; try { final String resource = cursor.getString(cursor.getColumnIndexOrThrow(RESOURCE)); - jid = Jid.of( - cursor.getString(cursor.getColumnIndexOrThrow(USERNAME)), - cursor.getString(cursor.getColumnIndexOrThrow(SERVER)), - resource == null || resource.trim().isEmpty() ? null : resource); + jid = + Jid.of( + cursor.getString(cursor.getColumnIndexOrThrow(USERNAME)), + cursor.getString(cursor.getColumnIndexOrThrow(SERVER)), + resource == null || resource.trim().isEmpty() ? null : resource); } catch (final IllegalArgumentException e) { - Log.d(Config.LOGTAG, cursor.getString(cursor.getColumnIndexOrThrow(USERNAME)) + "@" + cursor.getString(cursor.getColumnIndexOrThrow(SERVER))); + Log.d( + Config.LOGTAG, + cursor.getString(cursor.getColumnIndexOrThrow(USERNAME)) + + "@" + + cursor.getString(cursor.getColumnIndexOrThrow(SERVER))); throw new AssertionError(e); } - return new Account(cursor.getString(cursor.getColumnIndexOrThrow(UUID)), + return new Account( + cursor.getString(cursor.getColumnIndexOrThrow(UUID)), jid, cursor.getString(cursor.getColumnIndexOrThrow(PASSWORD)), cursor.getInt(cursor.getColumnIndexOrThrow(OPTIONS)), @@ -155,10 +196,13 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable cursor.getString(cursor.getColumnIndexOrThrow(DISPLAY_NAME)), cursor.getString(cursor.getColumnIndexOrThrow(HOSTNAME)), cursor.getInt(cursor.getColumnIndexOrThrow(PORT)), - Presence.Status.fromShowString(cursor.getString(cursor.getColumnIndexOrThrow(STATUS))), + Presence.Status.fromShowString( + cursor.getString(cursor.getColumnIndexOrThrow(STATUS))), cursor.getString(cursor.getColumnIndexOrThrow(STATUS_MESSAGE)), cursor.getString(cursor.getColumnIndexOrThrow(PINNED_MECHANISM)), - cursor.getString(cursor.getColumnIndexOrThrow(PINNED_CHANNEL_BINDING))); + cursor.getString(cursor.getColumnIndexOrThrow(PINNED_CHANNEL_BINDING)), + cursor.getString(cursor.getColumnIndexOrThrow(FAST_MECHANISM)), + cursor.getString(cursor.getColumnIndexOrThrow(FAST_TOKEN))); } public boolean httpUploadAvailable(long size) { @@ -305,10 +349,18 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable public void setPinnedMechanism(final SaslMechanism mechanism) { this.pinnedMechanism = mechanism.getMechanism(); if (mechanism instanceof ScramPlusMechanism) { - this.pinnedChannelBinding = ((ScramPlusMechanism) mechanism).getChannelBinding().toString(); + this.pinnedChannelBinding = + ((ScramPlusMechanism) mechanism).getChannelBinding().toString(); + } else { + this.pinnedChannelBinding = null; } } + public void setFastToken(final HashedToken.Mechanism mechanism, final String token) { + this.fastMechanism = mechanism.name(); + this.fastToken = token; + } + public void resetPinnedMechanism() { this.pinnedMechanism = null; this.pinnedChannelBinding = null; @@ -328,12 +380,39 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable } } - public SaslMechanism getPinnedMechanism() { + private SaslMechanism getPinnedMechanism() { final String mechanism = Strings.nullToEmpty(this.pinnedMechanism); final ChannelBinding channelBinding = ChannelBinding.get(this.pinnedChannelBinding); return new SaslMechanism.Factory(this).of(mechanism, channelBinding); } + private HashedToken getFastMechanism() { + final HashedToken.Mechanism fastMechanism = HashedToken.Mechanism.ofOrNull(this.fastMechanism); + final String token = this.fastToken; + if (fastMechanism == null || Strings.isNullOrEmpty(token)) { + return null; + } + if (fastMechanism.hashFunction.equals("SHA-256")) { + return new HashedTokenSha256(this, fastMechanism.channelBinding); + } else if (fastMechanism.hashFunction.equals("SHA-512")) { + return new HashedTokenSha512(this, fastMechanism.channelBinding); + } else { + return null; + } + } + + public SaslMechanism getQuickStartMechanism() { + final HashedToken hashedTokenMechanism = getFastMechanism(); + if (hashedTokenMechanism != null) { + return hashedTokenMechanism; + } + return getPinnedMechanism(); + } + + public String getFastToken() { + return this.fastToken; + } + public State getTrueStatus() { return this.status; } @@ -435,6 +514,8 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable values.put(RESOURCE, jid.getResource()); values.put(PINNED_MECHANISM, pinnedMechanism); values.put(PINNED_CHANNEL_BINDING, pinnedChannelBinding); + values.put(FAST_MECHANISM, this.fastMechanism); + values.put(FAST_TOKEN, this.fastToken); return values; } @@ -480,7 +561,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable public int activeDevicesWithRtpCapability() { int i = 0; - for(Presence presence : getSelfContact().getPresences().getPresences()) { + for (Presence presence : getSelfContact().getPresences().getPresences()) { if (RtpCapability.check(presence) != RtpCapability.Capability.NONE) { i++; } @@ -617,7 +698,9 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable public String getShareableLink() { List fingerprints = this.getFingerprints(); - String uri = "https://conversations.im/i/" + XmppUri.lameUrlEncode(this.getJid().asBareJid().toEscapedString()); + String uri = + "https://conversations.im/i/" + + XmppUri.lameUrlEncode(this.getJid().asBareJid().toEscapedString()); if (fingerprints.size() > 0) { return XmppUri.getFingerprintUri(uri, fingerprints, '&'); } else { @@ -630,10 +713,18 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable if (axolotlService == null) { return fingerprints; } - fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OMEMO, axolotlService.getOwnFingerprint().substring(2), axolotlService.getOwnDeviceId())); + fingerprints.add( + new XmppUri.Fingerprint( + XmppUri.FingerprintType.OMEMO, + axolotlService.getOwnFingerprint().substring(2), + axolotlService.getOwnDeviceId())); for (XmppAxolotlSession session : axolotlService.findOwnSessions()) { if (session.getTrust().isVerified() && session.getTrust().isActive()) { - fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OMEMO, session.getFingerprint().substring(2).replaceAll("\\s", ""), session.getRemoteAddress().getDeviceId())); + fingerprints.add( + new XmppUri.Fingerprint( + XmppUri.FingerprintType.OMEMO, + session.getFingerprint().substring(2).replaceAll("\\s", ""), + session.getRemoteAddress().getDeviceId())); } } return fingerprints; @@ -641,7 +732,8 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable public boolean isBlocked(final ListItem contact) { final Jid jid = contact.getJid(); - return jid != null && (blocklist.contains(jid.asBareJid()) || blocklist.contains(jid.getDomain())); + return jid != null + && (blocklist.contains(jid.asBareJid()) || blocklist.contains(jid.getDomain())); } public boolean isBlocked(final Jid jid) { @@ -685,7 +777,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable REGISTRATION_CONFLICT(true, false), REGISTRATION_NOT_SUPPORTED(true, false), REGISTRATION_PLEASE_WAIT(true, false), - REGISTRATION_INVALID_TOKEN(true,false), + REGISTRATION_INVALID_TOKEN(true, false), REGISTRATION_PASSWORD_TOO_WEAK(true, false), TLS_ERROR, TLS_ERROR_DOMAIN, diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index 49de553ebc27190480530e527064e91a9d084e15..3eb48a1c94c5c80f39dc97dfb0ea584d374733ba 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -64,7 +64,7 @@ import eu.siacs.conversations.xmpp.mam.MamReference; public class DatabaseBackend extends SQLiteOpenHelper { private static final String DATABASE_NAME = "history"; - private static final int DATABASE_VERSION = 50; + private static final int DATABASE_VERSION = 51; private static boolean requiresMessageIndexRebuild = false; private static DatabaseBackend instance = null; @@ -232,6 +232,8 @@ public class DatabaseBackend extends SQLiteOpenHelper { + Account.RESOURCE + " TEXT," + Account.PINNED_MECHANISM + " TEXT," + Account.PINNED_CHANNEL_BINDING + " TEXT," + + Account.FAST_MECHANISM + " TEXT," + + Account.FAST_TOKEN + " TEXT," + Account.PORT + " NUMBER DEFAULT 5222)"); db.execSQL("create table " + Conversation.TABLENAME + " (" + Conversation.UUID + " TEXT PRIMARY KEY, " + Conversation.NAME @@ -594,7 +596,10 @@ public class DatabaseBackend extends SQLiteOpenHelper { if (oldVersion < 50 && newVersion >= 50) { db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.PINNED_MECHANISM + " TEXT"); db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.PINNED_CHANNEL_BINDING + " TEXT"); - + } + if (oldVersion < 51 && newVersion >= 51) { + db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.FAST_MECHANISM + " TEXT"); + db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.FAST_TOKEN + " TEXT"); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 2fa8d6df3135534d5923f6f17ac1d2f90c0281ed..2c60534dd0faed2dda5c1d29949acad3f0fe0763 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -757,7 +757,6 @@ public class XmppConnection implements Runnable { Config.LOGTAG, account.getJid().asBareJid() + ": jid changed during SASL 2.0. updating database"); - mXmppConnectionService.databaseBackend.updateAccount(account); } final Element bound = success.findChild("bound", Namespace.BIND2); final Element resumed = success.findChild("resumed", "urn:xmpp:sm:3"); @@ -798,11 +797,21 @@ public class XmppConnection implements Runnable { } sendPostBindInitialization(waitForDisco, carbonsEnabled != null); } - //TODO figure out name either by the existence of hashTokenRequest or if scramMechanism is of instance HashedToken - if (this.hashTokenRequest != null && !Strings.isNullOrEmpty(token)) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": storing hashed token "+this.hashTokenRequest.name()+ " "+token); + final HashedToken.Mechanism tokenMechanism; + final SaslMechanism currentMechanism = this.saslMechanism; + if (currentMechanism instanceof HashedToken) { + tokenMechanism = ((HashedToken) currentMechanism).getTokenMechanism(); + } else if (this.hashTokenRequest != null) { + tokenMechanism = this.hashTokenRequest; + } else { + tokenMechanism = null; + } + if (tokenMechanism != null && !Strings.isNullOrEmpty(token)) { + this.account.setFastToken(tokenMechanism,token); + Log.d(Config.LOGTAG,account.getJid().asBareJid()+": storing hashed token "+tokenMechanism); } } + mXmppConnectionService.databaseBackend.updateAccount(account); this.quickStartInProgress = false; if (version == SaslMechanism.Version.SASL) { tagReader.reset(); @@ -826,6 +835,7 @@ public class XmppConnection implements Runnable { } catch (final IllegalArgumentException e) { throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } + Log.d(Config.LOGTAG,failure.toString()); Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": login failure " + version); if (failure.hasChild("temporary-auth-failure")) { throw new StateChangingException(Account.State.TEMPORARY_AUTH_FAILURE); @@ -1340,6 +1350,7 @@ public class XmppConnection implements Runnable { } final boolean quickStartAvailable; final String firstMessage = saslMechanism.getClientFirstMessage(); + final boolean usingFast = saslMechanism instanceof HashedToken; final Element authenticate; if (version == SaslMechanism.Version.SASL) { authenticate = new Element("auth", Namespace.SASL); @@ -1350,9 +1361,15 @@ public class XmppConnection implements Runnable { } else if (version == SaslMechanism.Version.SASL_2) { final Element inline = authElement.findChild("inline", Namespace.SASL_2); final boolean sm = inline != null && inline.hasChild("sm", "urn:xmpp:sm:3"); - final Element fast = inline == null ? null : inline.findChild("fast", Namespace.FAST); - final Collection fastMechanisms = SaslMechanism.mechanisms(fast); - final HashedToken.Mechanism hashTokenRequest = HashedToken.Mechanism.best(fastMechanisms, SSLSockets.version(this.socket)); + final HashedToken.Mechanism hashTokenRequest; + if (usingFast) { + hashTokenRequest = null; + } else { + final Element fast = inline == null ? null : inline.findChild("fast", Namespace.FAST); + final Collection fastMechanisms = SaslMechanism.mechanisms(fast); + hashTokenRequest = + HashedToken.Mechanism.best(fastMechanisms, SSLSockets.version(this.socket)); + } final Collection bindFeatures = Bind2.features(inline); quickStartAvailable = sm @@ -1370,7 +1387,7 @@ public class XmppConnection implements Runnable { } } this.hashTokenRequest = hashTokenRequest; - authenticate = generateAuthenticationRequest(firstMessage, hashTokenRequest, bindFeatures, sm); + authenticate = generateAuthenticationRequest(firstMessage, usingFast, hashTokenRequest, bindFeatures, sm); } else { throw new AssertionError("Missing implementation for " + version); } @@ -1390,12 +1407,13 @@ public class XmppConnection implements Runnable { tagWriter.writeElement(authenticate); } - private Element generateAuthenticationRequest(final String firstMessage) { - return generateAuthenticationRequest(firstMessage, null, Bind2.QUICKSTART_FEATURES, true); + private Element generateAuthenticationRequest(final String firstMessage, final boolean usingFast) { + return generateAuthenticationRequest(firstMessage, usingFast, null, Bind2.QUICKSTART_FEATURES, true); } private Element generateAuthenticationRequest( final String firstMessage, + final boolean usingFast, final HashedToken.Mechanism hashedTokenRequest, final Collection bind, final boolean inlineStreamManagement) { @@ -1423,7 +1441,12 @@ public class XmppConnection implements Runnable { authenticate.addChild(resume); } if (hashedTokenRequest != null) { - authenticate.addChild("request-token", Namespace.FAST).setAttribute("mechanism", hashedTokenRequest.name()); + authenticate + .addChild("request-token", Namespace.FAST) + .setAttribute("mechanism", hashedTokenRequest.name()); + } + if (usingFast) { + authenticate.addChild("fast", Namespace.FAST); } return authenticate; } @@ -2059,25 +2082,26 @@ public class XmppConnection implements Runnable { private boolean establishStream(final SSLSockets.Version sslVersion) throws IOException, InterruptedException { - final SaslMechanism pinnedMechanism = - SaslMechanism.ensureAvailable(account.getPinnedMechanism(), sslVersion); + final SaslMechanism quickStartMechanism = + SaslMechanism.ensureAvailable(account.getQuickStartMechanism(), sslVersion); final boolean secureConnection = sslVersion != SSLSockets.Version.NONE; if (secureConnection && Config.QUICKSTART_ENABLED - && pinnedMechanism != null + && quickStartMechanism != null && account.isOptionSet(Account.OPTION_QUICKSTART_AVAILABLE)) { mXmppConnectionService.restoredFromDatabaseLatch.await(); - this.saslMechanism = pinnedMechanism; + this.saslMechanism = quickStartMechanism; + final boolean usingFast = quickStartMechanism instanceof HashedToken; final Element authenticate = - generateAuthenticationRequest(pinnedMechanism.getClientFirstMessage()); - authenticate.setAttribute("mechanism", pinnedMechanism.getMechanism()); + generateAuthenticationRequest(quickStartMechanism.getClientFirstMessage(), usingFast); + authenticate.setAttribute("mechanism", quickStartMechanism.getMechanism()); sendStartStream(true, false); tagWriter.writeElement(authenticate); Log.d( Config.LOGTAG, account.getJid().toString() + ": quick start with " - + pinnedMechanism.getMechanism()); + + quickStartMechanism.getMechanism()); return true; } else { sendStartStream(secureConnection, true); From e2b9f0e77ac25a9ffa1cb173f8e2bdf121f53695 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 15 Oct 2022 20:53:59 +0200 Subject: [PATCH 067/101] add support for HashedToken channel binding --- .../conversations/crypto/sasl/Anonymous.java | 4 +- .../crypto/sasl/ChannelBindingMechanism.java | 94 +++++++++++++++++++ .../conversations/crypto/sasl/External.java | 4 +- .../crypto/sasl/HashedToken.java | 25 ++++- .../conversations/crypto/sasl/Plain.java | 4 +- .../crypto/sasl/SaslMechanism.java | 15 ++- .../crypto/sasl/ScramMechanism.java | 2 +- .../crypto/sasl/ScramPlusMechanism.java | 89 +----------------- .../siacs/conversations/entities/Account.java | 7 +- .../conversations/xmpp/XmppConnection.java | 75 ++++++++------- 10 files changed, 187 insertions(+), 132 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/Anonymous.java b/src/main/java/eu/siacs/conversations/crypto/sasl/Anonymous.java index 22cf80e6546a33734c9c69fa43fc57638a1fe881..6fc4b11ceed745cec500ff0e62759359d49792ba 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/Anonymous.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/Anonymous.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.crypto.sasl; +import javax.net.ssl.SSLSocket; + import eu.siacs.conversations.entities.Account; public class Anonymous extends SaslMechanism { @@ -21,7 +23,7 @@ public class Anonymous extends SaslMechanism { } @Override - public String getClientFirstMessage() { + public String getClientFirstMessage(final SSLSocket sslSocket) { return ""; } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBindingMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBindingMechanism.java index d4e34ba59230320500fbb37bcba9b08084e212e0..b94210a60d5ead030bf78db758a6ad34b349111d 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBindingMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBindingMechanism.java @@ -1,6 +1,100 @@ package eu.siacs.conversations.crypto.sasl; +import org.bouncycastle.jcajce.provider.digest.SHA256; +import org.conscrypt.Conscrypt; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; + +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; + public interface ChannelBindingMechanism { + String EXPORTER_LABEL = "EXPORTER-Channel-Binding"; + ChannelBinding getChannelBinding(); + + static byte[] getChannelBindingData(final SSLSocket sslSocket, final ChannelBinding channelBinding) + throws SaslMechanism.AuthenticationException { + if (sslSocket == null) { + throw new SaslMechanism.AuthenticationException("Channel binding attempt on non secure socket"); + } + if (channelBinding == ChannelBinding.TLS_EXPORTER) { + final byte[] keyingMaterial; + try { + keyingMaterial = + Conscrypt.exportKeyingMaterial(sslSocket, EXPORTER_LABEL, new byte[0], 32); + } catch (final SSLException e) { + throw new SaslMechanism.AuthenticationException("Could not export keying material"); + } + if (keyingMaterial == null) { + throw new SaslMechanism.AuthenticationException( + "Could not export keying material. Socket not ready"); + } + return keyingMaterial; + } else if (channelBinding == ChannelBinding.TLS_UNIQUE) { + final byte[] unique = Conscrypt.getTlsUnique(sslSocket); + if (unique == null) { + throw new SaslMechanism.AuthenticationException( + "Could not retrieve tls unique. Socket not ready"); + } + return unique; + } else if (channelBinding == ChannelBinding.TLS_SERVER_END_POINT) { + return getServerEndPointChannelBinding(sslSocket.getSession()); + } else { + throw new SaslMechanism.AuthenticationException( + String.format("%s is not a valid channel binding", channelBinding)); + } + } + + static byte[] getServerEndPointChannelBinding(final SSLSession session) + throws SaslMechanism.AuthenticationException { + final Certificate[] certificates; + try { + certificates = session.getPeerCertificates(); + } catch (final SSLPeerUnverifiedException e) { + throw new SaslMechanism.AuthenticationException("Could not verify peer certificates"); + } + if (certificates == null || certificates.length == 0) { + throw new SaslMechanism.AuthenticationException("Could not retrieve peer certificate"); + } + final X509Certificate certificate; + if (certificates[0] instanceof X509Certificate) { + certificate = (X509Certificate) certificates[0]; + } else { + throw new SaslMechanism.AuthenticationException("Certificate was not X509"); + } + final String algorithm = certificate.getSigAlgName(); + final int withIndex = algorithm.indexOf("with"); + if (withIndex <= 0) { + throw new SaslMechanism.AuthenticationException("Unable to parse SigAlgName"); + } + final String hashAlgorithm = algorithm.substring(0, withIndex); + final MessageDigest messageDigest; + // https://www.rfc-editor.org/rfc/rfc5929#section-4.1 + if ("MD5".equalsIgnoreCase(hashAlgorithm) || "SHA1".equalsIgnoreCase(hashAlgorithm)) { + messageDigest = new SHA256.Digest(); + } else { + try { + messageDigest = MessageDigest.getInstance(hashAlgorithm); + } catch (final NoSuchAlgorithmException e) { + throw new SaslMechanism.AuthenticationException( + "Could not instantiate message digest for " + hashAlgorithm); + } + } + final byte[] encodedCertificate; + try { + encodedCertificate = certificate.getEncoded(); + } catch (final CertificateEncodingException e) { + throw new SaslMechanism.AuthenticationException("Could not encode certificate"); + } + messageDigest.update(encodedCertificate); + return messageDigest.digest(); + } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/External.java b/src/main/java/eu/siacs/conversations/crypto/sasl/External.java index 06323f039b57f1fde0539907ef9be1025eba9472..6aba413a5682c9a544797aac797bec6ee6fa35e3 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/External.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/External.java @@ -2,6 +2,8 @@ package eu.siacs.conversations.crypto.sasl; import android.util.Base64; +import javax.net.ssl.SSLSocket; + import eu.siacs.conversations.entities.Account; public class External extends SaslMechanism { @@ -23,7 +25,7 @@ public class External extends SaslMechanism { } @Override - public String getClientFirstMessage() { + public String getClientFirstMessage(final SSLSocket sslSocket) { return Base64.encodeToString( account.getJid().asBareJid().toEscapedString().getBytes(), Base64.NO_WRAP); } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/HashedToken.java b/src/main/java/eu/siacs/conversations/crypto/sasl/HashedToken.java index 1d8aeac69cb20091b657ccc295ba080007cf4c51..d3595b9e4ccb92c489ea4de69a40f83a5b6de0d7 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/HashedToken.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/HashedToken.java @@ -1,6 +1,7 @@ package eu.siacs.conversations.crypto.sasl; import android.util.Base64; +import android.util.Log; import com.google.common.base.MoreObjects; import com.google.common.base.Strings; @@ -18,6 +19,7 @@ import java.util.List; import javax.net.ssl.SSLSocket; +import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.utils.SSLSockets; @@ -42,10 +44,10 @@ public abstract class HashedToken extends SaslMechanism implements ChannelBindin } @Override - public String getClientFirstMessage() { + public String getClientFirstMessage(final SSLSocket sslSocket) { final String token = Strings.nullToEmpty(this.account.getFastToken()); final HashFunction hashing = getHashFunction(token.getBytes(StandardCharsets.UTF_8)); - final byte[] cbData = new byte[0]; + final byte[] cbData = getChannelBindingData(sslSocket); final byte[] initiatorHashedToken = hashing.hashBytes(Bytes.concat(INITIATOR, cbData)).asBytes(); final byte[] firstMessage = @@ -56,6 +58,23 @@ public abstract class HashedToken extends SaslMechanism implements ChannelBindin return Base64.encodeToString(firstMessage, Base64.NO_WRAP); } + private byte[] getChannelBindingData(final SSLSocket sslSocket) { + if (this.channelBinding == ChannelBinding.NONE) { + return new byte[0]; + } + try { + return ChannelBindingMechanism.getChannelBindingData(sslSocket, this.channelBinding); + } catch (final AuthenticationException e) { + Log.e( + Config.LOGTAG, + account.getJid().asBareJid() + + ": unable to retrieve channel binding data for " + + getMechanism(), + e); + return new byte[0]; + } + } + @Override public String getResponse(final String challenge, final SSLSocket socket) throws AuthenticationException { @@ -67,7 +86,7 @@ public abstract class HashedToken extends SaslMechanism implements ChannelBindin } final String token = Strings.nullToEmpty(this.account.getFastToken()); final HashFunction hashing = getHashFunction(token.getBytes(StandardCharsets.UTF_8)); - final byte[] cbData = new byte[0]; + final byte[] cbData = getChannelBindingData(socket); final byte[] expectedResponderMessage = hashing.hashBytes(Bytes.concat(RESPONDER, cbData)).asBytes(); if (Arrays.equals(responderMessage, expectedResponderMessage)) { diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java b/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java index 875538becc99e9a68f3e148ad902fe76c686f3f7..2be5d0bcb2a09482653b88df5ce4427adc6fe2ec 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java @@ -4,6 +4,8 @@ import android.util.Base64; import java.nio.charset.Charset; +import javax.net.ssl.SSLSocket; + import eu.siacs.conversations.entities.Account; public class Plain extends SaslMechanism { @@ -30,7 +32,7 @@ public class Plain extends SaslMechanism { } @Override - public String getClientFirstMessage() { + public String getClientFirstMessage(final SSLSocket sslSocket) { return getMessage(account.getUsername(), account.getPassword()); } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java index 48835f9df727356f63ebcbba99598ca20d932841..b8d1d04659015e4d37d37f0e61257e09bdbed814 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java @@ -44,7 +44,7 @@ public abstract class SaslMechanism { public abstract String getMechanism(); - public String getClientFirstMessage() { + public String getClientFirstMessage(final SSLSocket sslSocket) { return ""; } @@ -154,7 +154,12 @@ public abstract class SaslMechanism { public SaslMechanism of( final Collection mechanisms, final Collection bindings, + final Version version, final SSLSockets.Version sslVersion) { + final HashedToken fastMechanism = account.getFastMechanism(); + if (version == Version.SASL_2 && fastMechanism != null) { + return fastMechanism; + } final ChannelBinding channelBinding = ChannelBinding.best(bindings, sslVersion); return of(mechanisms, channelBinding); } @@ -180,4 +185,12 @@ public abstract class SaslMechanism { return mechanism; } } + + public static boolean hashedToken(final SaslMechanism saslMechanism) { + return saslMechanism instanceof HashedToken; + } + + public static boolean pin(final SaslMechanism saslMechanism) { + return !hashedToken(saslMechanism); + } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java index aba434e3a66d23f7bac64c8138b843dcbfb17a41..5825df29d68a909be53f0702be61b323a5798a13 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java @@ -112,7 +112,7 @@ abstract class ScramMechanism extends SaslMechanism { } @Override - public String getClientFirstMessage() { + public String getClientFirstMessage(final SSLSocket sslSocket) { if (clientFirstMessageBare.isEmpty() && state == State.INITIAL) { clientFirstMessageBare = "n=" diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java index c6a63ddbdaa329ab64f472b1290c52370fe56046..0c836933fada9dc7dfd796ed2188e0038c210a30 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java @@ -1,25 +1,11 @@ package eu.siacs.conversations.crypto.sasl; -import org.bouncycastle.jcajce.provider.digest.SHA256; -import org.conscrypt.Conscrypt; - -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.cert.Certificate; -import java.security.cert.CertificateEncodingException; -import java.security.cert.X509Certificate; - -import javax.net.ssl.SSLException; -import javax.net.ssl.SSLPeerUnverifiedException; -import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocket; import eu.siacs.conversations.entities.Account; public abstract class ScramPlusMechanism extends ScramMechanism implements ChannelBindingMechanism { - private static final String EXPORTER_LABEL = "EXPORTER-Channel-Binding"; - ScramPlusMechanism(Account account, ChannelBinding channelBinding) { super(account, channelBinding); } @@ -27,80 +13,7 @@ public abstract class ScramPlusMechanism extends ScramMechanism implements Chann @Override protected byte[] getChannelBindingData(final SSLSocket sslSocket) throws AuthenticationException { - if (sslSocket == null) { - throw new AuthenticationException("Channel binding attempt on non secure socket"); - } - if (this.channelBinding == ChannelBinding.TLS_EXPORTER) { - final byte[] keyingMaterial; - try { - keyingMaterial = - Conscrypt.exportKeyingMaterial(sslSocket, EXPORTER_LABEL, new byte[0], 32); - } catch (final SSLException e) { - throw new AuthenticationException("Could not export keying material"); - } - if (keyingMaterial == null) { - throw new AuthenticationException( - "Could not export keying material. Socket not ready"); - } - return keyingMaterial; - } else if (this.channelBinding == ChannelBinding.TLS_UNIQUE) { - final byte[] unique = Conscrypt.getTlsUnique(sslSocket); - if (unique == null) { - throw new AuthenticationException( - "Could not retrieve tls unique. Socket not ready"); - } - return unique; - } else if (this.channelBinding == ChannelBinding.TLS_SERVER_END_POINT) { - return getServerEndPointChannelBinding(sslSocket.getSession()); - } else { - throw new AuthenticationException( - String.format("%s is not a valid channel binding", channelBinding)); - } - } - - private byte[] getServerEndPointChannelBinding(final SSLSession session) - throws AuthenticationException { - final Certificate[] certificates; - try { - certificates = session.getPeerCertificates(); - } catch (final SSLPeerUnverifiedException e) { - throw new AuthenticationException("Could not verify peer certificates"); - } - if (certificates == null || certificates.length == 0) { - throw new AuthenticationException("Could not retrieve peer certificate"); - } - final X509Certificate certificate; - if (certificates[0] instanceof X509Certificate) { - certificate = (X509Certificate) certificates[0]; - } else { - throw new AuthenticationException("Certificate was not X509"); - } - final String algorithm = certificate.getSigAlgName(); - final int withIndex = algorithm.indexOf("with"); - if (withIndex <= 0) { - throw new AuthenticationException("Unable to parse SigAlgName"); - } - final String hashAlgorithm = algorithm.substring(0, withIndex); - final MessageDigest messageDigest; - // https://www.rfc-editor.org/rfc/rfc5929#section-4.1 - if ("MD5".equalsIgnoreCase(hashAlgorithm) || "SHA1".equalsIgnoreCase(hashAlgorithm)) { - messageDigest = new SHA256.Digest(); - } else { - try { - messageDigest = MessageDigest.getInstance(hashAlgorithm); - } catch (final NoSuchAlgorithmException e) { - throw new AuthenticationException( - "Could not instantiate message digest for " + hashAlgorithm); - } - } - final byte[] encodedCertificate; - try { - encodedCertificate = certificate.getEncoded(); - } catch (final CertificateEncodingException e) { - throw new AuthenticationException("Could not encode certificate"); - } - messageDigest.update(encodedCertificate); - return messageDigest.digest(); + return ChannelBindingMechanism.getChannelBindingData(sslSocket, this.channelBinding); } @Override diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index d570cbec355de9c13d603eeb6fceb63e6b5688b3..4457e4d1f0d290db271ad52b49a7b17768430288 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -26,6 +26,7 @@ import eu.siacs.conversations.crypto.PgpDecryptionService; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession; import eu.siacs.conversations.crypto.sasl.ChannelBinding; +import eu.siacs.conversations.crypto.sasl.ChannelBindingMechanism; import eu.siacs.conversations.crypto.sasl.HashedToken; import eu.siacs.conversations.crypto.sasl.HashedTokenSha256; import eu.siacs.conversations.crypto.sasl.HashedTokenSha512; @@ -348,9 +349,9 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable public void setPinnedMechanism(final SaslMechanism mechanism) { this.pinnedMechanism = mechanism.getMechanism(); - if (mechanism instanceof ScramPlusMechanism) { + if (mechanism instanceof ChannelBindingMechanism) { this.pinnedChannelBinding = - ((ScramPlusMechanism) mechanism).getChannelBinding().toString(); + ((ChannelBindingMechanism) mechanism).getChannelBinding().toString(); } else { this.pinnedChannelBinding = null; } @@ -386,7 +387,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable return new SaslMechanism.Factory(this).of(mechanism, channelBinding); } - private HashedToken getFastMechanism() { + public HashedToken getFastMechanism() { final HashedToken.Mechanism fastMechanism = HashedToken.Mechanism.ofOrNull(this.fastMechanism); final String token = this.fastToken; if (fastMechanism == null || Strings.isNullOrEmpty(token)) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 2c60534dd0faed2dda5c1d29949acad3f0fe0763..fc753ec34fbbe58155b71ea0712c9f8e90878194 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -14,6 +14,7 @@ import android.util.Pair; import android.util.SparseArray; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.google.common.base.Strings; @@ -704,7 +705,9 @@ public class XmppConnection implements Runnable { Log.d( Config.LOGTAG, account.getJid().asBareJid().toString() + ": logged in (using " + version + ")"); - account.setPinnedMechanism(saslMechanism); + if (SaslMechanism.pin(this.saslMechanism)) { + account.setPinnedMechanism(this.saslMechanism); + } if (version == SaslMechanism.Version.SASL_2) { final Tag tag = tagReader.readTag(); if (tag != null && tag.isStart("features", Namespace.STREAMS)) { @@ -837,6 +840,7 @@ public class XmppConnection implements Runnable { } Log.d(Config.LOGTAG,failure.toString()); Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": login failure " + version); + //TODO check if we are doing FAST; reset token if (failure.hasChild("temporary-auth-failure")) { throw new StateChangingException(Account.State.TEMPORARY_AUTH_FAILURE); } else if (failure.hasChild("account-disabled")) { @@ -1242,6 +1246,7 @@ public class XmppConnection implements Runnable { account.getJid().asBareJid() + ": quick start in progress. ignoring features: " + XmlHelper.printElementNames(this.streamFeatures)); + //TODO check if 'fast' is available but we are doing something else return; } Log.d(Config.LOGTAG,account.getJid().asBareJid()+": server lost support for SASL 2. quick start not possible"); @@ -1320,37 +1325,12 @@ public class XmppConnection implements Runnable { final Element cbElement = this.streamFeatures.findChild("sasl-channel-binding", Namespace.CHANNEL_BINDING); final Collection channelBindings = ChannelBinding.of(cbElement); - Log.d(Config.LOGTAG,"mechanisms: "+mechanisms); - Log.d(Config.LOGTAG, "channel bindings: " + channelBindings); final SaslMechanism.Factory factory = new SaslMechanism.Factory(account); - this.saslMechanism = factory.of(mechanisms, channelBindings, SSLSockets.version(this.socket)); - - //TODO externalize checks - - if (saslMechanism == null) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": unable to find supported SASL mechanism in " - + mechanisms); - throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); - } - final int pinnedMechanism = account.getPinnedMechanismPriority(); - if (pinnedMechanism > saslMechanism.getPriority()) { - Log.e( - Config.LOGTAG, - "Auth failed. Authentication mechanism " - + saslMechanism.getMechanism() - + " has lower priority (" - + saslMechanism.getPriority() - + ") than pinned priority (" - + pinnedMechanism - + "). Possible downgrade attack?"); - throw new StateChangingException(Account.State.DOWNGRADE_ATTACK); - } + final SaslMechanism saslMechanism = factory.of(mechanisms, channelBindings, version, SSLSockets.version(this.socket)); + this.saslMechanism = validate(saslMechanism, mechanisms); final boolean quickStartAvailable; - final String firstMessage = saslMechanism.getClientFirstMessage(); - final boolean usingFast = saslMechanism instanceof HashedToken; + final String firstMessage = this.saslMechanism.getClientFirstMessage(sslSocketOrNull(this.socket)); + final boolean usingFast = SaslMechanism.hashedToken(this.saslMechanism); final Element authenticate; if (version == SaslMechanism.Version.SASL) { authenticate = new Element("auth", Namespace.SASL); @@ -1402,11 +1382,40 @@ public class XmppConnection implements Runnable { + ": Authenticating with " + version + "/" - + saslMechanism.getMechanism()); - authenticate.setAttribute("mechanism", saslMechanism.getMechanism()); + + this.saslMechanism.getMechanism()); + authenticate.setAttribute("mechanism", this.saslMechanism.getMechanism()); tagWriter.writeElement(authenticate); } + @NonNull + private SaslMechanism validate(final @Nullable SaslMechanism saslMechanism, Collection mechanisms) throws StateChangingException { + if (saslMechanism == null) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": unable to find supported SASL mechanism in " + + mechanisms); + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); + } + if (SaslMechanism.hashedToken(saslMechanism)) { + return saslMechanism; + } + final int pinnedMechanism = account.getPinnedMechanismPriority(); + if (pinnedMechanism > saslMechanism.getPriority()) { + Log.e( + Config.LOGTAG, + "Auth failed. Authentication mechanism " + + saslMechanism.getMechanism() + + " has lower priority (" + + saslMechanism.getPriority() + + ") than pinned priority (" + + pinnedMechanism + + "). Possible downgrade attack?"); + throw new StateChangingException(Account.State.DOWNGRADE_ATTACK); + } + return saslMechanism; + } + private Element generateAuthenticationRequest(final String firstMessage, final boolean usingFast) { return generateAuthenticationRequest(firstMessage, usingFast, null, Bind2.QUICKSTART_FEATURES, true); } @@ -2093,7 +2102,7 @@ public class XmppConnection implements Runnable { this.saslMechanism = quickStartMechanism; final boolean usingFast = quickStartMechanism instanceof HashedToken; final Element authenticate = - generateAuthenticationRequest(quickStartMechanism.getClientFirstMessage(), usingFast); + generateAuthenticationRequest(quickStartMechanism.getClientFirstMessage(sslSocketOrNull(this.socket)), usingFast); authenticate.setAttribute("mechanism", quickStartMechanism.getMechanism()); sendStartStream(true, false); tagWriter.writeElement(authenticate); From a29c7c725ec64c5f5b20b8df75931db828b0ba8d Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 24 Oct 2022 13:11:30 +0200 Subject: [PATCH 068/101] modify scram mechanisms to use guava hashing --- .../crypto/sasl/ScramMechanism.java | 29 ++++++------------- .../conversations/crypto/sasl/ScramSha1.java | 13 ++++----- .../crypto/sasl/ScramSha1Plus.java | 13 ++++----- .../crypto/sasl/ScramSha256.java | 12 ++++---- .../crypto/sasl/ScramSha256Plus.java | 13 ++++----- .../crypto/sasl/ScramSha512.java | 11 ++++--- .../crypto/sasl/ScramSha512Plus.java | 13 ++++----- 7 files changed, 47 insertions(+), 57 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java index 5825df29d68a909be53f0702be61b323a5798a13..931debe017cc9a744cb2efa07a76f20c4945543f 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java @@ -1,15 +1,13 @@ package eu.siacs.conversations.crypto.sasl; import android.util.Base64; +import android.util.Log; import com.google.common.base.CaseFormat; import com.google.common.base.Objects; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; - -import org.bouncycastle.crypto.Digest; -import org.bouncycastle.crypto.macs.HMac; -import org.bouncycastle.crypto.params.KeyParameter; +import com.google.common.hash.HashFunction; import java.nio.charset.Charset; import java.security.InvalidKeyException; @@ -17,6 +15,7 @@ import java.util.concurrent.ExecutionException; import javax.net.ssl.SSLSocket; +import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.utils.CryptoHelper; @@ -54,14 +53,14 @@ abstract class ScramMechanism extends SaslMechanism { clientFirstMessageBare = ""; } - protected abstract HMac getHMAC(); + protected abstract HashFunction getHMac(final byte[] key); - protected abstract Digest getDigest(); + protected abstract HashFunction getDigest(); private KeyPair getKeyPair(final String password, final String salt, final int iterations) throws ExecutionException { return CACHE.get( - new CacheKey(getHMAC().getAlgorithmName(), password, salt, iterations), + new CacheKey(getMechanism(), password, salt, iterations), () -> { final byte[] saltedPassword, serverKey, clientKey; saltedPassword = @@ -76,21 +75,11 @@ abstract class ScramMechanism extends SaslMechanism { } private byte[] hmac(final byte[] key, final byte[] input) throws InvalidKeyException { - final HMac hMac = getHMAC(); - hMac.init(new KeyParameter(key)); - hMac.update(input, 0, input.length); - final byte[] out = new byte[hMac.getMacSize()]; - hMac.doFinal(out, 0); - return out; + return getHMac(key).hashBytes(input).asBytes(); } - public byte[] digest(final byte[] bytes) { - final Digest digest = getDigest(); - digest.reset(); - digest.update(bytes, 0, bytes.length); - final byte[] out = new byte[digest.getDigestSize()]; - digest.doFinal(out, 0); - return out; + private byte[] digest(final byte[] bytes) { + return getDigest().hashBytes(bytes).asBytes(); } /* diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java index 9bcc8ad47915b33911ade6831b0b1876e0972c7d..6f00c49eb948dd78599b0062fc5f5e1ac065d31a 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java @@ -1,8 +1,7 @@ package eu.siacs.conversations.crypto.sasl; -import org.bouncycastle.crypto.Digest; -import org.bouncycastle.crypto.digests.SHA1Digest; -import org.bouncycastle.crypto.macs.HMac; +import com.google.common.hash.HashFunction; +import com.google.common.hash.Hashing; import eu.siacs.conversations.entities.Account; @@ -15,13 +14,13 @@ public class ScramSha1 extends ScramMechanism { } @Override - protected HMac getHMAC() { - return new HMac(new SHA1Digest()); + protected HashFunction getHMac(final byte[] key) { + return Hashing.hmacSha1(key); } @Override - protected Digest getDigest() { - return new SHA1Digest(); + protected HashFunction getDigest() { + return Hashing.sha1(); } @Override diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1Plus.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1Plus.java index d4f2fcb0bc444912f129fea417d5e6106273749d..d353bd9ee5da623581920a97dbdaed20ebe174cc 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1Plus.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1Plus.java @@ -1,8 +1,7 @@ package eu.siacs.conversations.crypto.sasl; -import org.bouncycastle.crypto.Digest; -import org.bouncycastle.crypto.digests.SHA1Digest; -import org.bouncycastle.crypto.macs.HMac; +import com.google.common.hash.HashFunction; +import com.google.common.hash.Hashing; import eu.siacs.conversations.entities.Account; @@ -15,13 +14,13 @@ public class ScramSha1Plus extends ScramPlusMechanism { } @Override - protected HMac getHMAC() { - return new HMac(new SHA1Digest()); + protected HashFunction getHMac(final byte[] key) { + return Hashing.hmacSha1(key); } @Override - protected Digest getDigest() { - return new SHA1Digest(); + protected HashFunction getDigest() { + return Hashing.sha1(); } @Override diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256.java index 610ed788bbb10138c1f44b48657e1321e6d8c18f..9d7d62c365827bb810291e94ff4d7aaef6450dd6 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256.java @@ -1,5 +1,8 @@ package eu.siacs.conversations.crypto.sasl; +import com.google.common.hash.HashFunction; +import com.google.common.hash.Hashing; + import org.bouncycastle.crypto.Digest; import org.bouncycastle.crypto.digests.SHA256Digest; import org.bouncycastle.crypto.macs.HMac; @@ -15,15 +18,14 @@ public class ScramSha256 extends ScramMechanism { } @Override - protected HMac getHMAC() { - return new HMac(new SHA256Digest()); + protected HashFunction getHMac(final byte[] key) { + return Hashing.hmacSha256(key); } @Override - protected Digest getDigest() { - return new SHA256Digest(); + protected HashFunction getDigest() { + return Hashing.sha256(); } - @Override public int getPriority() { return 25; diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256Plus.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256Plus.java index f48a052abf50852160addc63e82fb8c156405b74..5f15e9bf18fd77a79df792837ca587591a6a13d7 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256Plus.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256Plus.java @@ -1,8 +1,7 @@ package eu.siacs.conversations.crypto.sasl; -import org.bouncycastle.crypto.Digest; -import org.bouncycastle.crypto.digests.SHA256Digest; -import org.bouncycastle.crypto.macs.HMac; +import com.google.common.hash.HashFunction; +import com.google.common.hash.Hashing; import eu.siacs.conversations.entities.Account; @@ -15,13 +14,13 @@ public class ScramSha256Plus extends ScramPlusMechanism { } @Override - protected HMac getHMAC() { - return new HMac(new SHA256Digest()); + protected HashFunction getHMac(final byte[] key) { + return Hashing.hmacSha256(key); } @Override - protected Digest getDigest() { - return new SHA256Digest(); + protected HashFunction getDigest() { + return Hashing.sha256(); } @Override diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512.java index 3d54b39e962cb48c2545680853a35ebef782a3c6..8194ac0ac226a2e475da6a52fc0fc9bf7b8548ea 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512.java @@ -1,5 +1,8 @@ package eu.siacs.conversations.crypto.sasl; +import com.google.common.hash.HashFunction; +import com.google.common.hash.Hashing; + import org.bouncycastle.crypto.Digest; import org.bouncycastle.crypto.digests.SHA512Digest; import org.bouncycastle.crypto.macs.HMac; @@ -15,13 +18,13 @@ public class ScramSha512 extends ScramMechanism { } @Override - protected HMac getHMAC() { - return new HMac(new SHA512Digest()); + protected HashFunction getHMac(final byte[] key) { + return Hashing.hmacSha512(key); } @Override - protected Digest getDigest() { - return new SHA512Digest(); + protected HashFunction getDigest() { + return Hashing.sha512(); } @Override diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512Plus.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512Plus.java index 8cec1f33f7906a86204eb812c27d8fc7feae63a3..610c87e2310477d13ebb786b2649c644e9903ef1 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512Plus.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512Plus.java @@ -1,8 +1,7 @@ package eu.siacs.conversations.crypto.sasl; -import org.bouncycastle.crypto.Digest; -import org.bouncycastle.crypto.digests.SHA512Digest; -import org.bouncycastle.crypto.macs.HMac; +import com.google.common.hash.HashFunction; +import com.google.common.hash.Hashing; import eu.siacs.conversations.entities.Account; @@ -15,13 +14,13 @@ public class ScramSha512Plus extends ScramPlusMechanism { } @Override - protected HMac getHMAC() { - return new HMac(new SHA512Digest()); + protected HashFunction getHMac(final byte[] key) { + return Hashing.hmacSha512(key); } @Override - protected Digest getDigest() { - return new SHA512Digest(); + protected HashFunction getDigest() { + return Hashing.sha512(); } @Override From 6e562d4cf906e50a793bb0a968028dc197051ebf Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 25 Oct 2022 12:44:51 +0200 Subject: [PATCH 069/101] Update README.md --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 5f62f6195469892c30eae3ea4622e747ba03e2ef..037e04106ab2da09b7b1622cec3d7e7984fc51ea 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,6 @@

Conversations: the very last word in instant messaging

- - chat on our conference room - build status From 35ee01cb285bc0c711aaedbdb057de86979cbc58 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 1 Nov 2022 16:44:05 +0100 Subject: [PATCH 070/101] reset fast token on login failure --- src/main/java/eu/siacs/conversations/entities/Account.java | 5 +++++ .../java/eu/siacs/conversations/xmpp/XmppConnection.java | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index 4457e4d1f0d290db271ad52b49a7b17768430288..7c5f22b27a4af400c41dc28d643ddacc414fc166 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -362,6 +362,11 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable this.fastToken = token; } + public void resetFastToken() { + this.fastMechanism = null; + this.fastToken = null; + } + public void resetPinnedMechanism() { this.pinnedMechanism = null; this.pinnedChannelBinding = null; diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index fc753ec34fbbe58155b71ea0712c9f8e90878194..1793c9f292861aacc69bbd1b8df9c1f49308e0c7 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -840,7 +840,11 @@ public class XmppConnection implements Runnable { } Log.d(Config.LOGTAG,failure.toString()); Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": login failure " + version); - //TODO check if we are doing FAST; reset token + if (this.saslMechanism instanceof HashedToken) { + Log.d(Config.LOGTAG,account.getJid().asBareJid() + ": resetting token"); + account.resetFastToken(); + mXmppConnectionService.databaseBackend.updateAccount(account); + } if (failure.hasChild("temporary-auth-failure")) { throw new StateChangingException(Account.State.TEMPORARY_AUTH_FAILURE); } else if (failure.hasChild("account-disabled")) { From 7e29d1d86258adc904017a8dd9b3965da3c1374e Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 1 Nov 2022 16:44:22 +0100 Subject: [PATCH 071/101] update gradle --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 27e620963716608769cfecb9169c2bfca27f0dae..6ccdcf7053c0605a78e27e234faace9f33997a40 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.2.2' + classpath 'com.android.tools.build:gradle:7.3.1' } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e639f29f374c17fdf67dee47e7c8e96c951a546b..d33b7f161ec081d83a49d753f2c744dc152612c0 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip From dac2e1713384a3458dcceef18ae8e3de1fd4abc4 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 1 Nov 2022 18:06:32 +0100 Subject: [PATCH 072/101] =?UTF-8?q?disable=20quick=20start=20if=20fast=20i?= =?UTF-8?q?s=20available=20but=20we=20didn=E2=80=99t=20use=20fast?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../conversations/xmpp/XmppConnection.java | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 1793c9f292861aacc69bbd1b8df9c1f49308e0c7..ce37315360055d7615f8909fc71dbfe40308a9c5 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -802,7 +802,7 @@ public class XmppConnection implements Runnable { } final HashedToken.Mechanism tokenMechanism; final SaslMechanism currentMechanism = this.saslMechanism; - if (currentMechanism instanceof HashedToken) { + if (SaslMechanism.hashedToken(currentMechanism)) { tokenMechanism = ((HashedToken) currentMechanism).getTokenMechanism(); } else if (this.hashTokenRequest != null) { tokenMechanism = this.hashTokenRequest; @@ -840,7 +840,7 @@ public class XmppConnection implements Runnable { } Log.d(Config.LOGTAG,failure.toString()); Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": login failure " + version); - if (this.saslMechanism instanceof HashedToken) { + if (SaslMechanism.hashedToken(this.saslMechanism)) { Log.d(Config.LOGTAG,account.getJid().asBareJid() + ": resetting token"); account.resetFastToken(); mXmppConnectionService.databaseBackend.updateAccount(account); @@ -1250,12 +1250,26 @@ public class XmppConnection implements Runnable { account.getJid().asBareJid() + ": quick start in progress. ignoring features: " + XmlHelper.printElementNames(this.streamFeatures)); - //TODO check if 'fast' is available but we are doing something else + if (SaslMechanism.hashedToken(this.saslMechanism)) { + return; + } + if (isFastTokenAvailable( + this.streamFeatures.findChild("authentication", Namespace.SASL_2))) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": fast token available; resetting quick start"); + account.setOption(Account.OPTION_QUICKSTART_AVAILABLE, false); + mXmppConnectionService.databaseBackend.updateAccount(account); + } return; } - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": server lost support for SASL 2. quick start not possible"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": server lost support for SASL 2. quick start not possible"); this.account.setOption(Account.OPTION_QUICKSTART_AVAILABLE, false); - mXmppConnectionService.updateAccount(account); + mXmppConnectionService.databaseBackend.updateAccount(account); throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } if (this.streamFeatures.hasChild("starttls", Namespace.TLS) @@ -1377,7 +1391,7 @@ public class XmppConnection implements Runnable { } if (account.setOption(Account.OPTION_QUICKSTART_AVAILABLE, quickStartAvailable)) { - mXmppConnectionService.updateAccount(account); + mXmppConnectionService.databaseBackend.updateAccount(account); } Log.d( @@ -1391,6 +1405,11 @@ public class XmppConnection implements Runnable { tagWriter.writeElement(authenticate); } + private static boolean isFastTokenAvailable(final Element authentication) { + final Element inline = authentication == null ? null : authentication.findChild("inline"); + return inline != null && inline.hasChild("fast", Namespace.FAST); + } + @NonNull private SaslMechanism validate(final @Nullable SaslMechanism saslMechanism, Collection mechanisms) throws StateChangingException { if (saslMechanism == null) { From 5dbd86155fa0eda09124f23db67a75a5ad99b542 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 10 Nov 2022 07:54:47 +0100 Subject: [PATCH 073/101] show help button only if Config.HELP is set --- src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java | 2 +- 1 file changed, 1 insertion(+), 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 302fbf81d710c777b19cea79d340567b52187afc..f9c7177a2d6a5d65c9c550aaf34ffd77403f187d 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -164,7 +164,7 @@ public class RtpSessionActivity extends XmppActivity getMenuInflater().inflate(R.menu.activity_rtp_session, menu); final MenuItem help = menu.findItem(R.id.action_help); final MenuItem gotoChat = menu.findItem(R.id.action_goto_chat); - help.setVisible(isHelpButtonVisible()); + help.setVisible(Config.HELP != null && isHelpButtonVisible()); gotoChat.setVisible(isSwitchToConversationVisible()); return super.onCreateOptionsMenu(menu); } From 6ececb4d2bda38e95c8085bb2a9e927a40ebf39d Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 12 Nov 2022 13:37:18 +0100 Subject: [PATCH 074/101] refactor webrtc video source + capture code --- .../xmpp/jingle/TrackWrapper.java | 31 + .../xmpp/jingle/VideoSourceWrapper.java | 181 +++++ .../xmpp/jingle/WebRTCWrapper.java | 635 +++++++++--------- 3 files changed, 518 insertions(+), 329 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/TrackWrapper.java create mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/VideoSourceWrapper.java diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/TrackWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/TrackWrapper.java new file mode 100644 index 0000000000000000000000000000000000000000..4e2952127497a59e6cd03b86ad7c0b6b9eb17d53 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/TrackWrapper.java @@ -0,0 +1,31 @@ +package eu.siacs.conversations.xmpp.jingle; + +import com.google.common.base.Optional; +import com.google.common.base.Preconditions; + +import org.webrtc.MediaStreamTrack; +import org.webrtc.PeerConnection; +import org.webrtc.RtpSender; + +class TrackWrapper { + private final T track; + private final RtpSender rtpSender; + + private TrackWrapper(final T track, final RtpSender rtpSender) { + Preconditions.checkNotNull(track); + Preconditions.checkNotNull(rtpSender); + this.track = track; + this.rtpSender = rtpSender; + } + + public static TrackWrapper addTrack( + final PeerConnection peerConnection, final T mediaStreamTrack) { + final RtpSender rtpSender = peerConnection.addTrack(mediaStreamTrack); + return new TrackWrapper<>(mediaStreamTrack, rtpSender); + } + + public static Optional get( + final TrackWrapper trackWrapper) { + return trackWrapper == null ? Optional.absent() : Optional.of(trackWrapper.track); + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/VideoSourceWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/VideoSourceWrapper.java new file mode 100644 index 0000000000000000000000000000000000000000..5e83f2ba9ef8ce9cadcae9df4460cf05430dc612 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/VideoSourceWrapper.java @@ -0,0 +1,181 @@ +package eu.siacs.conversations.xmpp.jingle; + +import android.content.Context; +import android.util.Log; + +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; + +import org.webrtc.Camera2Enumerator; +import org.webrtc.CameraEnumerationAndroid; +import org.webrtc.CameraEnumerator; +import org.webrtc.CameraVideoCapturer; +import org.webrtc.EglBase; +import org.webrtc.PeerConnectionFactory; +import org.webrtc.SurfaceTextureHelper; +import org.webrtc.VideoSource; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Set; + +import javax.annotation.Nullable; + +import eu.siacs.conversations.Config; + +class VideoSourceWrapper { + + private static final int CAPTURING_RESOLUTION = 1920; + private static final int CAPTURING_MAX_FRAME_RATE = 30; + + private final CameraVideoCapturer cameraVideoCapturer; + private final CameraEnumerationAndroid.CaptureFormat captureFormat; + private final Set availableCameras; + private boolean isFrontCamera = false; + private VideoSource videoSource; + + VideoSourceWrapper( + CameraVideoCapturer cameraVideoCapturer, + CameraEnumerationAndroid.CaptureFormat captureFormat, + Set cameras) { + this.cameraVideoCapturer = cameraVideoCapturer; + this.captureFormat = captureFormat; + this.availableCameras = cameras; + } + + private int getFrameRate() { + return Math.max( + captureFormat.framerate.min, + Math.min(CAPTURING_MAX_FRAME_RATE, captureFormat.framerate.max)); + } + + public void initialize( + final PeerConnectionFactory peerConnectionFactory, + final Context context, + final EglBase.Context eglBaseContext) { + final SurfaceTextureHelper surfaceTextureHelper = + SurfaceTextureHelper.create("webrtc", eglBaseContext); + this.videoSource = peerConnectionFactory.createVideoSource(false); + this.cameraVideoCapturer.initialize( + surfaceTextureHelper, context, this.videoSource.getCapturerObserver()); + } + + public VideoSource getVideoSource() { + final VideoSource videoSource = this.videoSource; + if (videoSource == null) { + throw new IllegalStateException("VideoSourceWrapper was not initialized"); + } + return videoSource; + } + + public void startCapture() { + final int frameRate = getFrameRate(); + Log.d( + Config.LOGTAG, + String.format( + "start capturing at %dx%d@%d", + captureFormat.width, captureFormat.height, frameRate)); + this.cameraVideoCapturer.startCapture(captureFormat.width, captureFormat.height, frameRate); + } + + public void stopCapture() throws InterruptedException { + this.cameraVideoCapturer.stopCapture(); + } + + public void dispose() { + this.cameraVideoCapturer.dispose(); + if (this.videoSource != null) { + this.videoSource.dispose(); + } + } + + public ListenableFuture switchCamera() { + final SettableFuture future = SettableFuture.create(); + this.cameraVideoCapturer.switchCamera( + new CameraVideoCapturer.CameraSwitchHandler() { + @Override + public void onCameraSwitchDone(final boolean isFrontCamera) { + VideoSourceWrapper.this.isFrontCamera = isFrontCamera; + future.set(isFrontCamera); + } + + @Override + public void onCameraSwitchError(final String message) { + future.setException( + new IllegalStateException( + String.format("Unable to switch camera %s", message))); + } + }); + return future; + } + + public boolean isFrontCamera() { + return this.isFrontCamera; + } + + public boolean isCameraSwitchable() { + return this.availableCameras.size() > 1; + } + + public static class Factory { + final Context context; + + public Factory(final Context context) { + this.context = context; + } + + public Optional create() { + final CameraEnumerator enumerator = new Camera2Enumerator(context); + final Set deviceNames = ImmutableSet.copyOf(enumerator.getDeviceNames()); + for (final String deviceName : deviceNames) { + if (isFrontFacing(enumerator, deviceName)) { + final VideoSourceWrapper videoSourceWrapper = + of(enumerator, deviceName, deviceNames); + if (videoSourceWrapper == null) { + return Optional.absent(); + } + videoSourceWrapper.isFrontCamera = true; + return Optional.of(videoSourceWrapper); + } + } + if (deviceNames.size() == 0) { + return Optional.absent(); + } else { + return Optional.fromNullable( + of(enumerator, Iterables.get(deviceNames, 0), deviceNames)); + } + } + + @Nullable + private VideoSourceWrapper of( + final CameraEnumerator enumerator, + final String deviceName, + final Set availableCameras) { + final CameraVideoCapturer capturer = enumerator.createCapturer(deviceName, null); + if (capturer == null) { + return null; + } + final ArrayList choices = + new ArrayList<>(enumerator.getSupportedFormats(deviceName)); + Collections.sort(choices, (a, b) -> b.width - a.width); + for (final CameraEnumerationAndroid.CaptureFormat captureFormat : choices) { + if (captureFormat.width <= CAPTURING_RESOLUTION) { + return new VideoSourceWrapper(capturer, captureFormat, availableCameras); + } + } + return null; + } + + private static boolean isFrontFacing( + final CameraEnumerator cameraEnumerator, final String deviceName) { + try { + return cameraEnumerator.isFrontFacing(deviceName); + } catch (final NullPointerException e) { + return false; + } + } + } +} 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 8cd65447b48547e6d77b93e39a5dd32c849083cd..f71799bdf5481857186a6a1a592eac1213f7acb2 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -9,7 +9,6 @@ import android.util.Log; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Iterables; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; @@ -17,10 +16,6 @@ import com.google.common.util.concurrent.SettableFuture; import org.webrtc.AudioSource; import org.webrtc.AudioTrack; -import org.webrtc.Camera2Enumerator; -import org.webrtc.CameraEnumerationAndroid; -import org.webrtc.CameraEnumerator; -import org.webrtc.CameraVideoCapturer; import org.webrtc.CandidatePairChangeEvent; import org.webrtc.DataChannel; import org.webrtc.DefaultVideoDecoderFactory; @@ -36,14 +31,10 @@ import org.webrtc.RtpReceiver; import org.webrtc.RtpTransceiver; import org.webrtc.SdpObserver; import org.webrtc.SessionDescription; -import org.webrtc.SurfaceTextureHelper; -import org.webrtc.VideoSource; import org.webrtc.VideoTrack; import org.webrtc.audio.JavaAudioDeviceModule; import org.webrtc.voiceengine.WebRtcAudioEffects; -import java.util.ArrayList; -import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Queue; @@ -59,140 +50,158 @@ import eu.siacs.conversations.Config; import eu.siacs.conversations.services.AppRTCAudioManager; import eu.siacs.conversations.services.XmppConnectionService; +@SuppressWarnings("UnstableApiUsage") public class WebRTCWrapper { private static final String EXTENDED_LOGGING_TAG = WebRTCWrapper.class.getSimpleName(); private final ExecutorService executorService = Executors.newSingleThreadExecutor(); - - private static final Set HARDWARE_AEC_BLACKLIST = new ImmutableSet.Builder() - .add("Pixel") - .add("Pixel XL") - .add("Moto G5") - .add("Moto G (5S) Plus") - .add("Moto G4") - .add("TA-1053") - .add("Mi A1") - .add("Mi A2") - .add("E5823") // Sony z5 compact - .add("Redmi Note 5") - .add("FP2") // Fairphone FP2 - .add("MI 5") - .add("GT-I9515") // Samsung Galaxy S4 Value Edition (jfvelte) - .add("GT-I9515L") // Samsung Galaxy S4 Value Edition (jfvelte) - .add("GT-I9505") // Samsung Galaxy S4 (jfltexx) - .build(); - - private static final int CAPTURING_RESOLUTION = 1920; - private static final int CAPTURING_MAX_FRAME_RATE = 30; + + private static final Set HARDWARE_AEC_BLACKLIST = + new ImmutableSet.Builder() + .add("Pixel") + .add("Pixel XL") + .add("Moto G5") + .add("Moto G (5S) Plus") + .add("Moto G4") + .add("TA-1053") + .add("Mi A1") + .add("Mi A2") + .add("E5823") // Sony z5 compact + .add("Redmi Note 5") + .add("FP2") // Fairphone FP2 + .add("MI 5") + .add("GT-I9515") // Samsung Galaxy S4 Value Edition (jfvelte) + .add("GT-I9515L") // Samsung Galaxy S4 Value Edition (jfvelte) + .add("GT-I9505") // Samsung Galaxy S4 (jfltexx) + .build(); private final EventCallback eventCallback; private final AtomicBoolean readyToReceivedIceCandidates = new AtomicBoolean(false); private final Queue iceCandidates = new LinkedList<>(); - private final AppRTCAudioManager.AudioManagerEvents audioManagerEvents = new AppRTCAudioManager.AudioManagerEvents() { - @Override - public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set availableAudioDevices) { - eventCallback.onAudioDeviceChanged(selectedAudioDevice, availableAudioDevices); - } - }; + private final AppRTCAudioManager.AudioManagerEvents audioManagerEvents = + new AppRTCAudioManager.AudioManagerEvents() { + @Override + public void onAudioDeviceChanged( + AppRTCAudioManager.AudioDevice selectedAudioDevice, + Set availableAudioDevices) { + eventCallback.onAudioDeviceChanged(selectedAudioDevice, availableAudioDevices); + } + }; private final Handler mainHandler = new Handler(Looper.getMainLooper()); - private VideoTrack localVideoTrack = null; + private TrackWrapper localAudioTrack = null; + private TrackWrapper localVideoTrack = null; private VideoTrack remoteVideoTrack = null; - private final PeerConnection.Observer peerConnectionObserver = new PeerConnection.Observer() { - @Override - public void onSignalingChange(PeerConnection.SignalingState signalingState) { - Log.d(EXTENDED_LOGGING_TAG, "onSignalingChange(" + signalingState + ")"); - //this is called after removeTrack or addTrack - //and should then trigger a content-add or content-remove or something - //https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/removeTrack - } - - @Override - public void onConnectionChange(final PeerConnection.PeerConnectionState newState) { - eventCallback.onConnectionChange(newState); - } - - @Override - public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) { - Log.d(EXTENDED_LOGGING_TAG, "onIceConnectionChange(" + iceConnectionState + ")"); - } - - @Override - public void onSelectedCandidatePairChanged(CandidatePairChangeEvent event) { - Log.d(Config.LOGTAG, "remote candidate selected: " + event.remote); - Log.d(Config.LOGTAG, "local candidate selected: " + event.local); - } - - @Override - public void onIceConnectionReceivingChange(boolean b) { + private final PeerConnection.Observer peerConnectionObserver = + new PeerConnection.Observer() { + @Override + public void onSignalingChange(PeerConnection.SignalingState signalingState) { + Log.d(EXTENDED_LOGGING_TAG, "onSignalingChange(" + signalingState + ")"); + // this is called after removeTrack or addTrack + // and should then trigger a content-add or content-remove or something + // https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/removeTrack + } - } + @Override + public void onConnectionChange(final PeerConnection.PeerConnectionState newState) { + eventCallback.onConnectionChange(newState); + } - @Override - public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) { - Log.d(EXTENDED_LOGGING_TAG, "onIceGatheringChange(" + iceGatheringState + ")"); - } + @Override + public void onIceConnectionChange( + PeerConnection.IceConnectionState iceConnectionState) { + Log.d( + EXTENDED_LOGGING_TAG, + "onIceConnectionChange(" + iceConnectionState + ")"); + } - @Override - public void onIceCandidate(IceCandidate iceCandidate) { - if (readyToReceivedIceCandidates.get()) { - eventCallback.onIceCandidate(iceCandidate); - } else { - iceCandidates.add(iceCandidate); - } - } + @Override + public void onSelectedCandidatePairChanged(CandidatePairChangeEvent event) { + Log.d(Config.LOGTAG, "remote candidate selected: " + event.remote); + Log.d(Config.LOGTAG, "local candidate selected: " + event.local); + } - @Override - public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) { + @Override + public void onIceConnectionReceivingChange(boolean b) {} - } + @Override + public void onIceGatheringChange( + PeerConnection.IceGatheringState iceGatheringState) { + Log.d(EXTENDED_LOGGING_TAG, "onIceGatheringChange(" + iceGatheringState + ")"); + } - @Override - public void onAddStream(MediaStream mediaStream) { - Log.d(EXTENDED_LOGGING_TAG, "onAddStream(numAudioTracks=" + mediaStream.audioTracks.size() + ",numVideoTracks=" + mediaStream.videoTracks.size() + ")"); - } + @Override + public void onIceCandidate(IceCandidate iceCandidate) { + if (readyToReceivedIceCandidates.get()) { + eventCallback.onIceCandidate(iceCandidate); + } else { + iceCandidates.add(iceCandidate); + } + } - @Override - public void onRemoveStream(MediaStream mediaStream) { + @Override + public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) {} - } + @Override + public void onAddStream(MediaStream mediaStream) { + Log.d( + EXTENDED_LOGGING_TAG, + "onAddStream(numAudioTracks=" + + mediaStream.audioTracks.size() + + ",numVideoTracks=" + + mediaStream.videoTracks.size() + + ")"); + } - @Override - public void onDataChannel(DataChannel dataChannel) { + @Override + public void onRemoveStream(MediaStream mediaStream) {} - } + @Override + public void onDataChannel(DataChannel dataChannel) {} - @Override - public void onRenegotiationNeeded() { - Log.d(EXTENDED_LOGGING_TAG, "onRenegotiationNeeded()"); - final PeerConnection.PeerConnectionState currentState = peerConnection == null ? null : peerConnection.connectionState(); - if (currentState != null && currentState != PeerConnection.PeerConnectionState.NEW) { - eventCallback.onRenegotiationNeeded(); - } - } + @Override + public void onRenegotiationNeeded() { + Log.d(EXTENDED_LOGGING_TAG, "onRenegotiationNeeded()"); + final PeerConnection.PeerConnectionState currentState = + peerConnection == null ? null : peerConnection.connectionState(); + if (currentState != null + && currentState != PeerConnection.PeerConnectionState.NEW) { + eventCallback.onRenegotiationNeeded(); + } + } - @Override - public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) { - final MediaStreamTrack track = rtpReceiver.track(); - Log.d(EXTENDED_LOGGING_TAG, "onAddTrack(kind=" + (track == null ? "null" : track.kind()) + ",numMediaStreams=" + mediaStreams.length + ")"); - if (track instanceof VideoTrack) { - remoteVideoTrack = (VideoTrack) track; - } - } + @Override + public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) { + final MediaStreamTrack track = rtpReceiver.track(); + Log.d( + EXTENDED_LOGGING_TAG, + "onAddTrack(kind=" + + (track == null ? "null" : track.kind()) + + ",numMediaStreams=" + + mediaStreams.length + + ")"); + if (track instanceof VideoTrack) { + remoteVideoTrack = (VideoTrack) track; + } + } - @Override - public void onTrack(RtpTransceiver transceiver) { - Log.d(EXTENDED_LOGGING_TAG, "onTrack(mid=" + transceiver.getMid() + ",media=" + transceiver.getMediaType() + ")"); - } - }; - @Nullable - private PeerConnection peerConnection = null; - private AudioTrack localAudioTrack = null; + @Override + public void onTrack(RtpTransceiver transceiver) { + Log.d( + EXTENDED_LOGGING_TAG, + "onTrack(mid=" + + transceiver.getMid() + + ",media=" + + transceiver.getMediaType() + + ")"); + } + }; + @Nullable private PeerConnection peerConnection = null; private AppRTCAudioManager appRTCAudioManager = null; private ToneManager toneManager = null; private Context context = null; private EglBase eglBase = null; - private CapturerChoice capturerChoice; + private VideoSourceWrapper videoSourceWrapper; WebRTCWrapper(final EventCallback eventCallback) { this.eventCallback = eventCallback; @@ -206,37 +215,15 @@ public class WebRTCWrapper { } } - @Nullable - private static CapturerChoice of(CameraEnumerator enumerator, final String deviceName, Set availableCameras) { - final CameraVideoCapturer capturer = enumerator.createCapturer(deviceName, null); - if (capturer == null) { - return null; - } - final ArrayList choices = new ArrayList<>(enumerator.getSupportedFormats(deviceName)); - Collections.sort(choices, (a, b) -> b.width - a.width); - for (final CameraEnumerationAndroid.CaptureFormat captureFormat : choices) { - if (captureFormat.width <= CAPTURING_RESOLUTION) { - return new CapturerChoice(capturer, captureFormat, availableCameras); - } - } - return null; - } - - private static boolean isFrontFacing(final CameraEnumerator cameraEnumerator, final String deviceName) { - try { - return cameraEnumerator.isFrontFacing(deviceName); - } catch (final NullPointerException e) { - return false; - } - } - - public void setup(final XmppConnectionService service, final AppRTCAudioManager.SpeakerPhonePreference speakerPhonePreference) throws InitializationException { + public void setup( + final XmppConnectionService service, + final AppRTCAudioManager.SpeakerPhonePreference speakerPhonePreference) + throws InitializationException { try { PeerConnectionFactory.initialize( PeerConnectionFactory.InitializationOptions.builder(service) - .setFieldTrials("WebRTC-BindUsingInterfaceName/Enabled/") - .createInitializationOptions() - ); + .setFieldTrials("WebRTC-BindUsingInterfaceName/Enabled/") + .createInitializationOptions()); } catch (final UnsatisfiedLinkError e) { throw new InitializationException("Unable to initialize PeerConnectionFactory", e); } @@ -247,68 +234,93 @@ public class WebRTCWrapper { } this.context = service; this.toneManager = service.getJingleConnectionManager().toneManager; - mainHandler.post(() -> { - appRTCAudioManager = AppRTCAudioManager.create(service, speakerPhonePreference); - toneManager.setAppRtcAudioManagerHasControl(true); - appRTCAudioManager.start(audioManagerEvents); - eventCallback.onAudioDeviceChanged(appRTCAudioManager.getSelectedAudioDevice(), appRTCAudioManager.getAudioDevices()); - }); - } - - synchronized void initializePeerConnection(final Set media, final List iceServers) throws InitializationException { + mainHandler.post( + () -> { + appRTCAudioManager = AppRTCAudioManager.create(service, speakerPhonePreference); + toneManager.setAppRtcAudioManagerHasControl(true); + appRTCAudioManager.start(audioManagerEvents); + eventCallback.onAudioDeviceChanged( + appRTCAudioManager.getSelectedAudioDevice(), + appRTCAudioManager.getAudioDevices()); + }); + } + + 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"); - final boolean setUseHardwareAcousticEchoCanceler = WebRtcAudioEffects.canUseAcousticEchoCanceler() && !HARDWARE_AEC_BLACKLIST.contains(Build.MODEL); - Log.d(Config.LOGTAG, String.format("setUseHardwareAcousticEchoCanceler(%s) model=%s", setUseHardwareAcousticEchoCanceler, Build.MODEL)); - PeerConnectionFactory peerConnectionFactory = PeerConnectionFactory.builder() - .setVideoDecoderFactory(new DefaultVideoDecoderFactory(eglBase.getEglBaseContext())) - .setVideoEncoderFactory(new DefaultVideoEncoderFactory(eglBase.getEglBaseContext(), true, true)) - .setAudioDeviceModule(JavaAudioDeviceModule.builder(context) - .setUseHardwareAcousticEchoCanceler(setUseHardwareAcousticEchoCanceler) - .createAudioDeviceModule() - ) - .createPeerConnectionFactory(); - + Preconditions.checkArgument( + media.size() > 0, "media can not be empty when initializing peer connection"); + final boolean setUseHardwareAcousticEchoCanceler = + WebRtcAudioEffects.canUseAcousticEchoCanceler() + && !HARDWARE_AEC_BLACKLIST.contains(Build.MODEL); + Log.d( + Config.LOGTAG, + String.format( + "setUseHardwareAcousticEchoCanceler(%s) model=%s", + setUseHardwareAcousticEchoCanceler, Build.MODEL)); + PeerConnectionFactory peerConnectionFactory = + PeerConnectionFactory.builder() + .setVideoDecoderFactory( + new DefaultVideoDecoderFactory(eglBase.getEglBaseContext())) + .setVideoEncoderFactory( + new DefaultVideoEncoderFactory( + eglBase.getEglBaseContext(), true, true)) + .setAudioDeviceModule( + JavaAudioDeviceModule.builder(context) + .setUseHardwareAcousticEchoCanceler( + setUseHardwareAcousticEchoCanceler) + .createAudioDeviceModule()) + .createPeerConnectionFactory(); final PeerConnection.RTCConfiguration rtcConfig = buildConfiguration(iceServers); - final PeerConnection peerConnection = peerConnectionFactory.createPeerConnection(rtcConfig, peerConnectionObserver); + final PeerConnection peerConnection = + peerConnectionFactory.createPeerConnection(rtcConfig, peerConnectionObserver); if (peerConnection == null) { throw new InitializationException("Unable to create PeerConnection"); } - final Optional optionalCapturerChoice = media.contains(Media.VIDEO) ? getVideoCapturer() : Optional.absent(); + final Optional optionalVideoSourceWrapper = + media.contains(Media.VIDEO) + ? new VideoSourceWrapper.Factory(requireContext()).create() + : Optional.absent(); - if (optionalCapturerChoice.isPresent()) { - this.capturerChoice = optionalCapturerChoice.get(); - final CameraVideoCapturer capturer = this.capturerChoice.cameraVideoCapturer; - final VideoSource videoSource = peerConnectionFactory.createVideoSource(false); - SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create("webrtc", eglBase.getEglBaseContext()); - capturer.initialize(surfaceTextureHelper, requireContext(), videoSource.getCapturerObserver()); - Log.d(Config.LOGTAG, String.format("start capturing at %dx%d@%d", capturerChoice.captureFormat.width, capturerChoice.captureFormat.height, capturerChoice.getFrameRate())); - capturer.startCapture(capturerChoice.captureFormat.width, capturerChoice.captureFormat.height, capturerChoice.getFrameRate()); + if (optionalVideoSourceWrapper.isPresent()) { + this.videoSourceWrapper = optionalVideoSourceWrapper.get(); + this.videoSourceWrapper.initialize( + peerConnectionFactory, context, eglBase.getEglBaseContext()); + this.videoSourceWrapper.startCapture(); - this.localVideoTrack = peerConnectionFactory.createVideoTrack("my-video-track", videoSource); + final VideoTrack videoTrack = + peerConnectionFactory.createVideoTrack( + "my-video-track", this.videoSourceWrapper.getVideoSource()); - peerConnection.addTrack(this.localVideoTrack); + this.localVideoTrack = TrackWrapper.addTrack(peerConnection, videoTrack); } - if (media.contains(Media.AUDIO)) { - //set up audio track - final AudioSource audioSource = peerConnectionFactory.createAudioSource(new MediaConstraints()); - this.localAudioTrack = peerConnectionFactory.createAudioTrack("my-audio-track", audioSource); - peerConnection.addTrack(this.localAudioTrack); + // set up audio track + final AudioSource audioSource = + peerConnectionFactory.createAudioSource(new MediaConstraints()); + final AudioTrack audioTrack = + peerConnectionFactory.createAudioTrack("my-audio-track", audioSource); + this.localAudioTrack = TrackWrapper.addTrack(peerConnection, audioTrack); } peerConnection.setAudioPlayout(true); peerConnection.setAudioRecording(true); + this.peerConnection = peerConnection; } - private static PeerConnection.RTCConfiguration buildConfiguration(final List iceServers) { - final PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(iceServers); - rtcConfig.tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.DISABLED; //XEP-0176 doesn't support tcp - rtcConfig.continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY; + private static PeerConnection.RTCConfiguration buildConfiguration( + final List iceServers) { + final PeerConnection.RTCConfiguration rtcConfig = + new PeerConnection.RTCConfiguration(iceServers); + rtcConfig.tcpCandidatePolicy = + PeerConnection.TcpCandidatePolicy.DISABLED; // XEP-0176 doesn't support tcp + rtcConfig.continualGatheringPolicy = + PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY; rtcConfig.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN; rtcConfig.rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.NEGOTIATE; rtcConfig.enableImplicitRollback = true; @@ -332,7 +344,7 @@ public class WebRTCWrapper { synchronized void close() { final PeerConnection peerConnection = this.peerConnection; - final CapturerChoice capturerChoice = this.capturerChoice; + final VideoSourceWrapper videoSourceWrapper = this.videoSourceWrapper; final AppRTCAudioManager audioManager = this.appRTCAudioManager; final EglBase eglBase = this.eglBase; if (peerConnection != null) { @@ -345,12 +357,13 @@ public class WebRTCWrapper { } this.localVideoTrack = null; this.remoteVideoTrack = null; - if (capturerChoice != null) { + if (videoSourceWrapper != null) { try { - capturerChoice.cameraVideoCapturer.stopCapture(); - } catch (InterruptedException e) { + videoSourceWrapper.stopCapture(); + } catch (final InterruptedException e) { Log.e(Config.LOGTAG, "unable to stop capturing"); } + // TODO call dispose } if (eglBase != null) { eglBase.release(); @@ -363,132 +376,148 @@ public class WebRTCWrapper { || this.eglBase != null || this.localVideoTrack != null || this.remoteVideoTrack != null) { - final IllegalStateException e = new IllegalStateException("WebRTCWrapper hasn't been closed properly"); + final IllegalStateException e = + new IllegalStateException("WebRTCWrapper hasn't been closed properly"); Log.e(Config.LOGTAG, "verifyClosed() failed. Going to throw", e); throw e; } } boolean isCameraSwitchable() { - final CapturerChoice capturerChoice = this.capturerChoice; - return capturerChoice != null && capturerChoice.availableCameras.size() > 1; + final VideoSourceWrapper videoSourceWrapper = this.videoSourceWrapper; + return videoSourceWrapper != null && videoSourceWrapper.isCameraSwitchable(); } boolean isFrontCamera() { - final CapturerChoice capturerChoice = this.capturerChoice; - return capturerChoice == null || capturerChoice.isFrontCamera; + final VideoSourceWrapper videoSourceWrapper = this.videoSourceWrapper; + return videoSourceWrapper == null || videoSourceWrapper.isFrontCamera(); } ListenableFuture switchCamera() { - final CapturerChoice capturerChoice = this.capturerChoice; - if (capturerChoice == null) { - return Futures.immediateFailedFuture(new IllegalStateException("CameraCapturer has not been initialized")); + final VideoSourceWrapper videoSourceWrapper = this.videoSourceWrapper; + if (videoSourceWrapper == null) { + return Futures.immediateFailedFuture( + new IllegalStateException("VideoSourceWrapper has not been initialized")); } - final SettableFuture future = SettableFuture.create(); - capturerChoice.cameraVideoCapturer.switchCamera(new CameraVideoCapturer.CameraSwitchHandler() { - @Override - public void onCameraSwitchDone(boolean isFrontCamera) { - capturerChoice.isFrontCamera = isFrontCamera; - future.set(isFrontCamera); - } - - @Override - public void onCameraSwitchError(final String message) { - future.setException(new IllegalStateException(String.format("Unable to switch camera %s", message))); - } - }); - return future; + return videoSourceWrapper.switchCamera(); } boolean isMicrophoneEnabled() { - final AudioTrack audioTrack = this.localAudioTrack; - if (audioTrack == null) { + final Optional audioTrack = TrackWrapper.get(this.localAudioTrack); + if (audioTrack.isPresent()) { + try { + return audioTrack.get().enabled(); + } catch (final IllegalStateException e) { + // sometimes UI might still be rendering the buttons when a background thread has + // already ended the call + return false; + } + } else { throw new IllegalStateException("Local audio track does not exist (yet)"); } - try { - return audioTrack.enabled(); - } catch (final IllegalStateException e) { - //sometimes UI might still be rendering the buttons when a background thread has already ended the call - return false; - } } boolean setMicrophoneEnabled(final boolean enabled) { - final AudioTrack audioTrack = this.localAudioTrack; - if (audioTrack == null) { + final Optional audioTrack = TrackWrapper.get(this.localAudioTrack); + if (audioTrack.isPresent()) { + try { + audioTrack.get().setEnabled(enabled); + return true; + } catch (final IllegalStateException e) { + Log.d(Config.LOGTAG, "unable to toggle microphone", e); + // ignoring race condition in case MediaStreamTrack has been disposed + return false; + } + } else { throw new IllegalStateException("Local audio track does not exist (yet)"); } - try { - audioTrack.setEnabled(enabled); - return true; - } catch (final IllegalStateException e) { - Log.d(Config.LOGTAG, "unable to toggle microphone", e); - //ignoring race condition in case MediaStreamTrack has been disposed - return false; - } } boolean isVideoEnabled() { - final VideoTrack videoTrack = this.localVideoTrack; - if (videoTrack == null) { - return false; + final Optional videoTrack = TrackWrapper.get(this.localVideoTrack); + if (videoTrack.isPresent()) { + return videoTrack.get().enabled(); } - return videoTrack.enabled(); + return false; } void setVideoEnabled(final boolean enabled) { - final VideoTrack videoTrack = this.localVideoTrack; - if (videoTrack == null) { - throw new IllegalStateException("Local video track does not exist"); + final Optional videoTrack = TrackWrapper.get(this.localVideoTrack); + if (videoTrack.isPresent()) { + videoTrack.get().setEnabled(enabled); + return; } - videoTrack.setEnabled(enabled); + throw new IllegalStateException("Local video track does not exist"); } synchronized ListenableFuture setLocalDescription() { - return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> { - final SettableFuture future = SettableFuture.create(); - peerConnection.setLocalDescription(new SetSdpObserver() { - @Override - public void onSetSuccess() { - final SessionDescription description = peerConnection.getLocalDescription(); - Log.d(EXTENDED_LOGGING_TAG, "set local description:"); - logDescription(description); - future.set(description); - } - - @Override - public void onSetFailure(final String message) { - future.setException(new FailureToSetDescriptionException(message)); - } - }); - return future; - }, MoreExecutors.directExecutor()); + return Futures.transformAsync( + getPeerConnectionFuture(), + peerConnection -> { + if (peerConnection == null) { + return Futures.immediateFailedFuture( + new IllegalStateException("PeerConnection was null")); + } + final SettableFuture future = SettableFuture.create(); + peerConnection.setLocalDescription( + new SetSdpObserver() { + @Override + public void onSetSuccess() { + final SessionDescription description = + peerConnection.getLocalDescription(); + Log.d(EXTENDED_LOGGING_TAG, "set local description:"); + logDescription(description); + future.set(description); + } + + @Override + public void onSetFailure(final String message) { + future.setException( + new FailureToSetDescriptionException(message)); + } + }); + return future; + }, + MoreExecutors.directExecutor()); } private static void logDescription(final SessionDescription sessionDescription) { - for (final String line : sessionDescription.description.split(eu.siacs.conversations.xmpp.jingle.SessionDescription.LINE_DIVIDER)) { + for (final String line : + sessionDescription.description.split( + eu.siacs.conversations.xmpp.jingle.SessionDescription.LINE_DIVIDER)) { Log.d(EXTENDED_LOGGING_TAG, line); } } - synchronized ListenableFuture setRemoteDescription(final SessionDescription sessionDescription) { + synchronized ListenableFuture setRemoteDescription( + final SessionDescription sessionDescription) { Log.d(EXTENDED_LOGGING_TAG, "setting remote description:"); logDescription(sessionDescription); - return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> { - final SettableFuture future = SettableFuture.create(); - peerConnection.setRemoteDescription(new SetSdpObserver() { - @Override - public void onSetSuccess() { - future.set(null); - } - - @Override - public void onSetFailure(final String message) { - future.setException(new FailureToSetDescriptionException(message)); - } - }, sessionDescription); - return future; - }, MoreExecutors.directExecutor()); + return Futures.transformAsync( + getPeerConnectionFuture(), + peerConnection -> { + if (peerConnection == null) { + return Futures.immediateFailedFuture( + new IllegalStateException("PeerConnection was null")); + } + final SettableFuture future = SettableFuture.create(); + peerConnection.setRemoteDescription( + new SetSdpObserver() { + @Override + public void onSetSuccess() { + future.set(null); + } + + @Override + public void onSetFailure(final String message) { + future.setException( + new FailureToSetDescriptionException(message)); + } + }, + sessionDescription); + return future; + }, + MoreExecutors.directExecutor()); } @Nonnull @@ -513,26 +542,6 @@ public class WebRTCWrapper { requirePeerConnection().addIceCandidate(iceCandidate); } - private Optional getVideoCapturer() { - final CameraEnumerator enumerator = new Camera2Enumerator(requireContext()); - final Set deviceNames = ImmutableSet.copyOf(enumerator.getDeviceNames()); - for (final String deviceName : deviceNames) { - if (isFrontFacing(enumerator, deviceName)) { - final CapturerChoice capturerChoice = of(enumerator, deviceName, deviceNames); - if (capturerChoice == null) { - return Optional.absent(); - } - capturerChoice.isFrontCamera = true; - return Optional.of(capturerChoice); - } - } - if (deviceNames.size() == 0) { - return Optional.absent(); - } else { - return Optional.fromNullable(of(enumerator, Iterables.get(deviceNames, 0), deviceNames)); - } - } - PeerConnection.PeerConnectionState getState() { return requirePeerConnection().connectionState(); } @@ -541,13 +550,12 @@ public class WebRTCWrapper { return requirePeerConnection().signalingState(); } - EglBase.Context getEglBaseContext() { return this.eglBase.getEglBaseContext(); } Optional getLocalVideoTrack() { - return Optional.fromNullable(this.localVideoTrack); + return TrackWrapper.get(this.localVideoTrack); } Optional getRemoteVideoTrack() { @@ -575,12 +583,14 @@ public class WebRTCWrapper { void onConnectionChange(PeerConnection.PeerConnectionState newState); - void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set availableAudioDevices); + void onAudioDeviceChanged( + AppRTCAudioManager.AudioDevice selectedAudioDevice, + Set availableAudioDevices); void onRenegotiationNeeded(); } - private static abstract class SetSdpObserver implements SdpObserver { + private abstract static class SetSdpObserver implements SdpObserver { @Override public void onCreateSuccess(org.webrtc.SessionDescription sessionDescription) { @@ -591,22 +601,6 @@ public class WebRTCWrapper { public void onCreateFailure(String s) { throw new IllegalStateException("Not able to use SetSdpObserver"); } - - } - - private static abstract class CreateSdpObserver implements SdpObserver { - - - @Override - public void onSetSuccess() { - throw new IllegalStateException("Not able to use CreateSdpObserver"); - } - - - @Override - public void onSetFailure(String s) { - throw new IllegalStateException("Not able to use CreateSdpObserver"); - } } static class InitializationException extends Exception { @@ -625,7 +619,6 @@ public class WebRTCWrapper { private PeerConnectionNotInitialized() { super("initialize PeerConnection first"); } - } private static class FailureToSetDescriptionException extends IllegalArgumentException { @@ -634,20 +627,4 @@ public class WebRTCWrapper { } } - private static class CapturerChoice { - private final CameraVideoCapturer cameraVideoCapturer; - private final CameraEnumerationAndroid.CaptureFormat captureFormat; - private final Set availableCameras; - private boolean isFrontCamera = false; - - CapturerChoice(CameraVideoCapturer cameraVideoCapturer, CameraEnumerationAndroid.CaptureFormat captureFormat, Set cameras) { - this.cameraVideoCapturer = cameraVideoCapturer; - this.captureFormat = captureFormat; - this.availableCameras = cameras; - } - - int getFrameRate() { - return Math.max(captureFormat.framerate.min, Math.min(CAPTURING_MAX_FRAME_RATE, captureFormat.framerate.max)); - } - } } From 44bfff7e490496d02c3c22a7400848b6679f614e Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 16 Nov 2022 11:00:43 +0100 Subject: [PATCH 075/101] fall back to regular authentication if fast fails --- .../conversations/xmpp/XmppConnection.java | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index ce37315360055d7615f8909fc71dbfe40308a9c5..01c7cb3b39d2e1569d87a25b75557f7b8310f9f4 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -831,17 +831,17 @@ public class XmppConnection implements Runnable { } } - private void processFailure(final Element failure) throws StateChangingException { + private void processFailure(final Element failure) throws IOException { final SaslMechanism.Version version; try { version = SaslMechanism.Version.of(failure); } catch (final IllegalArgumentException e) { throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } - Log.d(Config.LOGTAG,failure.toString()); + Log.d(Config.LOGTAG, failure.toString()); Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": login failure " + version); if (SaslMechanism.hashedToken(this.saslMechanism)) { - Log.d(Config.LOGTAG,account.getJid().asBareJid() + ": resetting token"); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": resetting token"); account.resetFastToken(); mXmppConnectionService.databaseBackend.updateAccount(account); } @@ -866,7 +866,15 @@ public class XmppConnection implements Runnable { } } } - throw new StateChangingException(Account.State.UNAUTHORIZED); + if (SaslMechanism.hashedToken(this.saslMechanism)) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": fast authentication failed. falling back to regular authentication"); + authenticate(); + } else { + throw new StateChangingException(Account.State.UNAUTHORIZED); + } } private static SSLSocket sslSocketOrNull(final Socket socket) { @@ -1332,6 +1340,17 @@ public class XmppConnection implements Runnable { } } + private void authenticate() throws IOException { + final boolean isSecure = + features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS || account.isOnion(); + if (isSecure && this.streamFeatures.hasChild("authentication", Namespace.SASL_2)) {authenticate(SaslMechanism.Version.SASL_2); + } else if (isSecure && this.streamFeatures.hasChild("mechanisms", Namespace.SASL)) { + authenticate(SaslMechanism.Version.SASL); + } else { + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); + } + } + private void authenticate(final SaslMechanism.Version version) throws IOException { final Element authElement; if (version == SaslMechanism.Version.SASL) { From 29461edf40d4f5f40b7e587fd0d7020098060f22 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 17 Nov 2022 07:48:09 +0100 Subject: [PATCH 076/101] process challenge only on secure connection --- .../conversations/xmpp/XmppConnection.java | 79 ++++++++++++------- 1 file changed, 51 insertions(+), 28 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 01c7cb3b39d2e1569d87a25b75557f7b8310f9f4..823e0747b80138585cfbeb713ae1557de5fb7b3a 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -577,8 +577,16 @@ public class XmppConnection implements Runnable { // two step sasl2 - we don’t support this yet throw new StateChangingException(Account.State.INCOMPATIBLE_CLIENT); } else if (nextTag.isStart("challenge")) { - final Element challenge = tagReader.readElement(nextTag); - processChallenge(challenge); + if (isSecure() && this.saslMechanism != null) { + final Element challenge = tagReader.readElement(nextTag); + processChallenge(challenge); + } else { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": received 'challenge on an unsecure connection"); + throw new StateChangingException(Account.State.INCOMPATIBLE_CLIENT); + } } else if (nextTag.isStart("enabled", Namespace.STREAM_MANAGEMENT)) { final Element enabled = tagReader.readElement(nextTag); processEnabled(enabled); @@ -655,7 +663,7 @@ public class XmppConnection implements Runnable { } } - private void processChallenge(Element challenge) throws IOException { + private void processChallenge(final Element challenge) throws IOException { final SaslMechanism.Version version; try { version = SaslMechanism.Version.of(challenge); @@ -688,6 +696,10 @@ public class XmppConnection implements Runnable { } catch (final IllegalArgumentException e) { throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } + final SaslMechanism currentSaslMechanism = this.saslMechanism; + if (currentSaslMechanism == null) { + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); + } final String challenge; if (version == SaslMechanism.Version.SASL) { challenge = success.getContent(); @@ -697,7 +709,7 @@ public class XmppConnection implements Runnable { throw new AssertionError("Missing implementation for " + version); } try { - saslMechanism.getResponse(challenge, sslSocketOrNull(socket)); + currentSaslMechanism.getResponse(challenge, sslSocketOrNull(socket)); } catch (final SaslMechanism.AuthenticationException e) { Log.e(Config.LOGTAG, String.valueOf(e)); throw new StateChangingException(Account.State.UNAUTHORIZED); @@ -705,25 +717,10 @@ public class XmppConnection implements Runnable { Log.d( Config.LOGTAG, account.getJid().asBareJid().toString() + ": logged in (using " + version + ")"); - if (SaslMechanism.pin(this.saslMechanism)) { - account.setPinnedMechanism(this.saslMechanism); + if (SaslMechanism.pin(currentSaslMechanism)) { + account.setPinnedMechanism(currentSaslMechanism); } if (version == SaslMechanism.Version.SASL_2) { - final Tag tag = tagReader.readTag(); - if (tag != null && tag.isStart("features", Namespace.STREAMS)) { - this.streamFeatures = tagReader.readElement(tag); - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": processed NOP stream features after success " - + XmlHelper.printElementNames(this.streamFeatures)); - } else { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": server did not send stream features after SASL2 success"); - throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); - } final String authorizationIdentifier = success.findChildContent("authorization-identifier"); final Jid authorizationJid; @@ -761,6 +758,7 @@ public class XmppConnection implements Runnable { account.getJid().asBareJid() + ": jid changed during SASL 2.0. updating database"); } + final boolean nopStreamFeatures; final Element bound = success.findChild("bound", Namespace.BIND2); final Element resumed = success.findChild("resumed", "urn:xmpp:sm:3"); final Element failed = success.findChild("failed", "urn:xmpp:sm:3"); @@ -773,6 +771,7 @@ public class XmppConnection implements Runnable { + ": server sent bound and resumed in SASL2 success"); throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } + final boolean processNopStreamFeatures = (resumed != null && streamId != null) || bound != null; if (resumed != null && streamId != null) { processResumed(resumed); } else if (failed != null) { @@ -801,9 +800,8 @@ public class XmppConnection implements Runnable { sendPostBindInitialization(waitForDisco, carbonsEnabled != null); } final HashedToken.Mechanism tokenMechanism; - final SaslMechanism currentMechanism = this.saslMechanism; - if (SaslMechanism.hashedToken(currentMechanism)) { - tokenMechanism = ((HashedToken) currentMechanism).getTokenMechanism(); + if (SaslMechanism.hashedToken(currentSaslMechanism)) { + tokenMechanism = ((HashedToken) currentSaslMechanism).getTokenMechanism(); } else if (this.hashTokenRequest != null) { tokenMechanism = this.hashTokenRequest; } else { @@ -813,6 +811,9 @@ public class XmppConnection implements Runnable { this.account.setFastToken(tokenMechanism,token); Log.d(Config.LOGTAG,account.getJid().asBareJid()+": storing hashed token "+tokenMechanism); } + if (processNopStreamFeatures) { + processNopStreamFeatures(); + } } mXmppConnectionService.databaseBackend.updateAccount(account); this.quickStartInProgress = false; @@ -831,6 +832,25 @@ public class XmppConnection implements Runnable { } } + private void processNopStreamFeatures() throws IOException { + final Tag tag = tagReader.readTag(); + if (tag != null && tag.isStart("features", Namespace.STREAMS)) { + this.streamFeatures = tagReader.readElement(tag); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": processed NOP stream features after success: " + + XmlHelper.printElementNames(this.streamFeatures)); + } else { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received " + tag); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": server did not send stream features after SASL2 success"); + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); + } + } + private void processFailure(final Element failure) throws IOException { final SaslMechanism.Version version; try { @@ -1248,8 +1268,7 @@ public class XmppConnection implements Runnable { private void processStreamFeatures(final Tag currentTag) throws IOException { this.streamFeatures = tagReader.readElement(currentTag); - final boolean isSecure = - features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS || account.isOnion(); + final boolean isSecure = isSecure(); final boolean needsBinding = !isBound && !account.isOptionSet(Account.OPTION_REGISTER); if (this.quickStartInProgress) { if (this.streamFeatures.hasChild("authentication", Namespace.SASL_2)) { @@ -1341,8 +1360,7 @@ public class XmppConnection implements Runnable { } private void authenticate() throws IOException { - final boolean isSecure = - features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS || account.isOnion(); + final boolean isSecure = isSecure(); if (isSecure && this.streamFeatures.hasChild("authentication", Namespace.SASL_2)) {authenticate(SaslMechanism.Version.SASL_2); } else if (isSecure && this.streamFeatures.hasChild("mechanisms", Namespace.SASL)) { authenticate(SaslMechanism.Version.SASL); @@ -1351,6 +1369,10 @@ public class XmppConnection implements Runnable { } } + private boolean isSecure() { + return features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS || account.isOnion(); + } + private void authenticate(final SaslMechanism.Version version) throws IOException { final Element authElement; if (version == SaslMechanism.Version.SASL) { @@ -1658,6 +1680,7 @@ public class XmppConnection implements Runnable { synchronized (this.commands) { this.commands.clear(); } + this.saslMechanism = null; } private void sendBindRequest() { From 109a20ca4045f7e8bfc5d28b4e4de9f7ad86b766 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 17 Nov 2022 10:52:30 +0100 Subject: [PATCH 077/101] do not expect stream features after inline resume --- .../java/eu/siacs/conversations/xmpp/XmppConnection.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 823e0747b80138585cfbeb713ae1557de5fb7b3a..0c2c718a334f84f62234461f330a9fd67699f11e 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -771,7 +771,7 @@ public class XmppConnection implements Runnable { + ": server sent bound and resumed in SASL2 success"); throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } - final boolean processNopStreamFeatures = (resumed != null && streamId != null) || bound != null; + final boolean processNopStreamFeatures; if (resumed != null && streamId != null) { processResumed(resumed); } else if (failed != null) { @@ -798,6 +798,9 @@ public class XmppConnection implements Runnable { features.carbonsEnabled = true; } sendPostBindInitialization(waitForDisco, carbonsEnabled != null); + processNopStreamFeatures = true; + } else { + processNopStreamFeatures = false; } final HashedToken.Mechanism tokenMechanism; if (SaslMechanism.hashedToken(currentSaslMechanism)) { @@ -811,6 +814,7 @@ public class XmppConnection implements Runnable { this.account.setFastToken(tokenMechanism,token); Log.d(Config.LOGTAG,account.getJid().asBareJid()+": storing hashed token "+tokenMechanism); } + // TODO it is currently unclear if a successful resume triggers new stream features or not if (processNopStreamFeatures) { processNopStreamFeatures(); } @@ -1354,7 +1358,7 @@ public class XmppConnection implements Runnable { Log.d( Config.LOGTAG, account.getJid().asBareJid() - + ": received NOP stream features " + + ": received NOP stream features: " + XmlHelper.printElementNames(this.streamFeatures)); } } From e74e2652d70f5613aa103ba9f9638b2f0a43b108 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 17 Nov 2022 11:03:56 +0100 Subject: [PATCH 078/101] bump various dependencies --- build.gradle | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 6ccdcf7053c0605a78e27e234faace9f33997a40..b48db295f76bda2279735e9f8eb427c05c345dce 100644 --- a/build.gradle +++ b/build.gradle @@ -33,7 +33,7 @@ configurations { dependencies { implementation 'androidx.viewpager:viewpager:1.0.0' - playstoreImplementation('com.google.firebase:firebase-messaging:23.0.8') { + playstoreImplementation('com.google.firebase:firebase-messaging:23.1.0') { exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' @@ -43,10 +43,10 @@ dependencies { implementation 'org.sufficientlysecure:openpgp-api:10.0' implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0' implementation 'androidx.appcompat:appcompat:1.5.1' - implementation 'androidx.exifinterface:exifinterface:1.3.3' + implementation 'androidx.exifinterface:exifinterface:1.3.5' implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' - implementation 'com.google.android.material:material:1.6.1' + implementation 'com.google.android.material:material:1.7.0' implementation "androidx.emoji2:emoji2:1.2.0" freeImplementation "androidx.emoji2:emoji2-bundled:1.2.0" @@ -77,7 +77,7 @@ dependencies { implementation 'com.google.guava:guava:31.1-android' quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.12.49' freeImplementation 'ch.threema:webrtc-android:100.0.0' - playstoreImplementation fileTree(include: ['libwebrtc-m104.aar'], dir: 'libs') + playstoreImplementation fileTree(include: ['libwebrtc-m107.aar'], dir: 'libs') } ext { From c03d7b84215a9b5c3ea9f6546c8360082cf192fc Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 17 Nov 2022 11:05:34 +0100 Subject: [PATCH 079/101] update build instructions --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 037e04106ab2da09b7b1622cec3d7e7984fc51ea..5bed4cd3155ed1ec766ad575c05b74be9da03305 100644 --- a/README.md +++ b/README.md @@ -431,11 +431,11 @@ Then issue the following commands in order to build the apk. git clone https://github.com/inputmice/Conversations.git cd Conversations - ./gradlew assembleConversationsFreeSystemDebug + ./gradlew assembleConversationsFreeDebug There are two build flavors available. *free* and *playstore*. Unless you know what you are doing you only need *free*. -You will find the apks in the `./build/outputs/apk/conversationsFreeSystem/debug/` directory. +You will find the apks in the `./build/outputs/apk/conversationsFree/debug/` directory. Be careful, the resulting apks will not install unless you delete your existing Conversations installation (which will delete all the messages from your phone, and if you have used OMEMO, you will not be able to restore them from the server). Do it at your own risk. @@ -447,8 +447,6 @@ Then the resulting APK can be installed ALONGSIDE normal Conversations. And have WARNING: DO NOT REPLACE ANYTHING ELSE ANYWHERE ELSE, DO NOT REPLACE THIS PROJECT WIDE. JUST 2 strings in THAT specific file! -[![Build Status](https://travis-ci.org/inputmice/Conversations.svg?branch=development)](https://travis-ci.org/inputmice/Conversations) - #### How do I debug Conversations If something goes wrong Conversations usually exposes very little information in From c3410bae82be517eb9a0df47d564f16b7aef7715 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 18 Nov 2022 10:34:29 +0100 Subject: [PATCH 080/101] pulled translations from transifex --- src/conversations/res/values-hr/strings.xml | 16 +++++ src/main/res/values-da-rDK/strings.xml | 4 ++ src/main/res/values-de/strings.xml | 10 ++- src/main/res/values-es/strings.xml | 5 ++ src/main/res/values-gl/strings.xml | 4 ++ src/main/res/values-hr/strings.xml | 76 +++++++++++++++++++++ src/main/res/values-it/strings.xml | 5 ++ src/main/res/values-ja/strings.xml | 4 ++ src/main/res/values-pl/strings.xml | 4 ++ src/main/res/values-pt-rBR/strings.xml | 4 ++ src/main/res/values-ro-rRO/strings.xml | 4 ++ src/main/res/values-zh-rCN/strings.xml | 4 ++ src/main/res/values-zh-rTW/strings.xml | 45 ++++++++---- 13 files changed, 167 insertions(+), 18 deletions(-) create mode 100644 src/conversations/res/values-hr/strings.xml create mode 100644 src/main/res/values-hr/strings.xml diff --git a/src/conversations/res/values-hr/strings.xml b/src/conversations/res/values-hr/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..093c6d0b93515d0dbf590ba949a1a6e63b9edfb2 --- /dev/null +++ b/src/conversations/res/values-hr/strings.xml @@ -0,0 +1,16 @@ + + + Odaberite svog XMPP davatelja usluga. + Koristite conversations.im + Napravi novi račun + Već imate XMPP račun? To može biti slučaj ako već koristite drugi XMPP klijent ili ste prije koristili Razgovore. Ako niste, možete odmah stvoriti novi XMPP račun.\nSavjet: Neki pružatelji usluga e-pošte također nude XMPP račune. + XMPP je mreža za razmjenu izravnih poruka neovisna o pružatelju usluga. Možete koristiti ovaj klijent s bilo kojim XMPP poslužiteljem koji odaberete.\nMeđutim, radi vaše udobnosti olakšali smo kreiranje računa na conversations.im; pružatelj usluga posebno prilagođen za korištenje s Conversations. + Pozvani ste na %1$s. Vodit ćemo vas kroz postupak kreiranja računa.\nPrilikom odabira %1$s pružatelja moći ćete komunicirati s korisnicima drugih pružatelja dajući im svoju punu XMPP adresu. + Pozvani ste na %1$s. Korisničko ime je već odabrano za vas. Vodit ćemo vas kroz postupak kreiranja računa.\nMoći ćete komunicirati s korisnicima drugih pružatelja tako da im date svoju punu XMPP adresu. + Vaša pozivnica za poslužitelj + Neispravno formatiran kod za dodjelu + Dodirnite gumb za dijeljenje kako biste svom kontaktu poslali pozivnicu na %1$s. + Ako je vaš kontakt u blizini, također može skenirati kod u nastavku kako bi prihvatio vašu pozivnicu. + Pridružite se %1$s i razgovarajte sa mnom: %2$s + Podijelite pozivnicu s... + \ No newline at end of file diff --git a/src/main/res/values-da-rDK/strings.xml b/src/main/res/values-da-rDK/strings.xml index 80e81485c83f7b0100bc3e52475fd612a4be3145..fa1c87bd168249f082b02f7735ee88b8ecb7d4b2 100644 --- a/src/main/res/values-da-rDK/strings.xml +++ b/src/main/res/values-da-rDK/strings.xml @@ -294,6 +294,8 @@ Aktiver stilletid Notifikationer vil være lydløs under stilletid Andre + Synkroniser bogmærker + Indstil \"autojoin\"-flag, når du går ind i eller forlader en MUC, og reager på ændringer foretaget af andre klienter. OMEMO-fingeraftryk kopieret til udklipsholder Du er udelukket fra denne gruppechat Denne gruppechat er kun for medlemmer @@ -301,6 +303,7 @@ Du er blevet smidt ud af denne gruppechat Gruppechatten er lukket ned Du er ikke længere i denne gruppechat + Du forlod denne gruppechat af tekniske årsager anvender konto %s hostet på %s Tjekker %s på HTTP vært @@ -976,4 +979,5 @@ Kontoregistrering er ikke understøttet Ingen XMPP-adresse fundet Midlertidig godkendelsesfejl + Slet avatar diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index 928c8311f4a21d0cd2fe5b0b19d4fddf9af22e24..7fbc7ec10cc95d43417efe8e098a35270a76269c 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -292,8 +292,10 @@ Beginn Ende Ruhige Stunden aktivieren - Benachrichtigungen sind während der ruhigen Stunden stumm. + Benachrichtigungen sind während der ruhigen Stunden stumm Sonstiges + Lesezeichen synchronisieren + Setzt das \"Autojoin\"-Kennzeichen beim Betreten oder Verlassen eines Gruppenchats/Channels und reagiert auf Änderungen durch andere Clients. OMEMO-Fingerabdruck in die Zwischenablage kopiert Du wurdest aus diesem Gruppenchat ausgeschlossen Dieser Gruppenchat ist nur für Mitglieder @@ -301,6 +303,7 @@ Du wurdest aus diesem Gruppchat geworfen Gruppenchat wurde geschlossen Du bist nicht länger in diesem Gruppenchat + Du hast diesen Gruppenchat aus technischen Gründen verlassen verwende Konto %s gehostet bei %s %s auf HTTP-Host wird überprüft @@ -471,7 +474,7 @@ Fehlerhaft Status Abwesend bei gesperrtem Gerät - Als abwesend anzeigen, wenn das Gerät gesperrt ist. + Als abwesend anzeigen, wenn das Gerät gesperrt ist Beschäftigt im lautlosen Modus Als Beschäftigt anzeigen, wenn sich das Gerät im lautlosen Modus befindet Vibration als Lautlos behandeln @@ -554,7 +557,7 @@ Nicht verfügbar Beschäftigt Ein sicheres Passwort wurde erstellt - Dein Gerät unterstützt kein Ausschalten der Akkuoptimierung + Dein Gerät unterstützt nicht das Ausschalten der Akkuoptimierung Registrierung fehlgeschlagen: Bitte später versuchen Registrierung fehlgeschlagen: Passwort zu schwach Teilnehmer wählen @@ -976,4 +979,5 @@ Kontoregistrierungen werden nicht unterstützt Keine XMPP-Adresse gefunden Temporärer Authentifizierungsfehler + Profilbild löschen diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index fce2d4736a35c9dc0c3e417af1defdc3f9e5a4a2..04a5cc17039e6f5140e3be003255a73352c07f01 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -297,6 +297,8 @@ Habilitar horario de silencio Las notificaciones serán silenciadas durante el horario de silencio Otros + Sincronizar marcadores + Establecer la opción \"unirse automáticamente\" cuando entras o sales de un MUC y reaccionar a las modificaciones realizadas por otros clientes. Huella digital OMEMO copiada al portapapeles Tu entrada a esta conversación en grupo ha sido prohibida Esta conversación en grupo es solo para miembros @@ -304,6 +306,7 @@ Has sido expulsado de esta conversación La conversación en grupo ha sido cerrada Ya no estás dentro de esta conversación en grupo + Has dejado esta conversación en grupo debido a razones técnicas. Usando cuenta %s alojado en %s Comprobando %s en servidor HTTP @@ -418,6 +421,7 @@ vídeo imagen gráfico de vectores + archivo multimedia documento PDF Android App Contacto @@ -988,4 +992,5 @@ Los registros de cuenta no están soportados Dirección XMPP no encontrada Fallo temporal de autenticación + Eliminar imagen de perfil diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index 2b52435fe4131145626357b3c4be5962d6dceccc..8c8c00f60fd944e5a7b76c892bf4c6b14750c53c 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -294,6 +294,8 @@ Establecer horario sen notificacións As notificacións serán silenciadas durante estas horas Outro + Sincronizar marcadores + Poñer marca de \"autojoin\" ao entrar ou deixar unha MUC e reaccionar ás modificacións feitas desde outros clientes. Copiouse a impresión dixital OMEMO ao portapapeis Non podes acceder a esta conversa en grupo Esta conversa en grupo é so para membros @@ -301,6 +303,7 @@ Xa foi expulsado de esta conversa en grupo A conversa en grupo foi apagada Xa non estás nesta conversa en grupo + Deixaches esta conversa en grupo por razóns técnicas utilizando a conta %s hospedado en %s Comprobando %s no servidor HTTP @@ -976,4 +979,5 @@ Non está permitido o rexistro de novas contas Non se atopa un enderezo XMPP Fallo temporal da autenticación + Eliminar avatar diff --git a/src/main/res/values-hr/strings.xml b/src/main/res/values-hr/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..440ee1fedbd6003b265df6b09c7e67673481fa3c --- /dev/null +++ b/src/main/res/values-hr/strings.xml @@ -0,0 +1,76 @@ + + + Postavke + Novi razgovor + Upravljanje računima + Upravljaj računom + Zatvori razgovor + Kontakt podaci + Pojedinosti grupnog razgovora + Detalji kanala + Dodaj račun + Uredi ime + Dodaj u adresar + Izbriši s popisa + Blokiraj kontakt + Odblokiraj kontakt + Blokiraj domenu + Odblokiraj domenu + Blokiraj sudionika + Deblokiraj sudionika + Upravljanje računima + Postavke + Dijeli s Conversation + Započni razgovor + Odaberite Kontakt + Odaberite kontakte + Dijeli putem računa + Lista blokiranih + upravo sad + prije 1 min + prije %d min + + %d nepročitan razgovor + + + %d nepročitanih razgovora + + + %d nepročitani razgovori + + + slanje… + Dešifriranje poruke. Molimo pričekajte… + OpenPGP šifrirana poruka + Nadimak je već u upotrebi + Nevažeći nadimak + Admin + Vlasnik + Moderator + Sudionik + Posjetitelj + Želite li ukloniti %s s popisa kontakata? Razgovori s ovim kontaktom neće biti uklonjeni. + Želite li blokirati %s da vam šalje poruke? + Želite li deblokirati %s i dopustiti im da vam šalju poruke? + Blokirati sve kontakte iz %s? + Deblokirati sve kontakte iz %s? + Kontakt blokiran + Blokiran + Želite li ukloniti %s kao oznaku? Razgovori s ovom knjižnom oznakom neće biti uklonjeni. + Registrirajte novi račun na poslužitelju + Promjena lozinke na poslužitelju + Podijeli s… + Započni razgovor + Pozovi kontakt + Pozovi + Kontakti + Kontakt + Otkazati + Dodati + Uredi + Obriši + Blok + Odblokiraj + Sačuvaj + Ok + diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml index 37ef96f7916d6070e82d069dba2f9b44b7c0704d..df1331f462954b83e5d14741a8d871e0db737b3b 100644 --- a/src/main/res/values-it/strings.xml +++ b/src/main/res/values-it/strings.xml @@ -297,6 +297,8 @@ Attiva ore di quiete Le notifiche verranno silenziate durante le ore di quiete Altro + Sincronizza i segnalibri + Imposta il flag \"auto-entrata\" quando entri o esci da un MUC e reagisci alle modifiche fatte dagli altri client. Impronta OMEMO copiata negli appunti Sei stato bandito da questa chat di gruppo Questa chat di gruppo è solo per membri @@ -304,6 +306,7 @@ Sei stato buttato fuori da questa chat di gruppo La chat di gruppo è stata chiusa Non sei più in questa chat di gruppo + Hai lasciato questa chat di gruppo per motivi tecnici usando il profilo %s ospitato su %s Controllo %s su host HTTP @@ -418,6 +421,7 @@ video immagine grafica vettoriale + file multimediale Documento PDF Applicazione Android Contatto @@ -988,4 +992,5 @@ Le registrazioni di profili non sono supportate Nessun indirizzo XMPP trovato Errore di autenticazione temporaneo + Elimina avatar diff --git a/src/main/res/values-ja/strings.xml b/src/main/res/values-ja/strings.xml index 94335a35981d270041fb1ffa0485ecefc84ee108..5f1edfc60dd26ebf0e0435375e0d2d7314947264 100644 --- a/src/main/res/values-ja/strings.xml +++ b/src/main/res/values-ja/strings.xml @@ -291,6 +291,7 @@ 消音時間を有効化 消音時間の間、通知は無音になります その他 + ブックマーク同期 OMEMO フィンガープリントをクリップボードにコピーしました このグループチャットから出禁にされています このグループチャットはメンバー制です @@ -298,6 +299,7 @@ このグループチャットから蹴り出されています このグループチャットは閉鎖されました あなたはもうこのグループチャットに参加していません + 技術的理由の為、あなたはこのグループチャットを離れました アカウント %s を使用 %s 上でホストされた HTTP ホスト上の %s を確認中 @@ -412,6 +414,7 @@ ビデオ 画像 ベクター画像 + マルチメディアファイル PDF 文書 Android アプリ 連絡先 @@ -956,4 +959,5 @@ アカウント登録はサポートされていません XMPPアドレスがみつかりません 一時的な認証失敗 + アバターを削除 diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index 53158925fab28c75f128534b45cb47a67f5605c1..75c46057a0de5f167695e382dd25e38141f4b76d 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -300,6 +300,8 @@ Włącz godziny ciszy Powiadomienia będą wyciszone w wybranym przedziale czasu Inne + Synchronizuj zakładki + Ustaw flagę automatycznego dołączania przy wchodzeniu lub opuszczaniu pokoju i reaguj na zmiany innych klientów Odcisk klucza OMEMO został skopiowany do schowka Zbanowany Konferencja tylko dla użytkowników @@ -307,6 +309,7 @@ Wykopany Konferencja została zamknięta Nie uczestniczysz już w tej konferencji + Opuszczono rozmowę grupową z powodu usterki technicznej używając konta %s udostępnione na %s Sprawdzanie %s na hoście HTTP @@ -1003,4 +1006,5 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Rejestracja kont nie jest wspierana Nie znaleziono adresu XMPP Tymczasowy błąd uwierzytelniania + Usuń awatar diff --git a/src/main/res/values-pt-rBR/strings.xml b/src/main/res/values-pt-rBR/strings.xml index fd4b29cc257bed3e39aebbc752231a9e127017db..58e17a785f5af4515630262faff3a860daeba618 100644 --- a/src/main/res/values-pt-rBR/strings.xml +++ b/src/main/res/values-pt-rBR/strings.xml @@ -297,6 +297,8 @@ Habilitar horário de sossego As notificações serão silenciadas no horário de sossego. Outras + Sincronizar favoritos + Define a flag \"autojoin\" ao entrar ou sair de uma sala e reage a modificações feitas por outros clientes. Impressão digital OMEMO copiada para a área de transferência Você foi banido desta conversa em grupo Somente membros podem entrar nessa conversa em grupo @@ -304,6 +306,7 @@ Você foi retirado desta conversa em grupo A conversa em grupo foi encerrada Você não está mais nesta conversa em grupo + Você saiu desta conversa em grupo devido a razões técnicas usando a conta %s hospedado em %s Verificando %s no host HTTP @@ -989,4 +992,5 @@ O registro de contas não está ativo Não foi encontrado nenhum endereço XMPP Falha temporária na autenticação + Excluir avatar diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index 34c5c155fb3d09f7a1ff174a6f6d6211205a0e4a..2f35075a5d2295b93f498292fc793b90486eb842 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -297,6 +297,8 @@ Activează orar de liniște Notificările vor fi reduse la tăcere în timpul orelor de liniște Altele + Sincronizare semne de carte + Setați \"autojoin\" la intrarea sau ieșirea dintr-o discuție de grup și reacționați la modificările efectuate de alți clienți. Amprentă OMEMO copiată în memorie V-a fost interzis accesul la această discuție de grup Această discuție de grup este rezervată membrilor @@ -304,6 +306,7 @@ Ați fost dat(ă) afară din această discuție de grup Discuția de grup a fost închisă Nu mai sunteți în această discuție de grup + Ați părăsit această discuție de grup din motive tehnice folosind cont %s găzduit pe %s Verifica %s pe gazda HTTP @@ -989,4 +992,5 @@ Nu este posibilă înregistrarea unui cont Nu a fost găsită o adresă XMPP Eroare temporară de autentificare + Șterge avatar diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index ad19591cfb4a87c518e2d338e9850f4bbfe4aab9..cca20889f8f9c5682cf6a2366b886683bc6b0f9f 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -291,6 +291,8 @@ 启用静默时间段 在静默时间段内通知将保持静音 其他 + 同步书签 + 加入或离开多用户聊天时设置 “autojoin\" 标志,并回应其他客户端所做更改。 OMEMO指纹已拷贝到剪贴板 您被封禁了 这个群聊只允许成员聊天 @@ -298,6 +300,7 @@ 您被从此群聊踢出 这个群聊已被关闭 您已不在该群组 + 你出于技术原因离开了群聊 使用帐户%s 托管于%s 正在HTTP服务器中检查%s @@ -963,4 +966,5 @@ 不支持注册账户 未找到 XMPP 地址 临时认证失败 + 删除群聊 diff --git a/src/main/res/values-zh-rTW/strings.xml b/src/main/res/values-zh-rTW/strings.xml index edcacf37469d4aeae5d92a928d03cc86e734dd15..5c21adb4f764a45d28665f1222aa6bfd1a467ac3 100644 --- a/src/main/res/values-zh-rTW/strings.xml +++ b/src/main/res/values-zh-rTW/strings.xml @@ -145,8 +145,10 @@ 未找到伺服器 未連接網路 註冊失敗 - 用戶名已存在 + 使用者名稱已被使用 註冊完成 + 伺服器不支援註冊 + 無效的註冊權杖 違反政策 伺服器不相容 串流錯誤 @@ -162,6 +164,7 @@ 確定要移除上線狀態中的 OpenPGP 公開金鑰嗎?\n這樣一來,你的聯絡人就無法傳送以 OpenPGP 加密的訊息給你了。 啟用帳戶 確定? + 刪除帳戶將清除您全部的會話記錄 錄音 XMPP 位址 封鎖 XMPP 位址 @@ -186,18 +189,22 @@ 缺少公開金鑰通知 剛剛查看過 %d 分鐘前查看過 + 一小時前查看過 %d 小時前查看過 + 一天前查看過 %d 天前查看過 OpenPGP 金鑰 ID OMEMO 指紋 v\\OMEMO 指紋 + OMEMO 指紋 (訊息來源) + v\\OMEMO 指紋 (訊息來源) 其他裝置 信任的 OMEMO 指紋 正在擷取金鑰… 完成 解密 書籤 - 尋找 + 搜尋 輸入聯絡人 刪除聯絡人 檢視聯絡人詳細資料 @@ -310,7 +317,7 @@ 離線 拋棄 成員 - 高級模式 + 進階模式 授予管理員許可權 吊銷管理員許可權 不能修改 %s 的從屬關係 @@ -320,28 +327,34 @@ 您尚未參與 從不 直到新的通知 + 延遲 + 回覆 + 標示為已讀 輸入 - 回車是發送 - 顯示回車鍵 - 改變表情鍵為回車鍵 + Enter 鍵傳送 + 顯示 Enter 鍵 + 變更表情符號鍵為 Enter 鍵 音訊 影片 - 圖像 - PDF 文檔 - Android App - 連絡人 + 圖片 + 向量圖形 + 多媒體檔案 + PDF 文件 + Android 應用程式 + 聯絡人 頭像已經發佈! 發送中 %s 提供中 %s 隱藏離線連絡人 - %s 正在輸入中… - %s 停止輸入了 - %s 正在輸入中… - %s 停止輸入了 + %s 正在輸入… + %s 已停止輸入 + %s 正在輸入… + %s 已停止輸入 鍵盤輸入通知 讓聯絡人知道你正在寫訊息送給它們 - 發送位置 + 傳送位置 顯示位置 + 找不到可以顯示位置的應用程式 位置 Conversation 已關閉 不信任系統的憑證機構 @@ -394,6 +407,8 @@ %d 則訊息 載入更多訊息 + 與 %s 分享的檔案 + 與 %s 分享的圖片 與連絡人同步 為所有訊息顯示通知 關閉通知 From 6b9ebb3abfe17703d8b292443f56bcca6444848f Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 18 Nov 2022 10:40:16 +0100 Subject: [PATCH 081/101] remove TODO --- src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 0c2c718a334f84f62234461f330a9fd67699f11e..54305cdb7d0059ef8680390bba76aba16a39ec44 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -814,7 +814,7 @@ public class XmppConnection implements Runnable { this.account.setFastToken(tokenMechanism,token); Log.d(Config.LOGTAG,account.getJid().asBareJid()+": storing hashed token "+tokenMechanism); } - // TODO it is currently unclear if a successful resume triggers new stream features or not + // a successful resume will not send stream features if (processNopStreamFeatures) { processNopStreamFeatures(); } From d51682a9bc63048db4536a788ac51cc6ad75b23b Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 18 Nov 2022 10:45:12 +0100 Subject: [PATCH 082/101] version bump to 2.11.0-beta --- CHANGELOG.md | 7 +++++++ build.gradle | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a10c80882082d356d9407ca86dd08ad4980dea2..7bed8eedb3ead266e73bc66f73837c4581ab71b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +### Version 2.11.0 + +* Implement Extensible SASL Profile, Bind 2.0 and Fast for faster reconnects +* Implement Channel Binding +* Add ability to delete own avatar +* Add notification for missed calls + ### Version 2.10.10 * Minor bug fixes diff --git a/build.gradle b/build.gradle index b48db295f76bda2279735e9f8eb427c05c345dce..f5c5fda514c6739a0ec9b63b5d7b5602befd20fd 100644 --- a/build.gradle +++ b/build.gradle @@ -93,8 +93,8 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion 32 - versionCode 42038 - versionName "2.10.10" + versionCode 42039 + versionName "2.11.0-beta" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId From 8fb2c11771d8e7a419682da7be8a38e5068c4460 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 19 Nov 2022 08:14:50 +0100 Subject: [PATCH 083/101] use plurals for missed call strings --- .../services/NotificationService.java | 41 +++++++++++-------- .../xmpp/jingle/JingleRtpConnection.java | 1 + src/main/res/values/strings.xml | 16 ++++++-- 3 files changed, 38 insertions(+), 20 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index 55e220f62262495a0088098e10cc001f3613d89b..715cafe4264c1e019206ae46313503848f61789b 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -987,12 +987,17 @@ public class NotificationService { (totalCalls == 1) ? mXmppConnectionService.getString(R.string.missed_call) : (mMissedCalls.size() == 1) - ? mXmppConnectionService.getString( - R.string.n_missed_calls, totalCalls) - : mXmppConnectionService.getString( - R.string.n_missed_calls_from_m_contacts, - totalCalls, - mMissedCalls.size()); + ? mXmppConnectionService + .getResources() + .getQuantityString( + R.plurals.n_missed_calls, totalCalls, totalCalls) + : mXmppConnectionService + .getResources() + .getQuantityString( + R.plurals.n_missed_calls_from_m_contacts, + mMissedCalls.size(), + totalCalls, + mMissedCalls.size()); builder.setContentTitle(title); builder.setTicker(title); if (!publicVersion) { @@ -1027,21 +1032,25 @@ public class NotificationService { final String title = (info.getNumberOfCalls() == 1) ? mXmppConnectionService.getString(R.string.missed_call) - : mXmppConnectionService.getString( - R.string.n_missed_calls, info.getNumberOfCalls()); + : mXmppConnectionService + .getResources() + .getQuantityString( + R.plurals.n_missed_calls, + info.getNumberOfCalls(), + info.getNumberOfCalls()); builder.setContentTitle(title); final String name = conversation.getContact().getDisplayName(); if (publicVersion) { builder.setTicker(title); } else { - if (info.getNumberOfCalls() == 1) { - builder.setTicker( - mXmppConnectionService.getString(R.string.missed_call_from_x, name)); - } else { - builder.setTicker( - mXmppConnectionService.getString( - R.string.n_missed_calls_from_x, info.getNumberOfCalls(), name)); - } + builder.setTicker( + mXmppConnectionService + .getResources() + .getQuantityString( + R.plurals.n_missed_calls_from_x, + info.getNumberOfCalls(), + info.getNumberOfCalls(), + name)); builder.setContentText(name); } builder.setSmallIcon(R.drawable.ic_call_missed_white_24db); 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 c69fc6b023eaec40c762569d6ae6de91bb01ca5d..e2832355db0bba6ee5f01176956207c158a0d67d 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -1291,6 +1291,7 @@ public class JingleRtpConnection extends AbstractJingleConnection SessionDescription.parse(webRTCSessionDescription.description); final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription); this.initiatorRtpContentMap = rtpContentMap; + //TODO delay ready to receive ice until after session-init this.webRTCWrapper.setIsReadyToReceiveIceCandidates(true); final ListenableFuture outgoingContentMapFuture = encryptSessionInitiate(rtpContentMap); diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index f668e3f25d9fc767bdb839102bddcf0339b03f46..d14a0c97152c464011a51eecb8746537be6a2f5c 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -936,10 +936,18 @@ Outgoing call Outgoing call · %s Missed call - Missed call from %s - %1$d missed calls from %2$s - %d missed calls - %1$d missed calls from %2$d contacts + + %1$d missed call from %2$s + %1$d missed calls from %2$s + + + %d missed call + %d missed calls + + + %1$d missed calls from %2$d contact + %1$d missed calls from %2$d contacts + Audio call Video call Help From 27d8da2ab4e927a4b81c266a2501ed666eaedd4b Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 19 Nov 2022 13:03:34 +0100 Subject: [PATCH 084/101] refactor WebRTCWrapper to allow for track adds --- .../xmpp/jingle/VideoSourceWrapper.java | 12 +-- .../xmpp/jingle/WebRTCWrapper.java | 102 +++++++++++++----- 2 files changed, 80 insertions(+), 34 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/VideoSourceWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/VideoSourceWrapper.java index 5e83f2ba9ef8ce9cadcae9df4460cf05430dc612..b837131e88a2d7f28dc15527b4a4e7ab26af26e7 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/VideoSourceWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/VideoSourceWrapper.java @@ -3,7 +3,6 @@ package eu.siacs.conversations.xmpp.jingle; import android.content.Context; import android.util.Log; -import com.google.common.base.Optional; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.util.concurrent.ListenableFuture; @@ -127,7 +126,7 @@ class VideoSourceWrapper { this.context = context; } - public Optional create() { + public VideoSourceWrapper create() { final CameraEnumerator enumerator = new Camera2Enumerator(context); final Set deviceNames = ImmutableSet.copyOf(enumerator.getDeviceNames()); for (final String deviceName : deviceNames) { @@ -135,17 +134,16 @@ class VideoSourceWrapper { final VideoSourceWrapper videoSourceWrapper = of(enumerator, deviceName, deviceNames); if (videoSourceWrapper == null) { - return Optional.absent(); + return null; } videoSourceWrapper.isFrontCamera = true; - return Optional.of(videoSourceWrapper); + return videoSourceWrapper; } } if (deviceNames.size() == 0) { - return Optional.absent(); + return null; } else { - return Optional.fromNullable( - of(enumerator, Iterables.get(deviceNames, 0), deviceNames)); + return of(enumerator, Iterables.get(deviceNames, 0), deviceNames); } } 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 f71799bdf5481857186a6a1a592eac1213f7acb2..4ce9c147862e118dd28eeed99ffea0ef113e1de4 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -196,6 +196,7 @@ public class WebRTCWrapper { + ")"); } }; + @Nullable private PeerConnectionFactory peerConnectionFactory = null; @Nullable private PeerConnection peerConnection = null; private AppRTCAudioManager appRTCAudioManager = null; private ToneManager toneManager = null; @@ -260,7 +261,7 @@ public class WebRTCWrapper { String.format( "setUseHardwareAcousticEchoCanceler(%s) model=%s", setUseHardwareAcousticEchoCanceler, Build.MODEL)); - PeerConnectionFactory peerConnectionFactory = + this.peerConnectionFactory = PeerConnectionFactory.builder() .setVideoDecoderFactory( new DefaultVideoDecoderFactory(eglBase.getEglBaseContext())) @@ -268,7 +269,7 @@ public class WebRTCWrapper { new DefaultVideoEncoderFactory( eglBase.getEglBaseContext(), true, true)) .setAudioDeviceModule( - JavaAudioDeviceModule.builder(context) + JavaAudioDeviceModule.builder(requireContext()) .setUseHardwareAcousticEchoCanceler( setUseHardwareAcousticEchoCanceler) .createAudioDeviceModule()) @@ -276,36 +277,18 @@ public class WebRTCWrapper { final PeerConnection.RTCConfiguration rtcConfig = buildConfiguration(iceServers); final PeerConnection peerConnection = - peerConnectionFactory.createPeerConnection(rtcConfig, peerConnectionObserver); + requirePeerConnectionFactory() + .createPeerConnection(rtcConfig, peerConnectionObserver); if (peerConnection == null) { throw new InitializationException("Unable to create PeerConnection"); } - final Optional optionalVideoSourceWrapper = - media.contains(Media.VIDEO) - ? new VideoSourceWrapper.Factory(requireContext()).create() - : Optional.absent(); - - if (optionalVideoSourceWrapper.isPresent()) { - this.videoSourceWrapper = optionalVideoSourceWrapper.get(); - this.videoSourceWrapper.initialize( - peerConnectionFactory, context, eglBase.getEglBaseContext()); - this.videoSourceWrapper.startCapture(); - - final VideoTrack videoTrack = - peerConnectionFactory.createVideoTrack( - "my-video-track", this.videoSourceWrapper.getVideoSource()); - - this.localVideoTrack = TrackWrapper.addTrack(peerConnection, videoTrack); + if (media.contains(Media.VIDEO)) { + addVideoTrack(peerConnection); } if (media.contains(Media.AUDIO)) { - // set up audio track - final AudioSource audioSource = - peerConnectionFactory.createAudioSource(new MediaConstraints()); - final AudioTrack audioTrack = - peerConnectionFactory.createAudioTrack("my-audio-track", audioSource); - this.localAudioTrack = TrackWrapper.addTrack(peerConnection, audioTrack); + addAudioTrack(peerConnection); } peerConnection.setAudioPlayout(true); peerConnection.setAudioRecording(true); @@ -313,6 +296,58 @@ public class WebRTCWrapper { this.peerConnection = peerConnection; } + private VideoSourceWrapper initializeVideoSourceWrapper() { + final VideoSourceWrapper existingVideoSourceWrapper = this.videoSourceWrapper; + if (existingVideoSourceWrapper != null) { + existingVideoSourceWrapper.startCapture(); + return existingVideoSourceWrapper; + } + final VideoSourceWrapper videoSourceWrapper = + new VideoSourceWrapper.Factory(requireContext()).create(); + if (videoSourceWrapper == null) { + throw new IllegalStateException("Could not instantiate VideoSourceWrapper"); + } + videoSourceWrapper.initialize( + requirePeerConnectionFactory(), requireContext(), eglBase.getEglBaseContext()); + videoSourceWrapper.startCapture(); + return videoSourceWrapper; + } + + public synchronized boolean addTrack(final Media media) { + if (media == Media.VIDEO) { + return addVideoTrack(requirePeerConnection()); + } else if (media == Media.AUDIO) { + return addAudioTrack(requirePeerConnection()); + } + throw new IllegalStateException(String.format("Could not add track for %s", media)); + } + + private boolean addAudioTrack(final PeerConnection peerConnection) { + final AudioSource audioSource = + requirePeerConnectionFactory().createAudioSource(new MediaConstraints()); + final AudioTrack audioTrack = + requirePeerConnectionFactory().createAudioTrack("my-audio-track", audioSource); + this.localAudioTrack = TrackWrapper.addTrack(peerConnection, audioTrack); + return true; + } + + private boolean addVideoTrack(final PeerConnection peerConnection) { + Preconditions.checkState( + this.localVideoTrack == null, "A local video track already exists"); + final VideoSourceWrapper videoSourceWrapper; + try { + videoSourceWrapper = initializeVideoSourceWrapper(); + } catch (final IllegalStateException e) { + Log.d(Config.LOGTAG, "could not add video track", e); + return false; + } + final VideoTrack videoTrack = + requirePeerConnectionFactory() + .createVideoTrack("my-video-track", videoSourceWrapper.getVideoSource()); + this.localVideoTrack = TrackWrapper.addTrack(peerConnection, videoTrack); + return true; + } + private static PeerConnection.RTCConfiguration buildConfiguration( final List iceServers) { final PeerConnection.RTCConfiguration rtcConfig = @@ -344,6 +379,7 @@ public class WebRTCWrapper { synchronized void close() { final PeerConnection peerConnection = this.peerConnection; + final PeerConnectionFactory peerConnectionFactory = this.peerConnectionFactory; final VideoSourceWrapper videoSourceWrapper = this.videoSourceWrapper; final AppRTCAudioManager audioManager = this.appRTCAudioManager; final EglBase eglBase = this.eglBase; @@ -363,12 +399,15 @@ public class WebRTCWrapper { } catch (final InterruptedException e) { Log.e(Config.LOGTAG, "unable to stop capturing"); } - // TODO call dispose + videoSourceWrapper.dispose(); } if (eglBase != null) { eglBase.release(); this.eglBase = null; } + if (peerConnectionFactory != null) { + peerConnectionFactory.dispose(); + } } synchronized void verifyClosed() { @@ -530,6 +569,7 @@ public class WebRTCWrapper { } } + @Nonnull private PeerConnection requirePeerConnection() { final PeerConnection peerConnection = this.peerConnection; if (peerConnection == null) { @@ -538,6 +578,15 @@ public class WebRTCWrapper { return peerConnection; } + @Nonnull + private PeerConnectionFactory requirePeerConnectionFactory() { + final PeerConnectionFactory peerConnectionFactory = this.peerConnectionFactory; + if (peerConnectionFactory == null) { + throw new IllegalStateException("Make sure PeerConnectionFactory is initialized"); + } + return peerConnectionFactory; + } + void addIceCandidate(IceCandidate iceCandidate) { requirePeerConnection().addIceCandidate(iceCandidate); } @@ -626,5 +675,4 @@ public class WebRTCWrapper { super(message); } } - } From 59ea66ca78dde11f196c9764a82752c9145458b7 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 19 Nov 2022 14:19:07 +0100 Subject: [PATCH 085/101] make sure VideoSourceWrapper is stored in property --- .../java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java | 1 + 1 file changed, 1 insertion(+) 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 4ce9c147862e118dd28eeed99ffea0ef113e1de4..de26f1afc1bbfb44c0feee7b104adce58f15e5a8 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -310,6 +310,7 @@ public class WebRTCWrapper { videoSourceWrapper.initialize( requirePeerConnectionFactory(), requireContext(), eglBase.getEglBaseContext()); videoSourceWrapper.startCapture(); + this.videoSourceWrapper = videoSourceWrapper; return videoSourceWrapper; } From 304205b2e344ae1c1b6b17e230589109a230b121 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 20 Nov 2022 17:00:40 +0100 Subject: [PATCH 086/101] take senders attr into account when converting to and from sdp --- .../crypto/axolotl/AxolotlService.java | 4 +- .../jingle/JingleFileTransferConnection.java | 27 +-- .../xmpp/jingle/JingleRtpConnection.java | 14 +- .../xmpp/jingle/RtpContentMap.java | 104 +++++++++--- .../xmpp/jingle/SessionDescription.java | 158 ++++++++++++------ .../xmpp/jingle/stanzas/Content.java | 74 ++++++-- 6 files changed, 274 insertions(+), 107 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index 3d4f23360096f97a45ba7354dccbd672a4d586d0..05ffdbdca5bf085b1fd151362e67cd1c5f0122f4 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -1272,7 +1272,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } descriptionTransportBuilder.put( content.getKey(), - new RtpContentMap.DescriptionTransport(descriptionTransport.description, encryptedTransportInfo) + new RtpContentMap.DescriptionTransport(descriptionTransport.senders, descriptionTransport.description, encryptedTransportInfo) ); } return Futures.immediateFuture( @@ -1306,7 +1306,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { omemoVerification.setOrEnsureEqual(decryptedTransport); descriptionTransportBuilder.put( content.getKey(), - new RtpContentMap.DescriptionTransport(descriptionTransport.description, decryptedTransport.payload) + new RtpContentMap.DescriptionTransport(descriptionTransport.senders, descriptionTransport.description, decryptedTransport.payload) ); } processPostponed(); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java index 43aaa54b577534b38a06af647cc5a934fecbbd75..c4ed04bd048ff17be2ebb974881e2a4dda12d931 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java @@ -577,8 +577,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple private void sendInitRequest() { final JinglePacket packet = this.bootstrapPacket(JinglePacket.Action.SESSION_INITIATE); - final Content content = new Content(this.contentCreator, this.contentName); - content.setSenders(this.contentSenders); + final Content content = new Content(this.contentCreator, this.contentSenders, this.contentName); if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL && remoteSupportsOmemoJet) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": remote announced support for JET"); final Element security = new Element("security", Namespace.JINGLE_ENCRYPTED_TRANSPORT); @@ -656,8 +655,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple gatherAndConnectDirectCandidates(); this.jingleConnectionManager.getPrimaryCandidate(this.id.account, isInitiator(), (success, candidate) -> { final JinglePacket packet = bootstrapPacket(JinglePacket.Action.SESSION_ACCEPT); - final Content content = new Content(contentCreator, contentName); - content.setSenders(this.contentSenders); + final Content content = new Content(contentCreator, contentSenders, contentName); content.setDescription(this.description); if (success && candidate != null && !equalCandidateExists(candidate)) { final JingleSocks5Transport socksConnection = new JingleSocks5Transport(this, candidate); @@ -696,8 +694,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple private void sendAcceptIbb() { this.transport = new JingleInBandTransport(this, this.transportId, this.ibbBlockSize); final JinglePacket packet = bootstrapPacket(JinglePacket.Action.SESSION_ACCEPT); - final Content content = new Content(contentCreator, contentName); - content.setSenders(this.contentSenders); + final Content content = new Content(contentCreator, contentSenders, contentName); content.setDescription(this.description); content.setTransport(new IbbTransportInfo(this.transportId, this.ibbBlockSize)); packet.addJingleContent(content); @@ -910,8 +907,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple private void sendFallbackToIbb() { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": sending fallback to ibb"); final JinglePacket packet = this.bootstrapPacket(JinglePacket.Action.TRANSPORT_REPLACE); - final Content content = new Content(this.contentCreator, this.contentName); - content.setSenders(this.contentSenders); + final Content content = new Content(this.contentCreator, this.contentSenders, this.contentName); this.transportId = JingleConnectionManager.nextRandomId(); content.setTransport(new IbbTransportInfo(this.transportId, this.ibbBlockSize)); packet.addJingleContent(content); @@ -944,8 +940,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple final JinglePacket answer = bootstrapPacket(JinglePacket.Action.TRANSPORT_ACCEPT); - final Content content = new Content(contentCreator, contentName); - content.setSenders(this.contentSenders); + final Content content = new Content(contentCreator, contentSenders, contentName); content.setTransport(new IbbTransportInfo(this.transportId, this.ibbBlockSize)); answer.addJingleContent(content); @@ -1124,8 +1119,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple private void sendProxyActivated(String cid) { final JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO); - final Content content = new Content(this.contentCreator, this.contentName); - content.setSenders(this.contentSenders); + final Content content = new Content(this.contentCreator, this.contentSenders, this.contentName); content.setTransport(new S5BTransportInfo(this.transportId, new Element("activated").setAttribute("cid", cid))); packet.addJingleContent(content); this.sendJinglePacket(packet); @@ -1133,8 +1127,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple private void sendProxyError() { final JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO); - final Content content = new Content(this.contentCreator, this.contentName); - content.setSenders(this.contentSenders); + final Content content = new Content(this.contentCreator, this.contentSenders, this.contentName); content.setTransport(new S5BTransportInfo(this.transportId, new Element("proxy-error"))); packet.addJingleContent(content); this.sendJinglePacket(packet); @@ -1142,8 +1135,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple private void sendCandidateUsed(final String cid) { JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO); - final Content content = new Content(this.contentCreator, this.contentName); - content.setSenders(this.contentSenders); + final Content content = new Content(this.contentCreator, this.contentSenders, this.contentName); content.setTransport(new S5BTransportInfo(this.transportId, new Element("candidate-used").setAttribute("cid", cid))); packet.addJingleContent(content); this.sentCandidate = true; @@ -1156,8 +1148,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple private void sendCandidateError() { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": sending candidate error"); JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO); - Content content = new Content(this.contentCreator, this.contentName); - content.setSenders(this.contentSenders); + Content content = new Content(this.contentCreator, this.contentSenders, this.contentName); content.setTransport(new S5BTransportInfo(this.transportId, new Element("candidate-error"))); packet.addJingleContent(content); this.sentCandidate = true; 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 e2832355db0bba6ee5f01176956207c158a0d67d..5cccafa5a7a3317c552dd0f8ba01d1b3a90e51dc 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -425,7 +425,7 @@ public class JingleRtpConnection extends AbstractJingleConnection final RtpContentMap restartContentMap, final boolean isOffer) throws ExecutionException, InterruptedException { - final SessionDescription sessionDescription = SessionDescription.of(restartContentMap); + final SessionDescription sessionDescription = SessionDescription.of(restartContentMap, !isInitiator()); final org.webrtc.SessionDescription.Type type = isOffer ? org.webrtc.SessionDescription.Type.OFFER @@ -444,7 +444,7 @@ public class JingleRtpConnection extends AbstractJingleConnection if (isOffer) { webRTCWrapper.setIsReadyToReceiveIceCandidates(false); final SessionDescription localSessionDescription = setLocalSessionDescription(); - setLocalContentMap(RtpContentMap.of(localSessionDescription)); + setLocalContentMap(RtpContentMap.of(localSessionDescription, isInitiator())); // We need to respond OK before sending any candidates respondOk(jinglePacket); webRTCWrapper.setIsReadyToReceiveIceCandidates(true); @@ -726,7 +726,7 @@ public class JingleRtpConnection extends AbstractJingleConnection this.storePeerDtlsSetup(contentMap.getDtlsSetup()); final SessionDescription sessionDescription; try { - sessionDescription = SessionDescription.of(contentMap); + sessionDescription = SessionDescription.of(contentMap, false); } catch (final IllegalArgumentException | NullPointerException e) { Log.d( Config.LOGTAG, @@ -763,7 +763,7 @@ public class JingleRtpConnection extends AbstractJingleConnection } final SessionDescription offer; try { - offer = SessionDescription.of(rtpContentMap); + offer = SessionDescription.of(rtpContentMap, true); } catch (final IllegalArgumentException | NullPointerException e) { Log.d( Config.LOGTAG, @@ -838,7 +838,7 @@ public class JingleRtpConnection extends AbstractJingleConnection final org.webrtc.SessionDescription webRTCSessionDescription) { final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); - final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription); + final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription, false); this.responderRtpContentMap = respondingRtpContentMap; storePeerDtlsSetup(respondingRtpContentMap.getDtlsSetup().flip()); webRTCWrapper.setIsReadyToReceiveIceCandidates(true); @@ -1289,7 +1289,7 @@ public class JingleRtpConnection extends AbstractJingleConnection final org.webrtc.SessionDescription webRTCSessionDescription, final State targetState) { final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); - final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription); + final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription, true); this.initiatorRtpContentMap = rtpContentMap; //TODO delay ready to receive ice until after session-init this.webRTCWrapper.setIsReadyToReceiveIceCandidates(true); @@ -1922,7 +1922,7 @@ public class JingleRtpConnection extends AbstractJingleConnection sendSessionTerminate(Reason.FAILED_APPLICATION, cause.getMessage()); return; } - final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription); + final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription, isInitiator()); final RtpContentMap transportInfo = rtpContentMap.transportInfo(); final JinglePacket jinglePacket = transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId); 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 bba44f9634bd476f3329ab19da28f2e8bac79433..f9c245b0e76dbfb73cda6ce3fc008ef61943447d 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java @@ -1,5 +1,6 @@ package eu.siacs.conversations.xmpp.jingle; +import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.collect.Collections2; @@ -15,6 +16,8 @@ import java.util.List; import java.util.Map; import java.util.Set; +import javax.annotation.Nonnull; + import eu.siacs.conversations.xmpp.jingle.stanzas.Content; import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription; import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo; @@ -58,13 +61,15 @@ public class RtpContentMap { return true; } - public static RtpContentMap of(final SessionDescription sessionDescription) { + public static RtpContentMap of( + final SessionDescription sessionDescription, final boolean isInitiator) { final ImmutableMap.Builder contentMapBuilder = new ImmutableMap.Builder<>(); for (SessionDescription.Media media : sessionDescription.media) { final String id = Iterables.getFirst(media.attributes.get("mid"), null); Preconditions.checkNotNull(id, "media has no mid"); - contentMapBuilder.put(id, DescriptionTransport.of(sessionDescription, media)); + contentMapBuilder.put( + id, DescriptionTransport.of(sessionDescription, isInitiator, media)); } final String groupAttribute = Iterables.getFirst(sessionDescription.attributes.get("group"), null); @@ -140,11 +145,16 @@ public class RtpContentMap { jinglePacket.addGroup(this.group); } for (Map.Entry entry : this.contents.entrySet()) { - final Content content = new Content(Content.Creator.INITIATOR, entry.getKey()); - if (entry.getValue().description != null) { - content.addChild(entry.getValue().description); + final DescriptionTransport descriptionTransport = entry.getValue(); + final Content content = + new Content( + Content.Creator.INITIATOR, + descriptionTransport.senders, + entry.getKey()); + if (descriptionTransport.description != null) { + content.addChild(descriptionTransport.description); } - content.addChild(entry.getValue().transport); + content.addChild(descriptionTransport.transport); jinglePacket.addJingleContent(content); } return jinglePacket; @@ -163,7 +173,10 @@ public class RtpContentMap { newTransportInfo.addChild(candidate); return new RtpContentMap( null, - ImmutableMap.of(contentName, new DescriptionTransport(null, newTransportInfo))); + ImmutableMap.of( + contentName, + new DescriptionTransport( + descriptionTransport.senders, null, newTransportInfo))); } RtpContentMap transportInfo() { @@ -171,7 +184,9 @@ public class RtpContentMap { null, Maps.transformValues( contents, - dt -> new DescriptionTransport(null, dt.transport.cloneWrapper()))); + dt -> + new DescriptionTransport( + dt.senders, null, dt.transport.cloneWrapper()))); } public IceUdpTransportInfo.Credentials getDistinctCredentials() { @@ -179,7 +194,8 @@ public class RtpContentMap { final IceUdpTransportInfo.Credentials credentials = Iterables.getFirst(allCredentials, null); if (allCredentials.size() == 1 && credentials != null) { - if (Strings.isNullOrEmpty(credentials.password) || Strings.isNullOrEmpty(credentials.ufrag)) { + if (Strings.isNullOrEmpty(credentials.password) + || Strings.isNullOrEmpty(credentials.ufrag)) { throw new IllegalStateException("Credentials are missing password or ufrag"); } return credentials; @@ -233,23 +249,45 @@ public class RtpContentMap { final ImmutableMap.Builder contentMapBuilder = new ImmutableMap.Builder<>(); for (final Map.Entry content : contents.entrySet()) { - final RtpDescription rtpDescription = content.getValue().description; - IceUdpTransportInfo transportInfo = content.getValue().transport; + final DescriptionTransport descriptionTransport = content.getValue(); + final RtpDescription rtpDescription = descriptionTransport.description; + final IceUdpTransportInfo transportInfo = descriptionTransport.transport; final IceUdpTransportInfo modifiedTransportInfo = transportInfo.modifyCredentials(credentials, setup); contentMapBuilder.put( content.getKey(), - new DescriptionTransport(rtpDescription, modifiedTransportInfo)); + new DescriptionTransport( + descriptionTransport.senders, rtpDescription, modifiedTransportInfo)); } return new RtpContentMap(this.group, contentMapBuilder.build()); } + public Diff diff(final RtpContentMap rtpContentMap) { + final Set existingContentIds = this.contents.keySet(); + final Set newContentIds = rtpContentMap.contents.keySet(); + return new Diff( + Sets.difference(newContentIds, existingContentIds), + Sets.difference(existingContentIds, newContentIds)); + } + + public boolean iceRestart(final RtpContentMap rtpContentMap) { + try { + return !getDistinctCredentials().equals(rtpContentMap.getDistinctCredentials()); + } catch (final IllegalStateException e) { + return false; + } + } + public static class DescriptionTransport { + public final Content.Senders senders; public final RtpDescription description; public final IceUdpTransportInfo transport; public DescriptionTransport( - final RtpDescription description, final IceUdpTransportInfo transport) { + final Content.Senders senders, + final RtpDescription description, + final IceUdpTransportInfo transport) { + this.senders = senders; this.description = description; this.transport = transport; } @@ -257,6 +295,7 @@ public class RtpContentMap { public static DescriptionTransport of(final Content content) { final GenericDescription description = content.getDescription(); final GenericTransportInfo transportInfo = content.getTransport(); + final Content.Senders senders = content.getSenders(); final RtpDescription rtpDescription; final IceUdpTransportInfo iceUdpTransportInfo; if (description == null) { @@ -274,22 +313,26 @@ public class RtpContentMap { "Content does not contain ICE-UDP transport"); } return new DescriptionTransport( - rtpDescription, OmemoVerifiedIceUdpTransportInfo.upgrade(iceUdpTransportInfo)); + senders, + rtpDescription, + OmemoVerifiedIceUdpTransportInfo.upgrade(iceUdpTransportInfo)); } - public static DescriptionTransport of( - final SessionDescription sessionDescription, final SessionDescription.Media media) { + private static DescriptionTransport of( + final SessionDescription sessionDescription, + final boolean isInitiator, + final SessionDescription.Media media) { + final Content.Senders senders = Content.Senders.of(media, isInitiator); final RtpDescription rtpDescription = RtpDescription.of(sessionDescription, media); final IceUdpTransportInfo transportInfo = IceUdpTransportInfo.of(sessionDescription, media); - return new DescriptionTransport(rtpDescription, transportInfo); + return new DescriptionTransport(senders, rtpDescription, transportInfo); } public static Map of(final Map contents) { return ImmutableMap.copyOf( Maps.transformValues( - contents, - content -> content == null ? null : of(content))); + contents, content -> content == null ? null : of(content))); } } @@ -304,4 +347,27 @@ public class RtpContentMap { super(message); } } + + public static final class Diff { + public final Set added; + public final Set removed; + + private Diff(final Set added, final Set removed) { + this.added = added; + this.removed = removed; + } + + public boolean hasModifications() { + return !this.added.isEmpty() || !this.removed.isEmpty(); + } + + @Override + @Nonnull + public String toString() { + return MoreObjects.toStringHelper(this) + .add("added", added) + .add("removed", removed) + .toString(); + } + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java index e113146b135ed5beabbd61cd49b1a91babca520b..eef7ae0da975ff0ac6b0579e0b5099844e6fdc41 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java @@ -3,6 +3,8 @@ package eu.siacs.conversations.xmpp.jingle; import android.util.Log; import android.util.Pair; +import androidx.annotation.NonNull; + import com.google.common.base.CharMatcher; import com.google.common.base.Joiner; import com.google.common.base.Strings; @@ -21,11 +23,12 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; public class SessionDescription { - public final static String LINE_DIVIDER = "\r\n"; - private final static String HARDCODED_MEDIA_PROTOCOL = "UDP/TLS/RTP/SAVPF"; //probably only true for DTLS-SRTP aka when we have a fingerprint - private final static int HARDCODED_MEDIA_PORT = 9; - private final static String HARDCODED_ICE_OPTIONS = "trickle"; - private final static String HARDCODED_CONNECTION = "IN IP4 0.0.0.0"; + public static final String LINE_DIVIDER = "\r\n"; + private static final String HARDCODED_MEDIA_PROTOCOL = + "UDP/TLS/RTP/SAVPF"; // probably only true for DTLS-SRTP aka when we have a fingerprint + private static final int HARDCODED_MEDIA_PORT = 9; + private static final String HARDCODED_ICE_OPTIONS = "trickle"; + private static final String HARDCODED_CONNECTION = "IN IP4 0.0.0.0"; public final int version; public final String name; @@ -33,8 +36,12 @@ public class SessionDescription { public final ArrayListMultimap attributes; public final List media; - - public SessionDescription(int version, String name, String connectionData, ArrayListMultimap attributes, List media) { + public SessionDescription( + int version, + String name, + String connectionData, + ArrayListMultimap attributes, + List media) { this.version = version; this.name = name; this.connectionData = connectionData; @@ -42,7 +49,8 @@ public class SessionDescription { this.media = media; } - private static void appendAttributes(StringBuilder s, ArrayListMultimap attributes) { + private static void appendAttributes( + StringBuilder s, ArrayListMultimap attributes) { for (Map.Entry attribute : attributes.entries()) { final String key = attribute.getKey(); final String value = attribute.getValue(); @@ -109,7 +117,6 @@ public class SessionDescription { } break; } - } if (currentMediaBuilder != null) { currentMediaBuilder.setAttributes(attributeMap); @@ -121,7 +128,7 @@ public class SessionDescription { return sessionDescriptionBuilder.createSessionDescription(); } - public static SessionDescription of(final RtpContentMap contentMap) { + public static SessionDescription of(final RtpContentMap contentMap, final boolean isInitiatorContentMap) { final SessionDescriptionBuilder sessionDescriptionBuilder = new SessionDescriptionBuilder(); final ArrayListMultimap attributeMap = ArrayListMultimap.create(); final ImmutableList.Builder mediaListBuilder = new ImmutableList.Builder<>(); @@ -129,12 +136,17 @@ public class SessionDescription { if (group != null) { final String semantics = group.getSemantics(); checkNoWhitespace(semantics, "group semantics value must not contain any whitespace"); - attributeMap.put("group", group.getSemantics() + " " + Joiner.on(' ').join(group.getIdentificationTags())); + attributeMap.put( + "group", + group.getSemantics() + + " " + + Joiner.on(' ').join(group.getIdentificationTags())); } attributeMap.put("msid-semantic", " WMS my-media-stream"); - for (final Map.Entry entry : contentMap.contents.entrySet()) { + for (final Map.Entry entry : + contentMap.contents.entrySet()) { final String name = entry.getKey(); RtpContentMap.DescriptionTransport descriptionTransport = entry.getValue(); RtpDescription description = descriptionTransport.description; @@ -143,19 +155,22 @@ public class SessionDescription { final String ufrag = transport.getAttribute("ufrag"); final String pwd = transport.getAttribute("pwd"); if (Strings.isNullOrEmpty(ufrag)) { - throw new IllegalArgumentException("Transport element is missing required ufrag attribute"); + throw new IllegalArgumentException( + "Transport element is missing required ufrag attribute"); } checkNoWhitespace(ufrag, "ufrag value must not contain any whitespaces"); mediaAttributes.put("ice-ufrag", ufrag); if (Strings.isNullOrEmpty(pwd)) { - throw new IllegalArgumentException("Transport element is missing required pwd attribute"); + throw new IllegalArgumentException( + "Transport element is missing required pwd attribute"); } checkNoWhitespace(pwd, "pwd value must not contain any whitespaces"); mediaAttributes.put("ice-pwd", pwd); mediaAttributes.put("ice-options", HARDCODED_ICE_OPTIONS); final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint(); if (fingerprint != null) { - mediaAttributes.put("fingerprint", fingerprint.getHash() + " " + fingerprint.getContent()); + mediaAttributes.put( + "fingerprint", fingerprint.getHash() + " " + fingerprint.getContent()); final IceUdpTransportInfo.Setup setup = fingerprint.getSetup(); if (setup != null) { mediaAttributes.put("setup", setup.toString().toLowerCase(Locale.ROOT)); @@ -174,37 +189,56 @@ public class SessionDescription { mediaAttributes.put("rtpmap", payloadType.toSdpAttribute()); final List parameters = payloadType.getParameters(); if (parameters.size() == 1) { - mediaAttributes.put("fmtp", RtpDescription.Parameter.toSdpString(id, parameters.get(0))); + mediaAttributes.put( + "fmtp", RtpDescription.Parameter.toSdpString(id, parameters.get(0))); } else if (parameters.size() > 0) { - mediaAttributes.put("fmtp", RtpDescription.Parameter.toSdpString(id, parameters)); + mediaAttributes.put( + "fmtp", RtpDescription.Parameter.toSdpString(id, parameters)); } - for (RtpDescription.FeedbackNegotiation feedbackNegotiation : payloadType.getFeedbackNegotiations()) { + for (RtpDescription.FeedbackNegotiation feedbackNegotiation : + payloadType.getFeedbackNegotiations()) { final String type = feedbackNegotiation.getType(); final String subtype = feedbackNegotiation.getSubType(); if (Strings.isNullOrEmpty(type)) { - throw new IllegalArgumentException("a feedback for payload-type " + id + " negotiation is missing type"); + throw new IllegalArgumentException( + "a feedback for payload-type " + + id + + " negotiation is missing type"); } - checkNoWhitespace(type, "feedback negotiation type must not contain whitespace"); - mediaAttributes.put("rtcp-fb", id + " " + type + (Strings.isNullOrEmpty(subtype) ? "" : " " + subtype)); + checkNoWhitespace( + type, "feedback negotiation type must not contain whitespace"); + mediaAttributes.put( + "rtcp-fb", + id + + " " + + type + + (Strings.isNullOrEmpty(subtype) ? "" : " " + subtype)); } - for (RtpDescription.FeedbackNegotiationTrrInt feedbackNegotiationTrrInt : payloadType.feedbackNegotiationTrrInts()) { - mediaAttributes.put("rtcp-fb", id + " trr-int " + feedbackNegotiationTrrInt.getValue()); + for (RtpDescription.FeedbackNegotiationTrrInt feedbackNegotiationTrrInt : + payloadType.feedbackNegotiationTrrInts()) { + mediaAttributes.put( + "rtcp-fb", id + " trr-int " + feedbackNegotiationTrrInt.getValue()); } } - for (RtpDescription.FeedbackNegotiation feedbackNegotiation : description.getFeedbackNegotiations()) { + for (RtpDescription.FeedbackNegotiation feedbackNegotiation : + description.getFeedbackNegotiations()) { final String type = feedbackNegotiation.getType(); final String subtype = feedbackNegotiation.getSubType(); if (Strings.isNullOrEmpty(type)) { throw new IllegalArgumentException("a feedback negotiation is missing type"); } checkNoWhitespace(type, "feedback negotiation type must not contain whitespace"); - mediaAttributes.put("rtcp-fb", "* " + type + (Strings.isNullOrEmpty(subtype) ? "" : " " + subtype)); + mediaAttributes.put( + "rtcp-fb", + "* " + type + (Strings.isNullOrEmpty(subtype) ? "" : " " + subtype)); } - for (final RtpDescription.FeedbackNegotiationTrrInt feedbackNegotiationTrrInt : description.feedbackNegotiationTrrInts()) { + for (final RtpDescription.FeedbackNegotiationTrrInt feedbackNegotiationTrrInt : + description.feedbackNegotiationTrrInts()) { mediaAttributes.put("rtcp-fb", "* trr-int " + feedbackNegotiationTrrInt.getValue()); } - for (final RtpDescription.RtpHeaderExtension extension : description.getHeaderExtensions()) { + for (final RtpDescription.RtpHeaderExtension extension : + description.getHeaderExtensions()) { final String id = extension.getId(); final String uri = extension.getUri(); if (Strings.isNullOrEmpty(id)) { @@ -218,7 +252,8 @@ public class SessionDescription { mediaAttributes.put("extmap", id + " " + uri); } - if (description.hasChild("extmap-allow-mixed", Namespace.JINGLE_RTP_HEADER_EXTENSIONS)) { + if (description.hasChild( + "extmap-allow-mixed", Namespace.JINGLE_RTP_HEADER_EXTENSIONS)) { mediaAttributes.put("extmap-allow-mixed", ""); } @@ -226,13 +261,16 @@ public class SessionDescription { final String semantics = sourceGroup.getSemantics(); final List groups = sourceGroup.getSsrcs(); if (Strings.isNullOrEmpty(semantics)) { - throw new IllegalArgumentException("A SSRC group is missing semantics attribute"); + throw new IllegalArgumentException( + "A SSRC group is missing semantics attribute"); } checkNoWhitespace(semantics, "source group semantics must not contain whitespace"); if (groups.size() == 0) { throw new IllegalArgumentException("A SSRC group is missing SSRC ids"); } - mediaAttributes.put("ssrc-group", String.format("%s %s", semantics, Joiner.on(' ').join(groups))); + mediaAttributes.put( + "ssrc-group", + String.format("%s %s", semantics, Joiner.on(' ').join(groups))); } for (final RtpDescription.Source source : description.getSources()) { for (final RtpDescription.Source.Parameter parameter : source.getParameters()) { @@ -240,14 +278,18 @@ public class SessionDescription { final String parameterName = parameter.getParameterName(); final String parameterValue = parameter.getParameterValue(); if (Strings.isNullOrEmpty(id)) { - throw new IllegalArgumentException("A source specific media attribute is missing the id"); + throw new IllegalArgumentException( + "A source specific media attribute is missing the id"); } - checkNoWhitespace(id, "A source specific media attributes must not contain whitespaces"); + checkNoWhitespace( + id, "A source specific media attributes must not contain whitespaces"); if (Strings.isNullOrEmpty(parameterName)) { - throw new IllegalArgumentException("A source specific media attribute is missing its name"); + throw new IllegalArgumentException( + "A source specific media attribute is missing its name"); } if (Strings.isNullOrEmpty(parameterValue)) { - throw new IllegalArgumentException("A source specific media attribute is missing its value"); + throw new IllegalArgumentException( + "A source specific media attribute is missing its value"); } mediaAttributes.put("ssrc", id + " " + parameterName + ":" + parameterValue); } @@ -255,14 +297,14 @@ public class SessionDescription { mediaAttributes.put("mid", name); - //random additional attributes - mediaAttributes.put("rtcp", "9 IN IP4 0.0.0.0"); - mediaAttributes.put("sendrecv", ""); - + mediaAttributes.put(descriptionTransport.senders.asMediaAttribute(isInitiatorContentMap), ""); if (description.hasChild("rtcp-mux", Namespace.JINGLE_APPS_RTP)) { mediaAttributes.put("rtcp-mux", ""); } + // random additional attributes + mediaAttributes.put("rtcp", "9 IN IP4 0.0.0.0"); + final MediaBuilder mediaBuilder = new MediaBuilder(); mediaBuilder.setMedia(description.getMedia().toString().toLowerCase(Locale.ROOT)); mediaBuilder.setConnectionData(HARDCODED_CONNECTION); @@ -271,7 +313,6 @@ public class SessionDescription { mediaBuilder.setAttributes(mediaAttributes); mediaBuilder.setFormats(formatBuilder.build()); mediaListBuilder.add(mediaBuilder.createMedia()); - } sessionDescriptionBuilder.setVersion(0); sessionDescriptionBuilder.setName("-"); @@ -317,17 +358,33 @@ public class SessionDescription { } } + @NonNull @Override public String toString() { - final StringBuilder s = new StringBuilder() - .append("v=").append(version).append(LINE_DIVIDER) - //TODO randomize or static - .append("o=- 8770656990916039506 2 IN IP4 127.0.0.1").append(LINE_DIVIDER) //what ever that means - .append("s=").append(name).append(LINE_DIVIDER) - .append("t=0 0").append(LINE_DIVIDER); + final StringBuilder s = + new StringBuilder() + .append("v=") + .append(version) + .append(LINE_DIVIDER) + // TODO randomize or static + .append("o=- 8770656990916039506 2 IN IP4 127.0.0.1") + .append(LINE_DIVIDER) // what ever that means + .append("s=") + .append(name) + .append(LINE_DIVIDER) + .append("t=0 0") + .append(LINE_DIVIDER); appendAttributes(s, attributes); for (Media media : this.media) { - s.append("m=").append(media.media).append(' ').append(media.port).append(' ').append(media.protocol).append(' ').append(Joiner.on(' ').join(media.formats)).append(LINE_DIVIDER); + s.append("m=") + .append(media.media) + .append(' ') + .append(media.port) + .append(' ') + .append(media.protocol) + .append(' ') + .append(Joiner.on(' ').join(media.formats)) + .append(LINE_DIVIDER); s.append("c=").append(media.connectionData).append(LINE_DIVIDER); appendAttributes(s, media.attributes); } @@ -342,7 +399,13 @@ public class SessionDescription { public final String connectionData; public final ArrayListMultimap attributes; - public Media(String media, int port, String protocol, List formats, String connectionData, ArrayListMultimap attributes) { + public Media( + String media, + int port, + String protocol, + List formats, + String connectionData, + ArrayListMultimap attributes) { this.media = media; this.port = port; this.protocol = protocol; @@ -351,5 +414,4 @@ public class SessionDescription { this.attributes = attributes; } } - } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java index e21c3896836899ab55da2e7eaafca9e5f40b6811..96251529314ab791cc4b1d8a7caffd50881eb14c 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java @@ -1,20 +1,27 @@ package eu.siacs.conversations.xmpp.jingle.stanzas; +import android.util.Log; + import androidx.annotation.NonNull; import com.google.common.base.Preconditions; +import com.google.common.base.Strings; import java.util.Locale; +import java.util.Set; +import eu.siacs.conversations.Config; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; +import eu.siacs.conversations.xmpp.jingle.SessionDescription; public class Content extends Element { - public Content(final Creator creator, final String name) { + public Content(final Creator creator, final Senders senders, final String name) { super("content", Namespace.JINGLE); this.setAttribute("creator", creator.toString()); this.setAttribute("name", name); + this.setSenders(senders); } private Content() { @@ -38,11 +45,17 @@ public class Content extends Element { } public Senders getSenders() { + final String attribute = getAttribute("senders"); + if (Strings.isNullOrEmpty(attribute)) { + return Senders.BOTH; + } return Senders.of(getAttribute("senders")); } - public void setSenders(Senders senders) { - this.setAttribute("senders", senders.toString()); + public void setSenders(final Senders senders) { + if (senders != null && senders != Senders.BOTH) { + this.setAttribute("senders", senders.toString()); + } } public GenericDescription getDescription() { @@ -51,9 +64,7 @@ public class Content extends Element { return null; } final String namespace = description.getNamespace(); - if (FileTransferDescription.NAMESPACES.contains(namespace)) { - return FileTransferDescription.upgrade(description); - } else if (Namespace.JINGLE_APPS_RTP.equals(namespace)) { + if (Namespace.JINGLE_APPS_RTP.equals(namespace)) { return RtpDescription.upgrade(description); } else { return GenericDescription.upgrade(description); @@ -73,11 +84,7 @@ public class Content extends Element { public GenericTransportInfo getTransport() { final Element transport = this.findChild("transport"); final String namespace = transport == null ? null : transport.getNamespace(); - if (Namespace.JINGLE_TRANSPORTS_IBB.equals(namespace)) { - return IbbTransportInfo.upgrade(transport); - } else if (Namespace.JINGLE_TRANSPORTS_S5B.equals(namespace)) { - return S5BTransportInfo.upgrade(transport); - } else if (Namespace.JINGLE_TRANSPORT_ICE_UDP.equals(namespace)) { + if (Namespace.JINGLE_TRANSPORT_ICE_UDP.equals(namespace)) { return IceUdpTransportInfo.upgrade(transport); } else if (transport != null) { return GenericTransportInfo.upgrade(transport); @@ -91,7 +98,8 @@ public class Content extends Element { } public enum Creator { - INITIATOR, RESPONDER; + INITIATOR, + RESPONDER; public static Creator of(final String value) { return Creator.valueOf(value.toUpperCase(Locale.ROOT)); @@ -105,16 +113,56 @@ public class Content extends Element { } public enum Senders { - BOTH, INITIATOR, NONE, RESPONDER; + BOTH, + INITIATOR, + NONE, + RESPONDER; public static Senders of(final String value) { return Senders.valueOf(value.toUpperCase(Locale.ROOT)); } + public static Senders of(final SessionDescription.Media media, final boolean initiator) { + final Set attributes = media.attributes.keySet(); + if (attributes.contains("sendrecv")) { + return BOTH; + } else if (attributes.contains("inactive")) { + return NONE; + } else if (attributes.contains("sendonly")) { + return initiator ? INITIATOR : RESPONDER; + } else if (attributes.contains("recvonly")) { + return initiator ? RESPONDER : INITIATOR; + } + Log.w(Config.LOGTAG,"assuming default value for senders"); + // If none of the attributes "sendonly", "recvonly", "inactive", and "sendrecv" is + // present, "sendrecv" SHOULD be assumed as the default + // https://www.rfc-editor.org/rfc/rfc4566 + return BOTH; + } + @Override @NonNull public String toString() { return super.toString().toLowerCase(Locale.ROOT); } + + public String asMediaAttribute(final boolean initiator) { + final boolean responder = !initiator; + if (this == Content.Senders.BOTH) { + return "sendrecv"; + } else if (this == Content.Senders.NONE) { + return "inactive"; + } else if ((initiator && this == Content.Senders.INITIATOR) + || (responder && this == Content.Senders.RESPONDER)) { + return "sendonly"; + } else if ((initiator && this == Content.Senders.RESPONDER) + || (responder && this == Content.Senders.INITIATOR)) { + return "recvonly"; + } else { + throw new IllegalStateException( + String.format( + "illegal combination of initiator=%s and %s", initiator, this)); + } + } } } From 9897fa3a456f11b5603a662a3367a817e94ec3ba Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 21 Nov 2022 09:10:01 +0100 Subject: [PATCH 087/101] rename initiateIceRestart to renegotiate to handle content adds --- .../xmpp/jingle/JingleRtpConnection.java | 47 +++++++++++++++++-- 1 file changed, 44 insertions(+), 3 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 5cccafa5a7a3317c552dd0f8ba01d1b3a90e51dc..0c68f663c01c35fc78f600b485db97d3fc40d8a2 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -7,12 +7,14 @@ import androidx.annotation.Nullable; import com.google.common.base.Optional; import com.google.common.base.Preconditions; +import com.google.common.base.Predicates; import com.google.common.base.Stopwatch; 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; +import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.common.primitives.Ints; import com.google.common.util.concurrent.FutureCallback; @@ -499,6 +501,10 @@ public class JingleRtpConnection extends AbstractJingleConnection return isInitiator() ? this.responderRtpContentMap : this.initiatorRtpContentMap; } + private RtpContentMap getLocalContentMap() { + return isInitiator() ? this.initiatorRtpContentMap : this.responderRtpContentMap; + } + private List toIdentificationTags(final RtpContentMap rtpContentMap) { final Group originalGroup = rtpContentMap.group; final List identificationTags = @@ -1906,11 +1912,11 @@ public class JingleRtpConnection extends AbstractJingleConnection @Override public void onRenegotiationNeeded() { - this.webRTCWrapper.execute(this::initiateIceRestart); + this.webRTCWrapper.execute(this::renegotiate); } - private void initiateIceRestart() { - // TODO discover new TURN/STUN credentials + private void renegotiate() { + //TODO needs to be called only for ice restarts; maybe in the call to restartICe() this.stateHistory.clear(); this.webRTCWrapper.setIsReadyToReceiveIceCandidates(false); final SessionDescription sessionDescription; @@ -1919,10 +1925,41 @@ public class JingleRtpConnection extends AbstractJingleConnection } catch (final Exception e) { final Throwable cause = Throwables.getRootCause(e); Log.d(Config.LOGTAG, "failed to renegotiate", cause); + webRTCWrapper.close(); sendSessionTerminate(Reason.FAILED_APPLICATION, cause.getMessage()); return; } final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription, isInitiator()); + final RtpContentMap currentContentMap = getLocalContentMap(); + final boolean iceRestart = currentContentMap.iceRestart(rtpContentMap); + final RtpContentMap.Diff diff = currentContentMap.diff(rtpContentMap); + + Log.d( + Config.LOGTAG, + id.getAccount().getJid().asBareJid() + + ": renegotiate. iceRestart=" + + iceRestart + + " content id diff=" + + diff); + + if (diff.hasModifications() && iceRestart) { + webRTCWrapper.close(); + sendSessionTerminate(Reason.FAILED_APPLICATION, "WebRTC unexpectedly tried to modify content and transport at once"); + return; + } + + if (iceRestart) { + initiateIceRestart(rtpContentMap); + return; + } + + if (diff.added.size() > 0) { + sendContentAdd(rtpContentMap); + } + + } + + private void initiateIceRestart(final RtpContentMap rtpContentMap) { final RtpContentMap transportInfo = rtpContentMap.transportInfo(); final JinglePacket jinglePacket = transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId); @@ -1952,6 +1989,10 @@ public class JingleRtpConnection extends AbstractJingleConnection }); } + private void sendContentAdd(final RtpContentMap rtpContentMap) { + + } + private void setLocalContentMap(final RtpContentMap rtpContentMap) { if (isInitiator()) { this.initiatorRtpContentMap = rtpContentMap; From e2f98f6bbc819bbacd96d7b4e69aef7f740de745 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 22 Nov 2022 10:13:07 +0100 Subject: [PATCH 088/101] ensure cc-ed proceed is equivalent to accept --- .../xmpp/jingle/JingleRtpConnection.java | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 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 0c68f663c01c35fc78f600b485db97d3fc40d8a2..ad5aeac0e8b0b831f7e4c175dcf0633f96acc9ff 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -952,16 +952,7 @@ public class JingleRtpConnection extends AbstractJingleConnection from.asBareJid().equals(id.account.getJid().asBareJid()); if (originatedFromMyself) { if (transition(State.ACCEPTED)) { - if (serverMsgId != null) { - this.message.setServerMsgId(serverMsgId); - } - this.message.setTime(timestamp); - this.message.setCarbon(true); // indicate that call was accepted on other device - this.writeLogMessageSuccess(0); - this.xmppConnectionService - .getNotificationService() - .cancelIncomingCallNotification(); - this.finish(); + acceptedOnOtherDevice(serverMsgId, timestamp); } else { Log.d( Config.LOGTAG, @@ -976,6 +967,19 @@ public class JingleRtpConnection extends AbstractJingleConnection } } + private void acceptedOnOtherDevice(final String serverMsgId, final long timestamp) { + if (serverMsgId != null) { + this.message.setServerMsgId(serverMsgId); + } + this.message.setTime(timestamp); + this.message.setCarbon(true); // indicate that call was accepted on other device + this.writeLogMessageSuccess(0); + this.xmppConnectionService + .getNotificationService() + .cancelIncomingCallNotification(); + this.finish(); + } + private void receiveReject(final Jid from, final String serverMsgId, final long timestamp) { final boolean originatedFromMyself = from.asBareJid().equals(id.account.getJid().asBareJid()); @@ -1173,11 +1177,8 @@ public class JingleRtpConnection extends AbstractJingleConnection id.account.getJid().asBareJid() + ": moved session with " + id.with - + " into state accepted after received carbon copied procced"); - this.xmppConnectionService - .getNotificationService() - .cancelIncomingCallNotification(); - this.finish(); + + " into state accepted after received carbon copied proceed"); + acceptedOnOtherDevice(serverMsgId, timestamp); } } else { Log.d( From f4be142e4d70b90f15bd0793e2870e9ceb13629b Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 22 Nov 2022 10:13:48 +0100 Subject: [PATCH 089/101] add helper methods for content modification to RtpContentMap --- .../xmpp/jingle/RtpContentMap.java | 80 ++++++++++++++++++- .../xmpp/jingle/WebRTCWrapper.java | 9 ++- .../jingle/stanzas/IceUdpTransportInfo.java | 70 +++++++++++----- 3 files changed, 137 insertions(+), 22 deletions(-) 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 f9c245b0e76dbfb73cda6ce3fc008ef61943447d..7af1469cf5f2e385efef40a3c6f5144207b7005b 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java @@ -1,7 +1,9 @@ package eu.siacs.conversations.xmpp.jingle; import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; import com.google.common.base.Preconditions; +import com.google.common.base.Predicates; import com.google.common.base.Strings; import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; @@ -236,6 +238,23 @@ public class RtpContentMap { throw new IllegalStateException("Content map doesn't have distinct DTLS setup"); } + private DTLS getDistinctDtls() { + final Set dtlsSet = + ImmutableSet.copyOf( + Collections2.transform( + contents.values(), + dt -> { + final IceUdpTransportInfo.Fingerprint fp = + dt.transport.getFingerprint(); + return new DTLS(fp.getHash(), fp.getSetup(), fp.getContent()); + })); + final DTLS dtls = Iterables.getFirst(dtlsSet, null); + if (dtlsSet.size() == 1 && dtls != null) { + return dtls; + } + throw new IllegalStateException("Content map doesn't have distinct DTLS setup"); + } + public boolean emptyCandidates() { int count = 0; for (DescriptionTransport descriptionTransport : contents.values()) { @@ -262,12 +281,22 @@ public class RtpContentMap { return new RtpContentMap(this.group, contentMapBuilder.build()); } + public RtpContentMap toContentModification(final Collection modifications) { + return new RtpContentMap( + this.group, + Maps.transformValues( + Maps.filterKeys(contents, Predicates.in(modifications)), + dt -> + new DescriptionTransport( + dt.senders, dt.description, IceUdpTransportInfo.STUB))); + } + public Diff diff(final RtpContentMap rtpContentMap) { final Set existingContentIds = this.contents.keySet(); final Set newContentIds = rtpContentMap.contents.keySet(); return new Diff( - Sets.difference(newContentIds, existingContentIds), - Sets.difference(existingContentIds, newContentIds)); + ImmutableSet.copyOf(Sets.difference(newContentIds, existingContentIds)), + ImmutableSet.copyOf(Sets.difference(existingContentIds, newContentIds))); } public boolean iceRestart(final RtpContentMap rtpContentMap) { @@ -278,6 +307,26 @@ public class RtpContentMap { } } + public RtpContentMap addContent(final RtpContentMap modification) { + final IceUdpTransportInfo.Credentials credentials = getDistinctCredentials(); + final DTLS dtls = getDistinctDtls(); + final IceUdpTransportInfo iceUdpTransportInfo = + IceUdpTransportInfo.of(credentials, dtls.setup, dtls.hash, dtls.fingerprint); + final Map combined = + new ImmutableMap.Builder() + .putAll(contents) + .putAll( + Maps.transformValues( + modification.contents, + dt -> + new DescriptionTransport( + dt.senders, + dt.description, + iceUdpTransportInfo))) + .build(); + return new RtpContentMap(modification.group, combined); + } + public static class DescriptionTransport { public final Content.Senders senders; public final RtpDescription description; @@ -370,4 +419,31 @@ public class RtpContentMap { .toString(); } } + + public static final class DTLS { + public final String hash; + public final IceUdpTransportInfo.Setup setup; + public final String fingerprint; + + private DTLS(String hash, IceUdpTransportInfo.Setup setup, String fingerprint) { + this.hash = hash; + this.setup = setup; + this.fingerprint = fingerprint; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DTLS dtls = (DTLS) o; + return Objects.equal(hash, dtls.hash) + && setup == dtls.setup + && Objects.equal(fingerprint, dtls.fingerprint); + } + + @Override + public int hashCode() { + return Objects.hashCode(hash, setup, fingerprint); + } + } } 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 de26f1afc1bbfb44c0feee7b104adce58f15e5a8..53b7de1e0d85207f23eac73b6ff6849b8f613879 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -186,15 +186,22 @@ public class WebRTCWrapper { } @Override - public void onTrack(RtpTransceiver transceiver) { + public void onTrack(final RtpTransceiver transceiver) { Log.d( EXTENDED_LOGGING_TAG, "onTrack(mid=" + transceiver.getMid() + ",media=" + transceiver.getMediaType() + + ",direction=" + + transceiver.getDirection() + ")"); } + + @Override + public void onRemoveTrack(final RtpReceiver receiver) { + Log.d(EXTENDED_LOGGING_TAG, "onRemoveTrack(" + receiver.id() + ")"); + } }; @Nullable private PeerConnectionFactory peerConnectionFactory = null; @Nullable private PeerConnection peerConnection = null; 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 ee8d12b700c120d188cb5840cec38996040f26fc..432333090b28cc775076808388633f813cf2b596 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 @@ -12,8 +12,6 @@ import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; -import java.util.ArrayList; -import java.util.Collection; import java.util.HashMap; import java.util.Hashtable; import java.util.LinkedHashMap; @@ -28,23 +26,29 @@ import eu.siacs.conversations.xmpp.jingle.SessionDescription; public class IceUdpTransportInfo extends GenericTransportInfo { + public static final IceUdpTransportInfo STUB = new IceUdpTransportInfo(); + public IceUdpTransportInfo() { super("transport", Namespace.JINGLE_TRANSPORT_ICE_UDP); } public static IceUdpTransportInfo upgrade(final Element element) { - Preconditions.checkArgument("transport".equals(element.getName()), "Name of provided element is not transport"); - Preconditions.checkArgument(Namespace.JINGLE_TRANSPORT_ICE_UDP.equals(element.getNamespace()), "Element does not match ice-udp transport namespace"); + Preconditions.checkArgument( + "transport".equals(element.getName()), "Name of provided element is not transport"); + Preconditions.checkArgument( + Namespace.JINGLE_TRANSPORT_ICE_UDP.equals(element.getNamespace()), + "Element does not match ice-udp transport namespace"); final IceUdpTransportInfo transportInfo = new IceUdpTransportInfo(); transportInfo.setAttributes(element.getAttributes()); transportInfo.setChildren(element.getChildren()); return transportInfo; } - public static IceUdpTransportInfo of(SessionDescription sessionDescription, SessionDescription.Media media) { + public static IceUdpTransportInfo of( + SessionDescription sessionDescription, SessionDescription.Media media) { final String ufrag = Iterables.getFirst(media.attributes.get("ice-ufrag"), null); final String pwd = Iterables.getFirst(media.attributes.get("ice-pwd"), null); - IceUdpTransportInfo iceUdpTransportInfo = new IceUdpTransportInfo(); + final IceUdpTransportInfo iceUdpTransportInfo = new IceUdpTransportInfo(); if (ufrag != null) { iceUdpTransportInfo.setAttribute("ufrag", ufrag); } @@ -56,7 +60,15 @@ public class IceUdpTransportInfo extends GenericTransportInfo { iceUdpTransportInfo.addChild(fingerprint); } return iceUdpTransportInfo; + } + public static IceUdpTransportInfo of( + final Credentials credentials, final Setup setup, final String hash, final String fingerprint) { + final IceUdpTransportInfo iceUdpTransportInfo = new IceUdpTransportInfo(); + iceUdpTransportInfo.addChild(Fingerprint.of(setup, hash, fingerprint)); + iceUdpTransportInfo.setAttribute("ufrag", credentials.ufrag); + iceUdpTransportInfo.setAttribute("pwd", credentials.password); + return iceUdpTransportInfo; } public Fingerprint getFingerprint() { @@ -91,7 +103,8 @@ public class IceUdpTransportInfo extends GenericTransportInfo { transportInfo.setAttribute("ufrag", credentials.ufrag); transportInfo.setAttribute("pwd", credentials.password); for (final Element child : getChildren()) { - if (child.getName().equals("fingerprint") && Namespace.JINGLE_APPS_DTLS.equals(child.getNamespace())) { + if (child.getName().equals("fingerprint") + && Namespace.JINGLE_APPS_DTLS.equals(child.getNamespace())) { final Fingerprint fingerprint = new Fingerprint(); fingerprint.setAttributes(new Hashtable<>(child.getAttributes())); fingerprint.setContent(child.getContent()); @@ -231,7 +244,7 @@ public class IceUdpTransportInfo extends GenericTransportInfo { return getAttributeAsInt("rel-port"); } - public String getType() { //TODO might be converted to enum + public String getType() { // TODO might be converted to enum return getAttribute("type"); } @@ -256,7 +269,8 @@ public class IceUdpTransportInfo extends GenericTransportInfo { checkNotNullNoWhitespace(protocol, "protocol"); final String transport = protocol.toLowerCase(Locale.ROOT); if (!"udp".equals(transport)) { - throw new IllegalArgumentException(String.format("'%s' is not a supported protocol", transport)); + throw new IllegalArgumentException( + String.format("'%s' is not a supported protocol", transport)); } final String priority = this.getAttribute("priority"); checkNotNullNoWhitespace(priority, "priority"); @@ -284,7 +298,15 @@ public class IceUdpTransportInfo extends GenericTransportInfo { if (ufrag != null) { additionalParameter.put("ufrag", ufrag); } - final String parametersString = Joiner.on(' ').join(Collections2.transform(additionalParameter.entrySet(), input -> String.format("%s %s", input.getKey(), input.getValue()))); + final String parametersString = + Joiner.on(' ') + .join( + Collections2.transform( + additionalParameter.entrySet(), + input -> + String.format( + "%s %s", + input.getKey(), input.getValue()))); return String.format( "candidate:%s %s %s %s %s %s %s", foundation, @@ -293,20 +315,19 @@ public class IceUdpTransportInfo extends GenericTransportInfo { priority, connectionAddress, port, - parametersString - - ); + parametersString); } } private static void checkNotNullNoWhitespace(final String value, final String name) { if (Strings.isNullOrEmpty(value)) { - throw new IllegalArgumentException(String.format("Parameter %s is missing or empty", name)); + throw new IllegalArgumentException( + String.format("Parameter %s is missing or empty", name)); } - SessionDescription.checkNoWhitespace(value, String.format("Parameter %s contains white spaces", name)); + SessionDescription.checkNoWhitespace( + value, String.format("Parameter %s contains white spaces", name)); } - public static class Fingerprint extends Element { private Fingerprint() { @@ -340,11 +361,20 @@ public class IceUdpTransportInfo extends GenericTransportInfo { return null; } - public static Fingerprint of(final SessionDescription sessionDescription, final SessionDescription.Media media) { + public static Fingerprint of( + final SessionDescription sessionDescription, final SessionDescription.Media media) { final Fingerprint fingerprint = of(media.attributes); return fingerprint == null ? of(sessionDescription.attributes) : fingerprint; } + private static Fingerprint of(final Setup setup, final String hash, final String content) { + final Fingerprint fingerprint = new Fingerprint(); + fingerprint.setContent(content); + fingerprint.setAttribute("hash", hash); + fingerprint.setAttribute("setup", setup.toString().toLowerCase(Locale.ROOT)); + return fingerprint; + } + public String getHash() { return this.getAttribute("hash"); } @@ -356,7 +386,9 @@ public class IceUdpTransportInfo extends GenericTransportInfo { } public enum Setup { - ACTPASS, PASSIVE, ACTIVE; + ACTPASS, + PASSIVE, + ACTIVE; public static Setup of(String setup) { try { @@ -373,7 +405,7 @@ public class IceUdpTransportInfo extends GenericTransportInfo { if (this == ACTIVE) { return PASSIVE; } - throw new IllegalStateException(this.name()+" can not be flipped"); + throw new IllegalStateException(this.name() + " can not be flipped"); } } } From 63501adc45804ced79486fe7e7ff32213f38ef49 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 25 Nov 2022 08:50:58 +0100 Subject: [PATCH 090/101] trim xmpp address after user input --- src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java | 2 +- .../eu/siacs/conversations/ui/StartConversationActivity.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java b/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java index 15ebdb0b7b9a110cc3a079fa07ab1ab4b6e6cded..9ffd9c673343d10f6da3448d1668bb4bca48630c 100644 --- a/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java +++ b/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java @@ -170,7 +170,7 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected } final Jid contactJid; try { - contactJid = Jid.ofEscaped(binding.jid.getText().toString()); + contactJid = Jid.ofEscaped(binding.jid.getText().toString().trim()); } catch (final IllegalArgumentException e) { binding.jidLayout.setError(getActivity().getString(R.string.invalid_jid)); return; diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java index 91807295b9360aaef75e335136b13d75353ff484..7a2ffc0d85f41fcbcce6fe382fa5a04aff097a08 100644 --- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java @@ -1054,7 +1054,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne if (account == null) { return; } - final String input = jid.getText().toString(); + final String input = jid.getText().toString().trim(); Jid conferenceJid; try { conferenceJid = Jid.ofEscaped(input); From 4e8ceadfbf9f38cc9a53b7e912c9a7eb926dc2df Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 28 Nov 2022 08:59:23 +0100 Subject: [PATCH 091/101] prepare JingleRtpConnection for content-adds --- .../services/AppRTCAudioManager.java | 23 +- .../xmpp/jingle/ContentAddition.java | 88 +++ .../xmpp/jingle/JingleRtpConnection.java | 536 +++++++++++++++++- .../conversations/xmpp/jingle/Media.java | 15 + .../xmpp/jingle/RtpContentMap.java | 69 ++- .../xmpp/jingle/RtpEndUserState.java | 1 + .../xmpp/jingle/SessionDescription.java | 2 +- .../xmpp/jingle/ToneManager.java | 31 +- .../xmpp/jingle/TrackWrapper.java | 53 +- .../xmpp/jingle/WebRTCWrapper.java | 77 ++- .../xmpp/jingle/stanzas/RtpDescription.java | 136 +++-- 11 files changed, 921 insertions(+), 110 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/ContentAddition.java diff --git a/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java b/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java index e1fe854ff4a2502ecbe4f909c3571435beff6d73..3bed4eaba32604bd91ef082027a5f5e225fedb20 100644 --- a/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java +++ b/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java @@ -33,6 +33,7 @@ import java.util.concurrent.CountDownLatch; import eu.siacs.conversations.Config; import eu.siacs.conversations.utils.AppRTCUtils; +import eu.siacs.conversations.xmpp.jingle.Media; /** * AppRTCAudioManager manages all audio related parts of the AppRTC demo. @@ -44,7 +45,7 @@ public class AppRTCAudioManager { private final Context apprtcContext; // Contains speakerphone setting: auto, true or false @Nullable - private final SpeakerPhonePreference speakerPhonePreference; + private SpeakerPhonePreference speakerPhonePreference; // Handles all tasks related to Bluetooth headset devices. private final AppRTCBluetoothManager bluetoothManager; @Nullable @@ -110,6 +111,16 @@ public class AppRTCAudioManager { AppRTCUtils.logDeviceInfo(Config.LOGTAG); } + public void switchSpeakerPhonePreference(final SpeakerPhonePreference speakerPhonePreference) { + this.speakerPhonePreference = speakerPhonePreference; + if (speakerPhonePreference == SpeakerPhonePreference.EARPIECE && hasEarpiece()) { + defaultAudioDevice = AudioDevice.EARPIECE; + } else { + defaultAudioDevice = AudioDevice.SPEAKER_PHONE; + } + updateAudioDeviceState(); + } + /** * Construction. */ @@ -587,7 +598,15 @@ public class AppRTCAudioManager { } public enum SpeakerPhonePreference { - AUTO, EARPIECE, SPEAKER + AUTO, EARPIECE, SPEAKER; + + public static SpeakerPhonePreference of(final Set media) { + if (media.contains(Media.VIDEO)) { + return SPEAKER; + } else { + return EARPIECE; + } + } } /** diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/ContentAddition.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/ContentAddition.java new file mode 100644 index 0000000000000000000000000000000000000000..97bf802fd7fea0b1a97485fb640f7af3aa6fa7e8 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/ContentAddition.java @@ -0,0 +1,88 @@ +package eu.siacs.conversations.xmpp.jingle; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; +import com.google.common.collect.Collections2; +import com.google.common.collect.ImmutableSet; + +import java.util.Set; + +import eu.siacs.conversations.xmpp.jingle.stanzas.Content; + +public final class ContentAddition { + + public final Direction direction; + public final Set

summary; + + private ContentAddition(Direction direction, Set summary) { + this.direction = direction; + this.summary = summary; + } + + public Set media() { + return ImmutableSet.copyOf(Collections2.transform(summary, s -> s.media)); + } + + public static ContentAddition of(final Direction direction, final RtpContentMap rtpContentMap) { + return new ContentAddition(direction, summary(rtpContentMap)); + } + + public static Set summary(final RtpContentMap rtpContentMap) { + return ImmutableSet.copyOf( + Collections2.transform( + rtpContentMap.contents.entrySet(), + e -> { + final RtpContentMap.DescriptionTransport dt = e.getValue(); + return new Summary(e.getKey(), dt.description.getMedia(), dt.senders); + })); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("direction", direction) + .add("summary", summary) + .toString(); + } + + public enum Direction { + OUTGOING, + INCOMING + } + + public static final class Summary { + public final String name; + public final Media media; + public final Content.Senders senders; + + private Summary(final String name, final Media media, final Content.Senders senders) { + this.name = name; + this.media = media; + this.senders = senders; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Summary summary = (Summary) o; + return Objects.equal(name, summary.name) + && media == summary.media + && senders == summary.senders; + } + + @Override + public int hashCode() { + return Objects.hashCode(name, media, senders); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("name", name) + .add("media", media) + .add("senders", senders) + .toString(); + } + } +} 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 ad5aeac0e8b0b831f7e4c175dcf0633f96acc9ff..6e14fc56e50fd34a848d8e39fe47338776ea44f8 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -5,16 +5,16 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.google.common.base.Joiner; import com.google.common.base.Optional; import com.google.common.base.Preconditions; -import com.google.common.base.Predicates; import com.google.common.base.Stopwatch; 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; -import com.google.common.collect.Maps; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import com.google.common.primitives.Ints; import com.google.common.util.concurrent.FutureCallback; @@ -39,6 +39,7 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import eu.siacs.conversations.BuildConfig; import eu.siacs.conversations.Config; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.CryptoFailedException; @@ -53,6 +54,7 @@ import eu.siacs.conversations.utils.IP; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.jingle.stanzas.Content; import eu.siacs.conversations.xmpp.jingle.stanzas.Group; import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; @@ -163,6 +165,8 @@ public class JingleRtpConnection extends AbstractJingleConnection private Set proposedMedia; private RtpContentMap initiatorRtpContentMap; private RtpContentMap responderRtpContentMap; + private RtpContentMap incomingContentAdd; + private RtpContentMap outgoingContentAdd; private IceUdpTransportInfo.Setup peerDtlsSetup; private final Stopwatch sessionDuration = Stopwatch.createUnstarted(); private final Queue stateHistory = new LinkedList<>(); @@ -218,6 +222,18 @@ public class JingleRtpConnection extends AbstractJingleConnection case SESSION_TERMINATE: receiveSessionTerminate(jinglePacket); break; + case CONTENT_ADD: + receiveContentAdd(jinglePacket); + break; + case CONTENT_ACCEPT: + receiveContentAccept(jinglePacket); + break; + case CONTENT_REJECT: + receiveContentReject(jinglePacket); + break; + case CONTENT_REMOVE: + receiveContentRemove(jinglePacket); + break; default: respondOk(jinglePacket); Log.d( @@ -346,6 +362,405 @@ public class JingleRtpConnection extends AbstractJingleConnection } } + private void receiveContentAdd(final JinglePacket jinglePacket) { + final RtpContentMap modification; + try { + modification = RtpContentMap.of(jinglePacket); + modification.requireContentDescriptions(); + } catch (final RuntimeException e) { + Log.d( + Config.LOGTAG, + id.getAccount().getJid().asBareJid() + ": improperly formatted contents", + Throwables.getRootCause(e)); + respondOk(jinglePacket); + webRTCWrapper.close(); + sendSessionTerminate(Reason.of(e), e.getMessage()); + return; + } + if (isInState(State.SESSION_ACCEPTED)) { + receiveContentAdd(jinglePacket, modification); + } else { + terminateWithOutOfOrder(jinglePacket); + } + } + + private void receiveContentAdd( + final JinglePacket jinglePacket, final RtpContentMap modification) { + final RtpContentMap remote = getRemoteContentMap(); + if (!Collections.disjoint(modification.getNames(), remote.getNames())) { + respondOk(jinglePacket); + this.webRTCWrapper.close(); + sendSessionTerminate( + Reason.FAILED_APPLICATION, + String.format( + "contents with names %s already exists", + Joiner.on(", ").join(modification.getNames()))); + return; + } + final ContentAddition contentAddition = + ContentAddition.of(ContentAddition.Direction.INCOMING, modification); + + final RtpContentMap outgoing = this.outgoingContentAdd; + final Set outgoingContentAddSummary = + outgoing == null ? Collections.emptySet() : ContentAddition.summary(outgoing); + + if (outgoingContentAddSummary.equals(contentAddition.summary)) { + if (isInitiator()) { + Log.d( + Config.LOGTAG, + id.getAccount().getJid().asBareJid() + + ": respond with tie break to matching content-add offer"); + respondWithTieBreak(jinglePacket); + } else { + Log.d( + Config.LOGTAG, + id.getAccount().getJid().asBareJid() + + ": automatically accept matching content-add offer"); + acceptContentAdd(contentAddition.summary, modification); + } + return; + } + + // once we can display multiple video tracks we can be more loose with this condition + // theoretically it should also be fine to automatically accept audio only contents + if (Media.audioOnly(remote.getMedia()) && Media.videoOnly(contentAddition.media())) { + Log.d( + Config.LOGTAG, + id.getAccount().getJid().asBareJid() + ": received " + contentAddition); + this.incomingContentAdd = modification; + respondOk(jinglePacket); + updateEndUserState(); + } else { + respondOk(jinglePacket); + // TODO do we want to add a reason? + rejectContentAdd(modification); + } + } + + private void receiveContentAccept(final JinglePacket jinglePacket) { + final RtpContentMap receivedContentAccept; + try { + receivedContentAccept = RtpContentMap.of(jinglePacket); + receivedContentAccept.requireContentDescriptions(); + } catch (final RuntimeException e) { + Log.d( + Config.LOGTAG, + id.getAccount().getJid().asBareJid() + ": improperly formatted contents", + Throwables.getRootCause(e)); + respondOk(jinglePacket); + webRTCWrapper.close(); + sendSessionTerminate(Reason.of(e), e.getMessage()); + return; + } + + final RtpContentMap outgoingContentAdd = this.outgoingContentAdd; + if (outgoingContentAdd == null) { + Log.d(Config.LOGTAG, "received content-accept when we had no outgoing content add"); + terminateWithOutOfOrder(jinglePacket); + return; + } + final Set ourSummary = ContentAddition.summary(outgoingContentAdd); + if (ourSummary.equals(ContentAddition.summary(receivedContentAccept))) { + this.outgoingContentAdd = null; + respondOk(jinglePacket); + receiveContentAccept(receivedContentAccept); + } else { + Log.d(Config.LOGTAG, "received content-accept did not match our outgoing content-add"); + terminateWithOutOfOrder(jinglePacket); + } + } + + private void receiveContentAccept(final RtpContentMap receivedContentAccept) { + final IceUdpTransportInfo.Setup peerDtlsSetup = getPeerDtlsSetup(); + final RtpContentMap modifiedContentMap = + getRemoteContentMap().addContent(receivedContentAccept, peerDtlsSetup); + + setRemoteContentMap(modifiedContentMap); + + final SessionDescription answer = SessionDescription.of(modifiedContentMap, !isInitiator()); + + final org.webrtc.SessionDescription sdp = + new org.webrtc.SessionDescription( + org.webrtc.SessionDescription.Type.ANSWER, answer.toString()); + + try { + this.webRTCWrapper.setRemoteDescription(sdp).get(); + } catch (final Exception e) { + final Throwable cause = Throwables.getRootCause(e); + Log.d( + Config.LOGTAG, + id.getAccount().getJid().asBareJid() + + ": unable to set remote description after receiving content-accept", + cause); + webRTCWrapper.close(); + sendSessionTerminate(Reason.FAILED_APPLICATION, cause.getMessage()); + return; + } + updateEndUserState(); + Log.d( + Config.LOGTAG, + id.getAccount().getJid().asBareJid() + + ": remote has accepted content-add " + + ContentAddition.summary(receivedContentAccept)); + } + + private void receiveContentReject(final JinglePacket jinglePacket) { + final RtpContentMap receivedContentReject; + try { + receivedContentReject = RtpContentMap.of(jinglePacket); + } catch (final RuntimeException e) { + Log.d( + Config.LOGTAG, + id.getAccount().getJid().asBareJid() + ": improperly formatted contents", + Throwables.getRootCause(e)); + respondOk(jinglePacket); + this.webRTCWrapper.close(); + sendSessionTerminate(Reason.of(e), e.getMessage()); + return; + } + + final RtpContentMap outgoingContentAdd = this.outgoingContentAdd; + if (outgoingContentAdd == null) { + Log.d(Config.LOGTAG, "received content-reject when we had no outgoing content add"); + terminateWithOutOfOrder(jinglePacket); + return; + } + final Set ourSummary = ContentAddition.summary(outgoingContentAdd); + if (ourSummary.equals(ContentAddition.summary(receivedContentReject))) { + this.outgoingContentAdd = null; + respondOk(jinglePacket); + Log.d(Config.LOGTAG,jinglePacket.toString()); + receiveContentReject(ourSummary); + } else { + Log.d(Config.LOGTAG, "received content-reject did not match our outgoing content-add"); + terminateWithOutOfOrder(jinglePacket); + } + } + + private void receiveContentReject(final Set summary) { + try { + this.webRTCWrapper.removeTrack(Media.VIDEO); + final RtpContentMap localContentMap = customRollback(); + modifyLocalContentMap(localContentMap); + } catch (final Exception e) { + final Throwable cause = Throwables.getRootCause(e); + Log.d( + Config.LOGTAG, + id.getAccount().getJid().asBareJid() + + ": unable to rollback local description after receiving content-reject", + cause); + webRTCWrapper.close(); + sendSessionTerminate(Reason.FAILED_APPLICATION, cause.getMessage()); + return; + } + Log.d( + Config.LOGTAG, + id.getAccount().getJid().asBareJid() + + ": remote has rejected our content-add " + + summary); + } + + private void receiveContentRemove(final JinglePacket jinglePacket) { + final RtpContentMap receivedContentRemove; + try { + receivedContentRemove = RtpContentMap.of(jinglePacket); + receivedContentRemove.requireContentDescriptions(); + } catch (final RuntimeException e) { + Log.d( + Config.LOGTAG, + id.getAccount().getJid().asBareJid() + ": improperly formatted contents", + Throwables.getRootCause(e)); + respondOk(jinglePacket); + this.webRTCWrapper.close(); + sendSessionTerminate(Reason.of(e), e.getMessage()); + return; + } + respondOk(jinglePacket); + receiveContentRemove(receivedContentRemove); + } + + private void receiveContentRemove(final RtpContentMap receivedContentRemove) { + final RtpContentMap incomingContentAdd = this.incomingContentAdd; + final Set contentAddSummary = + incomingContentAdd == null + ? Collections.emptySet() + : ContentAddition.summary(incomingContentAdd); + final Set removeSummary = + ContentAddition.summary(receivedContentRemove); + if (contentAddSummary.equals(removeSummary)) { + this.incomingContentAdd = null; + updateEndUserState(); + } else { + webRTCWrapper.close(); + sendSessionTerminate( + Reason.FAILED_APPLICATION, + String.format( + "%s only supports %s as a means to retract a not yet accepted %s", + BuildConfig.APP_NAME, + JinglePacket.Action.CONTENT_REMOVE, + JinglePacket.Action.CONTENT_ACCEPT)); + } + } + + public synchronized void retractContentAdd() { + final RtpContentMap outgoingContentAdd = this.outgoingContentAdd; + if (outgoingContentAdd == null) { + throw new IllegalStateException("Not outgoing content add"); + } + try { + webRTCWrapper.removeTrack(Media.VIDEO); + final RtpContentMap localContentMap = customRollback(); + modifyLocalContentMap(localContentMap); + } catch (final Exception e) { + final Throwable cause = Throwables.getRootCause(e); + Log.d( + Config.LOGTAG, + id.getAccount().getJid().asBareJid() + + ": unable to rollback local description after trying to retract content-add", + cause); + webRTCWrapper.close(); + sendSessionTerminate(Reason.FAILED_APPLICATION, cause.getMessage()); + return; + } + this.outgoingContentAdd = null; + final JinglePacket retract = + outgoingContentAdd + .toStub() + .toJinglePacket(JinglePacket.Action.CONTENT_REMOVE, id.sessionId); + this.send(retract); + Log.d( + Config.LOGTAG, + id.getAccount().getJid() + + ": retract content-add " + + ContentAddition.summary(outgoingContentAdd)); + } + + private RtpContentMap customRollback() throws ExecutionException, InterruptedException { + final SessionDescription sdp = setLocalSessionDescription(); + final RtpContentMap localRtpContentMap = RtpContentMap.of(sdp, isInitiator()); + final SessionDescription answer = generateFakeResponse(localRtpContentMap); + this.webRTCWrapper + .setRemoteDescription( + new org.webrtc.SessionDescription( + org.webrtc.SessionDescription.Type.ANSWER, answer.toString())) + .get(); + return localRtpContentMap; + } + + private SessionDescription generateFakeResponse(final RtpContentMap localContentMap) { + final RtpContentMap currentRemote = getRemoteContentMap(); + final RtpContentMap.Diff diff = currentRemote.diff(localContentMap); + if (diff.isEmpty()) { + throw new IllegalStateException( + "Unexpected rollback condition. No difference between local and remote"); + } + final RtpContentMap patch = localContentMap.toContentModification(diff.added); + if (ImmutableSet.of(Content.Senders.NONE).equals(patch.getSenders())) { + final RtpContentMap nextRemote = + currentRemote.addContent( + patch.modifiedSenders(Content.Senders.NONE), getPeerDtlsSetup()); + return SessionDescription.of(nextRemote, !isInitiator()); + } + throw new IllegalStateException( + "Unexpected rollback condition. Senders were not uniformly none"); + } + + public synchronized void acceptContentAdd(@NonNull final Set contentAddition) { + final RtpContentMap incomingContentAdd = this.incomingContentAdd; + if (incomingContentAdd == null) { + throw new IllegalStateException("No incoming content add"); + } + + if (contentAddition.equals(ContentAddition.summary(incomingContentAdd))) { + this.incomingContentAdd = null; + acceptContentAdd(contentAddition, incomingContentAdd); + } else { + throw new IllegalStateException("Accepted content add does not match pending content-add"); + } + } + + private void acceptContentAdd(@NonNull final Set contentAddition, final RtpContentMap incomingContentAdd) { + final IceUdpTransportInfo.Setup setup = getPeerDtlsSetup(); + final RtpContentMap modifiedContentMap = getRemoteContentMap().addContent(incomingContentAdd, setup); + this.setRemoteContentMap(modifiedContentMap); + + final SessionDescription offer; + try { + offer = SessionDescription.of(modifiedContentMap, !isInitiator()); + } catch (final IllegalArgumentException | NullPointerException e) { + Log.d(Config.LOGTAG, id.getAccount().getJid().asBareJid() + ": unable convert offer from content-add to SDP", e); + webRTCWrapper.close(); + sendSessionTerminate(Reason.FAILED_APPLICATION, e.getMessage()); + return; + } + this.incomingContentAdd = null; + acceptContentAdd(contentAddition, offer); + } + + private void acceptContentAdd( + final Set contentAddition, final SessionDescription offer) { + final org.webrtc.SessionDescription sdp = + new org.webrtc.SessionDescription( + org.webrtc.SessionDescription.Type.OFFER, offer.toString()); + try { + this.webRTCWrapper.setRemoteDescription(sdp).get(); + + // TODO add tracks for 'media' where contentAddition.senders matches + + // TODO if senders.sending(isInitiator()) + + this.webRTCWrapper.addTrack(Media.VIDEO); + + // TODO add additional transceivers for recv only cases + + final SessionDescription answer = setLocalSessionDescription(); + final RtpContentMap rtpContentMap = RtpContentMap.of(answer, isInitiator()); + + final RtpContentMap contentAcceptMap = + rtpContentMap.toContentModification( + Collections2.transform(contentAddition, ca -> ca.name)); + Log.d( + Config.LOGTAG, + id.getAccount().getJid().asBareJid() + + ": sending content-accept " + + ContentAddition.summary(contentAcceptMap)); + modifyLocalContentMap(rtpContentMap); + sendContentAccept(contentAcceptMap); + } catch (final Exception e) { + Log.d(Config.LOGTAG, "unable to accept content add", Throwables.getRootCause(e)); + webRTCWrapper.close(); + sendSessionTerminate(Reason.FAILED_APPLICATION); + } + } + + private void sendContentAccept(final RtpContentMap contentAcceptMap) { + final JinglePacket jinglePacket = contentAcceptMap.toJinglePacket(JinglePacket.Action.CONTENT_ACCEPT, id.sessionId); + send(jinglePacket); + } + + public synchronized void rejectContentAdd() { + final RtpContentMap incomingContentAdd = this.incomingContentAdd; + if (incomingContentAdd == null) { + throw new IllegalStateException("No incoming content add"); + } + this.incomingContentAdd = null; + updateEndUserState(); + rejectContentAdd(incomingContentAdd); + } + + private void rejectContentAdd(final RtpContentMap contentMap) { + final JinglePacket jinglePacket = + contentMap + .toStub() + .toJinglePacket(JinglePacket.Action.CONTENT_REJECT, id.sessionId); + Log.d( + Config.LOGTAG, + id.getAccount().getJid().asBareJid() + + ": rejecting content " + + ContentAddition.summary(contentMap)); + send(jinglePacket); + } + private boolean checkForIceRestart( final JinglePacket jinglePacket, final RtpContentMap rtpContentMap) { final RtpContentMap existing = getRemoteContentMap(); @@ -1534,6 +1949,10 @@ public class JingleRtpConnection extends AbstractJingleConnection return RtpEndUserState.CONNECTING; } case SESSION_ACCEPTED: + final ContentAddition ca = getPendingContentAddition(); + if (ca != null && ca.direction == ContentAddition.Direction.INCOMING) { + return RtpEndUserState.INCOMING_CONTENT_ADD; + } return getPeerConnectionStateAsEndUserState(); case REJECTED: case REJECTED_RACED: @@ -1591,6 +2010,18 @@ public class JingleRtpConnection extends AbstractJingleConnection } } + public ContentAddition getPendingContentAddition() { + final RtpContentMap in = this.incomingContentAdd; + final RtpContentMap out = this.outgoingContentAdd; + if (out != null) { + return ContentAddition.of(ContentAddition.Direction.OUTGOING, out); + } else if (in != null) { + return ContentAddition.of(ContentAddition.Direction.INCOMING, in); + } else { + return null; + } + } + public Set getMedia() { final State current = getState(); if (current == State.NULL) { @@ -1604,14 +2035,16 @@ public class JingleRtpConnection extends AbstractJingleConnection return Preconditions.checkNotNull( this.proposedMedia, "RTP connection has not been initialized properly"); } + final RtpContentMap localContentMap = getLocalContentMap(); final RtpContentMap initiatorContentMap = initiatorRtpContentMap; - if (initiatorContentMap != null) { + if (localContentMap != null) { + return localContentMap.getMedia(); + } else if (initiatorContentMap != null) { return initiatorContentMap.getMedia(); } else if (isTerminated()) { - return Collections.emptySet(); // we might fail before we ever got a chance to set media + 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"); + return Preconditions.checkNotNull(this.proposedMedia, "RTP connection has not been initialized properly"); } } @@ -1625,6 +2058,16 @@ public class JingleRtpConnection extends AbstractJingleConnection return status != null && status.isVerified(); } + public boolean addMedia(final Media media) { + final Set currentMedia = getMedia(); + if (currentMedia.contains(media)) { + throw new IllegalStateException(String.format("%s has already been proposed", media)); + } + // TODO add state protection - can only add while ACCEPTED or so + Log.d(Config.LOGTAG,"adding media: "+media); + return webRTCWrapper.addTrack(media); + } + public synchronized void acceptCall() { switch (this.state) { case PROPOSED: @@ -1743,17 +2186,9 @@ public class JingleRtpConnection extends AbstractJingleConnection finish(); } - private void setupWebRTC( - final Set media, final List iceServers) - throws WebRTCWrapper.InitializationException { + private void setupWebRTC(final Set media, final List iceServers) throws WebRTCWrapper.InitializationException { this.jingleConnectionManager.ensureConnectionIsRegistered(this); - final AppRTCAudioManager.SpeakerPhonePreference speakerPhonePreference; - if (media.contains(Media.VIDEO)) { - speakerPhonePreference = AppRTCAudioManager.SpeakerPhonePreference.SPEAKER; - } else { - speakerPhonePreference = AppRTCAudioManager.SpeakerPhonePreference.EARPIECE; - } - this.webRTCWrapper.setup(this.xmppConnectionService, speakerPhonePreference); + this.webRTCWrapper.setup(this.xmppConnectionService, AppRTCAudioManager.SpeakerPhonePreference.of(media)); this.webRTCWrapper.initializePeerConnection(media, iceServers); } @@ -1905,21 +2340,23 @@ public class JingleRtpConnection extends AbstractJingleConnection webRTCWrapper.execute(this::closeWebRTCSessionAfterFailedConnection); return; } else { - webRTCWrapper.restartIce(); + this.restartIce(); } } updateEndUserState(); } + private void restartIce() { + this.stateHistory.clear(); + this.webRTCWrapper.restartIce(); + } + @Override public void onRenegotiationNeeded() { this.webRTCWrapper.execute(this::renegotiate); } private void renegotiate() { - //TODO needs to be called only for ice restarts; maybe in the call to restartICe() - this.stateHistory.clear(); - this.webRTCWrapper.setIsReadyToReceiveIceCandidates(false); final SessionDescription sessionDescription; try { sessionDescription = setLocalSessionDescription(); @@ -1945,19 +2382,26 @@ public class JingleRtpConnection extends AbstractJingleConnection if (diff.hasModifications() && iceRestart) { webRTCWrapper.close(); - sendSessionTerminate(Reason.FAILED_APPLICATION, "WebRTC unexpectedly tried to modify content and transport at once"); + sendSessionTerminate( + Reason.FAILED_APPLICATION, + "WebRTC unexpectedly tried to modify content and transport at once"); return; } if (iceRestart) { initiateIceRestart(rtpContentMap); return; + } else if (diff.isEmpty()) { + Log.d( + Config.LOGTAG, + "renegotiation. nothing to do. SignalingState=" + + this.webRTCWrapper.getSignalingState()); } if (diff.added.size() > 0) { - sendContentAdd(rtpContentMap); + modifyLocalContentMap(rtpContentMap); + sendContentAdd(rtpContentMap, diff.added); } - } private void initiateIceRestart(final RtpContentMap rtpContentMap) { @@ -1977,8 +2421,7 @@ public class JingleRtpConnection extends AbstractJingleConnection return; } if (response.getType() == IqPacket.TYPE.ERROR) { - final Element error = response.findChild("error"); - if (error != null && error.hasChild("tie-break", Namespace.JINGLE_ERRORS)) { + if (isTieBreak(response)) { Log.d(Config.LOGTAG, "received tie-break as result of ice restart"); return; } @@ -1990,8 +2433,40 @@ public class JingleRtpConnection extends AbstractJingleConnection }); } - private void sendContentAdd(final RtpContentMap rtpContentMap) { + private boolean isTieBreak(final IqPacket response) { + final Element error = response.findChild("error"); + return error != null && error.hasChild("tie-break", Namespace.JINGLE_ERRORS); + } + private void sendContentAdd(final RtpContentMap rtpContentMap, final Collection added) { + final RtpContentMap contentAdd = rtpContentMap.toContentModification(added); + this.outgoingContentAdd = contentAdd; + final JinglePacket jinglePacket = + contentAdd.toJinglePacket(JinglePacket.Action.CONTENT_ADD, id.sessionId); + jinglePacket.setTo(id.with); + xmppConnectionService.sendIqPacket( + id.account, + jinglePacket, + (connection, response) -> { + if (response.getType() == IqPacket.TYPE.RESULT) { + Log.d( + Config.LOGTAG, + id.getAccount().getJid().asBareJid() + + ": received ACK to our content-add"); + return; + } + if (response.getType() == IqPacket.TYPE.ERROR) { + if (isTieBreak(response)) { + this.outgoingContentAdd = null; + Log.d(Config.LOGTAG, "received tie-break as result of our content-add"); + return; + } + handleIqErrorResponse(response); + } + if (response.getType() == IqPacket.TYPE.TIMEOUT) { + handleIqTimeoutResponse(response); + } + }); } private void setLocalContentMap(final RtpContentMap rtpContentMap) { @@ -2010,6 +2485,15 @@ public class JingleRtpConnection extends AbstractJingleConnection } } + // this method is to be used for content map modifications that modify media + private void modifyLocalContentMap(final RtpContentMap rtpContentMap) { + final RtpContentMap activeContents = rtpContentMap.activeContents(); + setLocalContentMap(activeContents); + this.webRTCWrapper.switchSpeakerPhonePreference( + AppRTCAudioManager.SpeakerPhonePreference.of(activeContents.getMedia())); + updateEndUserState(); + } + private SessionDescription setLocalSessionDescription() throws ExecutionException, InterruptedException { final org.webrtc.SessionDescription sessionDescription = diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/Media.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/Media.java index da25516ca701476d68f073e11dc657e2f1d10b5b..6a41c89067e0d853f5b8db0d4a7a4140e6aab11b 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/Media.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/Media.java @@ -1,11 +1,18 @@ package eu.siacs.conversations.xmpp.jingle; +import com.google.common.collect.ImmutableSet; + import java.util.Locale; +import java.util.Set; + +import javax.annotation.Nonnull; public enum Media { + VIDEO, AUDIO, UNKNOWN; @Override + @Nonnull public String toString() { return super.toString().toLowerCase(Locale.ROOT); } @@ -17,4 +24,12 @@ public enum Media { return UNKNOWN; } } + + public static boolean audioOnly(Set media) { + return ImmutableSet.of(AUDIO).equals(media); + } + + public static boolean videoOnly(Set media) { + return ImmutableSet.of(VIDEO).equals(media); + } } 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 7af1469cf5f2e385efef40a3c6f5144207b7005b..994c3a23307a77fdf3350abcc9573061bd29eedc 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java @@ -14,6 +14,7 @@ import com.google.common.collect.Maps; import com.google.common.collect.Sets; import java.util.Collection; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -92,6 +93,10 @@ public class RtpContentMap { })); } + public Set getSenders() { + return ImmutableSet.copyOf(Collections2.transform(contents.values(),dt -> dt.senders)); + } + public List getNames() { return ImmutableList.copyOf(contents.keySet()); } @@ -281,6 +286,14 @@ public class RtpContentMap { return new RtpContentMap(this.group, contentMapBuilder.build()); } + public RtpContentMap modifiedSenders(final Content.Senders senders) { + return new RtpContentMap( + this.group, + Maps.transformValues( + contents, + dt -> new DescriptionTransport(senders, dt.description, dt.transport))); + } + public RtpContentMap toContentModification(final Collection modifications) { return new RtpContentMap( this.group, @@ -291,6 +304,22 @@ public class RtpContentMap { dt.senders, dt.description, IceUdpTransportInfo.STUB))); } + public RtpContentMap toStub() { + return new RtpContentMap( + null, + Maps.transformValues( + this.contents, + dt -> + new DescriptionTransport( + dt.senders, + RtpDescription.stub(dt.description.getMedia()), + IceUdpTransportInfo.STUB))); + } + + public RtpContentMap activeContents() { + return new RtpContentMap(group, Maps.filterValues(this.contents, dt -> dt.senders != Content.Senders.NONE)); + } + public Diff diff(final RtpContentMap rtpContentMap) { final Set existingContentIds = this.contents.keySet(); final Set newContentIds = rtpContentMap.contents.keySet(); @@ -307,24 +336,32 @@ public class RtpContentMap { } } - public RtpContentMap addContent(final RtpContentMap modification) { + public RtpContentMap addContent( + final RtpContentMap modification, final IceUdpTransportInfo.Setup setup) { final IceUdpTransportInfo.Credentials credentials = getDistinctCredentials(); final DTLS dtls = getDistinctDtls(); final IceUdpTransportInfo iceUdpTransportInfo = - IceUdpTransportInfo.of(credentials, dtls.setup, dtls.hash, dtls.fingerprint); - final Map combined = - new ImmutableMap.Builder() + IceUdpTransportInfo.of(credentials, setup, dtls.hash, dtls.fingerprint); + final Map combined = merge(contents, modification.contents); + /*new ImmutableMap.Builder() .putAll(contents) - .putAll( - Maps.transformValues( - modification.contents, - dt -> - new DescriptionTransport( - dt.senders, - dt.description, - iceUdpTransportInfo))) - .build(); - return new RtpContentMap(modification.group, combined); + .putAll(modification.contents) + .build();*/ + final Map combinedFixedTransport = + Maps.transformValues( + combined, + dt -> + new DescriptionTransport( + dt.senders, dt.description, iceUdpTransportInfo)); + return new RtpContentMap(modification.group, combinedFixedTransport); + } + + private static Map merge( + final Map a, final Map b) { + final Map combined = new HashMap<>(); + combined.putAll(a); + combined.putAll(b); + return ImmutableMap.copyOf(combined); } public static class DescriptionTransport { @@ -410,6 +447,10 @@ public class RtpContentMap { return !this.added.isEmpty() || !this.removed.isEmpty(); } + public boolean isEmpty() { + return this.added.isEmpty() && this.removed.isEmpty(); + } + @Override @Nonnull public String toString() { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java index 9a431bc011c7bb6758d3916ffcd240b6c0acb958..24ed790ddadc1b72ee560555f2fe7abea2023b43 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java @@ -5,6 +5,7 @@ public enum RtpEndUserState { CONNECTING, //session-initiate or session-accepted but no webrtc peer connection yet CONNECTED, //session-accepted and webrtc peer connection is connected RECONNECTING, //session-accepted and webrtc peer connection was connected once but is currently disconnected or failed + INCOMING_CONTENT_ADD, //session-accepted with a pending, incoming content-add FINDING_DEVICE, //'propose' has been sent out; no 184 ack yet RINGING, //'propose' has been sent out and it has been 184 acked ACCEPTING_CALL, //'proceed' message has been sent; but no session-initiate has been received diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java index eef7ae0da975ff0ac6b0579e0b5099844e6fdc41..f0f98260ba9d6cdc970df850b164cf57a5787b92 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java @@ -298,7 +298,7 @@ public class SessionDescription { mediaAttributes.put("mid", name); mediaAttributes.put(descriptionTransport.senders.asMediaAttribute(isInitiatorContentMap), ""); - if (description.hasChild("rtcp-mux", Namespace.JINGLE_APPS_RTP)) { + if (description.hasChild("rtcp-mux", Namespace.JINGLE_APPS_RTP) || group != null) { mediaAttributes.put("rtcp-mux", ""); } 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 e368d3b09ef0e7a6d4f7ca8761121c755db69402..02c1f6fe1aa0e68e4081e5bb71149a689c92a432 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java @@ -5,6 +5,7 @@ import android.media.AudioManager; import android.media.ToneGenerator; import android.util.Log; +import java.util.Arrays; import java.util.Set; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -19,6 +20,7 @@ class ToneManager { private final Context context; private ToneState state = null; + private RtpEndUserState endUserState = null; private ScheduledFuture currentTone; private ScheduledFuture currentResetFuture; private boolean appRtcAudioManagerHasControl = false; @@ -51,7 +53,11 @@ class ToneManager { return ToneState.ENDING_CALL; } } - if (state == RtpEndUserState.CONNECTED || state == RtpEndUserState.RECONNECTING) { + if (Arrays.asList( + RtpEndUserState.CONNECTED, + RtpEndUserState.RECONNECTING, + RtpEndUserState.INCOMING_CONTENT_ADD) + .contains(state)) { if (media.contains(Media.VIDEO)) { return ToneState.NULL; } else { @@ -62,14 +68,19 @@ class ToneManager { } void transition(final RtpEndUserState state, final Set media) { - transition(of(true, state, media), media); + transition(state, of(true, state, media), media); } void transition(final boolean isInitiator, final RtpEndUserState state, final Set media) { - transition(of(isInitiator, state, media), media); + transition(state, of(isInitiator, state, media), media); } - private synchronized void transition(ToneState state, final Set media) { + private synchronized void transition(final RtpEndUserState endUserState, final ToneState state, final Set media) { + final RtpEndUserState normalizeEndUserState = normalize(endUserState); + if (this.endUserState == normalizeEndUserState) { + return; + } + this.endUserState = normalizeEndUserState; if (this.state == state) { return; } @@ -105,6 +116,18 @@ class ToneManager { this.state = state; } + private static RtpEndUserState normalize(final RtpEndUserState endUserState) { + if (Arrays.asList( + RtpEndUserState.CONNECTED, + RtpEndUserState.RECONNECTING, + RtpEndUserState.INCOMING_CONTENT_ADD) + .contains(endUserState)) { + return RtpEndUserState.CONNECTED; + } else { + return endUserState; + } + } + void setAppRtcAudioManagerHasControl(final boolean appRtcAudioManagerHasControl) { this.appRtcAudioManagerHasControl = appRtcAudioManagerHasControl; } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/TrackWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/TrackWrapper.java index 4e2952127497a59e6cd03b86ad7c0b6b9eb17d53..31c3577eec981dfea8ad369f6580d84d03d1dcc5 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/TrackWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/TrackWrapper.java @@ -1,15 +1,26 @@ package eu.siacs.conversations.xmpp.jingle; +import android.util.Log; + +import com.google.common.base.CaseFormat; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import org.webrtc.MediaStreamTrack; import org.webrtc.PeerConnection; import org.webrtc.RtpSender; +import org.webrtc.RtpTransceiver; + +import java.util.UUID; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import eu.siacs.conversations.Config; class TrackWrapper { - private final T track; - private final RtpSender rtpSender; + public final T track; + public final RtpSender rtpSender; private TrackWrapper(final T track, final RtpSender rtpSender) { Preconditions.checkNotNull(track); @@ -25,7 +36,41 @@ class TrackWrapper { } public static Optional get( - final TrackWrapper trackWrapper) { - return trackWrapper == null ? Optional.absent() : Optional.of(trackWrapper.track); + @Nullable final PeerConnection peerConnection, final TrackWrapper trackWrapper) { + if (trackWrapper == null) { + return Optional.absent(); + } + final RtpTransceiver transceiver = + peerConnection == null ? null : getTransceiver(peerConnection, trackWrapper); + if (transceiver == null) { + Log.w(Config.LOGTAG, "unable to detect transceiver for " + trackWrapper.rtpSender.id()); + return Optional.of(trackWrapper.track); + } + final RtpTransceiver.RtpTransceiverDirection direction = transceiver.getDirection(); + if (direction == RtpTransceiver.RtpTransceiverDirection.SEND_ONLY + || direction == RtpTransceiver.RtpTransceiverDirection.SEND_RECV) { + return Optional.of(trackWrapper.track); + } else { + Log.d(Config.LOGTAG, "withholding track because transceiver is " + direction); + return Optional.absent(); + } + } + + public static RtpTransceiver getTransceiver( + @Nonnull final PeerConnection peerConnection, final TrackWrapper trackWrapper) { + final RtpSender rtpSender = trackWrapper.rtpSender; + for (final RtpTransceiver transceiver : peerConnection.getTransceivers()) { + if (transceiver.getSender().id().equals(rtpSender.id())) { + return transceiver; + } + } + return null; + } + + public static String id(final Class clazz) { + return String.format( + "%s-%s", + CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_HYPHEN, clazz.getSimpleName()), + UUID.randomUUID().toString()); } } 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 53b7de1e0d85207f23eac73b6ff6849b8f613879..b5ccf5c41520ff536eacefdf4886dc7fa6d5a874 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -225,7 +225,7 @@ public class WebRTCWrapper { public void setup( final XmppConnectionService service, - final AppRTCAudioManager.SpeakerPhonePreference speakerPhonePreference) + @Nonnull final AppRTCAudioManager.SpeakerPhonePreference speakerPhonePreference) throws InitializationException { try { PeerConnectionFactory.initialize( @@ -330,18 +330,35 @@ public class WebRTCWrapper { throw new IllegalStateException(String.format("Could not add track for %s", media)); } + public synchronized void removeTrack(final Media media) { + if (media == Media.VIDEO) { + removeVideoTrack(requirePeerConnection()); + } + } + private boolean addAudioTrack(final PeerConnection peerConnection) { final AudioSource audioSource = requirePeerConnectionFactory().createAudioSource(new MediaConstraints()); final AudioTrack audioTrack = - requirePeerConnectionFactory().createAudioTrack("my-audio-track", audioSource); + requirePeerConnectionFactory() + .createAudioTrack(TrackWrapper.id(AudioTrack.class), audioSource); this.localAudioTrack = TrackWrapper.addTrack(peerConnection, audioTrack); return true; } private boolean addVideoTrack(final PeerConnection peerConnection) { - Preconditions.checkState( - this.localVideoTrack == null, "A local video track already exists"); + final TrackWrapper existing = this.localVideoTrack; + if (existing != null) { + final RtpTransceiver transceiver = + TrackWrapper.getTransceiver(peerConnection, existing); + if (transceiver == null) { + Log.w(EXTENDED_LOGGING_TAG, "unable to restart video transceiver"); + return false; + } + transceiver.setDirection(RtpTransceiver.RtpTransceiverDirection.SEND_RECV); + this.videoSourceWrapper.startCapture(); + return true; + } final VideoSourceWrapper videoSourceWrapper; try { videoSourceWrapper = initializeVideoSourceWrapper(); @@ -351,11 +368,34 @@ public class WebRTCWrapper { } final VideoTrack videoTrack = requirePeerConnectionFactory() - .createVideoTrack("my-video-track", videoSourceWrapper.getVideoSource()); + .createVideoTrack( + TrackWrapper.id(VideoTrack.class), + videoSourceWrapper.getVideoSource()); this.localVideoTrack = TrackWrapper.addTrack(peerConnection, videoTrack); return true; } + private void removeVideoTrack(final PeerConnection peerConnection) { + final TrackWrapper localVideoTrack = this.localVideoTrack; + if (localVideoTrack != null) { + + final RtpTransceiver exactTransceiver = + TrackWrapper.getTransceiver(peerConnection, localVideoTrack); + if (exactTransceiver == null) { + throw new IllegalStateException(); + } + exactTransceiver.setDirection(RtpTransceiver.RtpTransceiverDirection.INACTIVE); + } + final VideoSourceWrapper videoSourceWrapper = this.videoSourceWrapper; + if (videoSourceWrapper != null) { + try { + videoSourceWrapper.stopCapture(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + private static PeerConnection.RTCConfiguration buildConfiguration( final List iceServers) { final PeerConnection.RTCConfiguration rtcConfig = @@ -375,7 +415,12 @@ public class WebRTCWrapper { } void restartIce() { - executorService.execute(() -> requirePeerConnection().restartIce()); + executorService.execute(() -> { + final PeerConnection peerConnection = requirePeerConnection(); + setIsReadyToReceiveIceCandidates(false); + peerConnection.restartIce(); + requirePeerConnection().restartIce();} + ); } public void setIsReadyToReceiveIceCandidates(final boolean ready) { @@ -450,7 +495,8 @@ public class WebRTCWrapper { } boolean isMicrophoneEnabled() { - final Optional audioTrack = TrackWrapper.get(this.localAudioTrack); + final Optional audioTrack = + TrackWrapper.get(peerConnection, this.localAudioTrack); if (audioTrack.isPresent()) { try { return audioTrack.get().enabled(); @@ -465,7 +511,8 @@ public class WebRTCWrapper { } boolean setMicrophoneEnabled(final boolean enabled) { - final Optional audioTrack = TrackWrapper.get(this.localAudioTrack); + final Optional audioTrack = + TrackWrapper.get(peerConnection, this.localAudioTrack); if (audioTrack.isPresent()) { try { audioTrack.get().setEnabled(enabled); @@ -481,7 +528,8 @@ public class WebRTCWrapper { } boolean isVideoEnabled() { - final Optional videoTrack = TrackWrapper.get(this.localVideoTrack); + final Optional videoTrack = + TrackWrapper.get(peerConnection, this.localVideoTrack); if (videoTrack.isPresent()) { return videoTrack.get().enabled(); } @@ -489,7 +537,8 @@ public class WebRTCWrapper { } void setVideoEnabled(final boolean enabled) { - final Optional videoTrack = TrackWrapper.get(this.localVideoTrack); + final Optional videoTrack = + TrackWrapper.get(peerConnection, this.localVideoTrack); if (videoTrack.isPresent()) { videoTrack.get().setEnabled(enabled); return; @@ -528,7 +577,7 @@ public class WebRTCWrapper { MoreExecutors.directExecutor()); } - private static void logDescription(final SessionDescription sessionDescription) { + public static void logDescription(final SessionDescription sessionDescription) { for (final String line : sessionDescription.description.split( eu.siacs.conversations.xmpp.jingle.SessionDescription.LINE_DIVIDER)) { @@ -612,7 +661,7 @@ public class WebRTCWrapper { } Optional getLocalVideoTrack() { - return TrackWrapper.get(this.localVideoTrack); + return TrackWrapper.get(peerConnection, this.localVideoTrack); } Optional getRemoteVideoTrack() { @@ -635,6 +684,10 @@ public class WebRTCWrapper { executorService.execute(command); } + public void switchSpeakerPhonePreference(AppRTCAudioManager.SpeakerPhonePreference preference) { + mainHandler.post(() -> appRTCAudioManager.switchSpeakerPhonePreference(preference)); + } + public interface EventCallback { void onIceCandidate(IceCandidate iceCandidate); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java index 650c26bef07af1eb1876641c4c4eb6c95cef3784..a7b62363c4cb5040e924f336f12b9363f99fcbcc 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java @@ -22,7 +22,6 @@ import eu.siacs.conversations.xmpp.jingle.SessionDescription; public class RtpDescription extends GenericDescription { - private RtpDescription(final String media) { super("description", Namespace.JINGLE_APPS_RTP); this.setAttribute("media", media); @@ -32,6 +31,10 @@ public class RtpDescription extends GenericDescription { super("description", Namespace.JINGLE_APPS_RTP); } + public static RtpDescription stub(final Media media) { + return new RtpDescription(media.toString()); + } + public Media getMedia() { return Media.of(this.getAttribute("media")); } @@ -57,7 +60,8 @@ public class RtpDescription extends GenericDescription { public List getHeaderExtensions() { final ImmutableList.Builder builder = new ImmutableList.Builder<>(); for (final Element child : getChildren()) { - if ("rtp-hdrext".equals(child.getName()) && Namespace.JINGLE_RTP_HEADER_EXTENSIONS.equals(child.getNamespace())) { + if ("rtp-hdrext".equals(child.getName()) + && Namespace.JINGLE_RTP_HEADER_EXTENSIONS.equals(child.getNamespace())) { builder.add(RtpHeaderExtension.upgrade(child)); } } @@ -67,7 +71,9 @@ public class RtpDescription extends GenericDescription { public List getSources() { final ImmutableList.Builder builder = new ImmutableList.Builder<>(); for (final Element child : this.children) { - if ("source".equals(child.getName()) && Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES.equals(child.getNamespace())) { + if ("source".equals(child.getName()) + && Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES.equals( + child.getNamespace())) { builder.add(Source.upgrade(child)); } } @@ -77,7 +83,9 @@ public class RtpDescription extends GenericDescription { public List getSourceGroups() { final ImmutableList.Builder builder = new ImmutableList.Builder<>(); for (final Element child : this.children) { - if ("ssrc-group".equals(child.getName()) && Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES.equals(child.getNamespace())) { + if ("ssrc-group".equals(child.getName()) + && Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES.equals( + child.getNamespace())) { builder.add(SourceGroup.upgrade(child)); } } @@ -85,8 +93,12 @@ public class RtpDescription extends GenericDescription { } public static RtpDescription upgrade(final Element element) { - Preconditions.checkArgument("description".equals(element.getName()), "Name of provided element is not description"); - Preconditions.checkArgument(Namespace.JINGLE_APPS_RTP.equals(element.getNamespace()), "Element does not match the jingle rtp namespace"); + Preconditions.checkArgument( + "description".equals(element.getName()), + "Name of provided element is not description"); + Preconditions.checkArgument( + Namespace.JINGLE_APPS_RTP.equals(element.getNamespace()), + "Element does not match the jingle rtp namespace"); final RtpDescription description = new RtpDescription(); description.setAttributes(element.getAttributes()); description.setChildren(element.getChildren()); @@ -116,7 +128,8 @@ public class RtpDescription extends GenericDescription { private static FeedbackNegotiation upgrade(final Element element) { Preconditions.checkArgument("rtcp-fb".equals(element.getName())); - Preconditions.checkArgument(Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION.equals(element.getNamespace())); + Preconditions.checkArgument( + Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION.equals(element.getNamespace())); final FeedbackNegotiation feedback = new FeedbackNegotiation(); feedback.setAttributes(element.getAttributes()); feedback.setChildren(element.getChildren()); @@ -126,13 +139,13 @@ public class RtpDescription extends GenericDescription { public static List fromChildren(final List children) { ImmutableList.Builder builder = new ImmutableList.Builder<>(); for (final Element child : children) { - if ("rtcp-fb".equals(child.getName()) && Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION.equals(child.getNamespace())) { + if ("rtcp-fb".equals(child.getName()) + && Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION.equals(child.getNamespace())) { builder.add(upgrade(child)); } } return builder.build(); } - } public static class FeedbackNegotiationTrrInt extends Element { @@ -142,7 +155,6 @@ public class RtpDescription extends GenericDescription { this.setAttribute("value", value); } - private FeedbackNegotiationTrrInt() { super("rtcp-fb-trr-int", Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION); } @@ -150,12 +162,12 @@ public class RtpDescription extends GenericDescription { public int getValue() { final String value = getAttribute("value"); return Integer.parseInt(value); - } private static FeedbackNegotiationTrrInt upgrade(final Element element) { Preconditions.checkArgument("rtcp-fb-trr-int".equals(element.getName())); - Preconditions.checkArgument(Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION.equals(element.getNamespace())); + Preconditions.checkArgument( + Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION.equals(element.getNamespace())); final FeedbackNegotiationTrrInt trr = new FeedbackNegotiationTrrInt(); trr.setAttributes(element.getAttributes()); trr.setChildren(element.getChildren()); @@ -163,9 +175,11 @@ public class RtpDescription extends GenericDescription { } public static List fromChildren(final List children) { - ImmutableList.Builder builder = new ImmutableList.Builder<>(); + ImmutableList.Builder builder = + new ImmutableList.Builder<>(); for (final Element child : children) { - if ("rtcp-fb-trr-int".equals(child.getName()) && Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION.equals(child.getNamespace())) { + if ("rtcp-fb-trr-int".equals(child.getName()) + && Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION.equals(child.getNamespace())) { builder.add(upgrade(child)); } } @@ -173,9 +187,8 @@ public class RtpDescription extends GenericDescription { } } - - //XEP-0294: Jingle RTP Header Extensions Negotiation - //maps to `extmap:$id $uri` + // XEP-0294: Jingle RTP Header Extensions Negotiation + // maps to `extmap:$id $uri` public static class RtpHeaderExtension extends Element { private RtpHeaderExtension() { @@ -198,7 +211,8 @@ public class RtpDescription extends GenericDescription { public static RtpHeaderExtension upgrade(final Element element) { Preconditions.checkArgument("rtp-hdrext".equals(element.getName())); - Preconditions.checkArgument(Namespace.JINGLE_RTP_HEADER_EXTENSIONS.equals(element.getNamespace())); + Preconditions.checkArgument( + Namespace.JINGLE_RTP_HEADER_EXTENSIONS.equals(element.getNamespace())); final RtpHeaderExtension extension = new RtpHeaderExtension(); extension.setAttributes(element.getAttributes()); extension.setChildren(element.getChildren()); @@ -217,7 +231,7 @@ public class RtpDescription extends GenericDescription { } } - //maps to `rtpmap:$id $name/$clockrate/$channels` + // maps to `rtpmap:$id $name/$clockrate/$channels` public static class PayloadType extends Element { private PayloadType() { @@ -238,8 +252,14 @@ public class RtpDescription extends GenericDescription { final int channels = getChannels(); final String name = getPayloadTypeName(); Preconditions.checkArgument(name != null, "Payload-type name must not be empty"); - SessionDescription.checkNoWhitespace(name, "payload-type name must not contain whitespaces"); - return getId() + " " + name + "/" + getClockRate() + (channels == 1 ? "" : "/" + channels); + SessionDescription.checkNoWhitespace( + name, "payload-type name must not contain whitespaces"); + return getId() + + " " + + name + + "/" + + getClockRate() + + (channels == 1 ? "" : "/" + channels); } public int getIntId() { @@ -251,7 +271,6 @@ public class RtpDescription extends GenericDescription { return this.getAttribute("id"); } - public String getPayloadTypeName() { return this.getAttribute("name"); } @@ -271,7 +290,8 @@ public class RtpDescription extends GenericDescription { public int getChannels() { final String channels = this.getAttribute("channels"); if (channels == null) { - return 1; // The number of channels; if omitted, it MUST be assumed to contain one channel + return 1; // The number of channels; if omitted, it MUST be assumed to contain one + // channel } try { return Integer.parseInt(channels); @@ -299,7 +319,9 @@ public class RtpDescription extends GenericDescription { } public static PayloadType of(final Element element) { - Preconditions.checkArgument("payload-type".equals(element.getName()), "element name must be called payload-type"); + Preconditions.checkArgument( + "payload-type".equals(element.getName()), + "element name must be called payload-type"); PayloadType payloadType = new PayloadType(); payloadType.setAttributes(element.getAttributes()); payloadType.setChildren(element.getChildren()); @@ -339,8 +361,8 @@ public class RtpDescription extends GenericDescription { } } - //map to `fmtp $id key=value;key=value - //where id is the id of the parent payload-type + // map to `fmtp $id key=value;key=value + // where id is the id of the parent payload-type public static class Parameter extends Element { private Parameter() { @@ -362,7 +384,8 @@ public class RtpDescription extends GenericDescription { } public static Parameter of(final Element element) { - Preconditions.checkArgument("parameter".equals(element.getName()), "element name must be called parameter"); + Preconditions.checkArgument( + "parameter".equals(element.getName()), "element name must be called parameter"); Parameter parameter = new Parameter(); parameter.setAttributes(element.getAttributes()); parameter.setChildren(element.getChildren()); @@ -375,12 +398,18 @@ public class RtpDescription extends GenericDescription { for (int i = 0; i < parameters.size(); ++i) { final Parameter p = parameters.get(i); final String name = p.getParameterName(); - Preconditions.checkArgument(name != null, String.format("parameter for %s must have a name", id)); - SessionDescription.checkNoWhitespace(name, String.format("parameter names for %s must not contain whitespaces", id)); + Preconditions.checkArgument( + name != null, String.format("parameter for %s must have a name", id)); + SessionDescription.checkNoWhitespace( + name, + String.format("parameter names for %s must not contain whitespaces", id)); final String value = p.getParameterValue(); - Preconditions.checkArgument(value != null, String.format("parameter for %s must have a value", id)); - SessionDescription.checkNoWhitespace(value, String.format("parameter values for %s must not contain whitespaces", id)); + Preconditions.checkArgument( + value != null, String.format("parameter for %s must have a value", id)); + SessionDescription.checkNoWhitespace( + value, + String.format("parameter values for %s must not contain whitespaces", id)); stringBuilder.append(name).append('=').append(value); if (i != parameters.size() - 1) { @@ -393,8 +422,11 @@ public class RtpDescription extends GenericDescription { public static String toSdpString(final String id, final Parameter parameter) { final String name = parameter.getParameterName(); final String value = parameter.getParameterValue(); - Preconditions.checkArgument(value != null, String.format("parameter for %s must have a value", id)); - SessionDescription.checkNoWhitespace(value, String.format("parameter values for %s must not contain whitespaces", id)); + Preconditions.checkArgument( + value != null, String.format("parameter for %s must have a value", id)); + SessionDescription.checkNoWhitespace( + value, + String.format("parameter values for %s must not contain whitespaces", id)); if (Strings.isNullOrEmpty(name)) { return String.format("%s %s", id, value); } else { @@ -420,8 +452,8 @@ public class RtpDescription extends GenericDescription { } } - //XEP-0339: Source-Specific Media Attributes in Jingle - //maps to `a=ssrc: :` + // XEP-0339: Source-Specific Media Attributes in Jingle + // maps to `a=ssrc: :` public static class Source extends Element { private Source() { @@ -452,7 +484,9 @@ public class RtpDescription extends GenericDescription { public static Source upgrade(final Element element) { Preconditions.checkArgument("source".equals(element.getName())); - Preconditions.checkArgument(Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES.equals(element.getNamespace())); + Preconditions.checkArgument( + Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES.equals( + element.getNamespace())); final Source source = new Source(); source.setChildren(element.getChildren()); source.setAttributes(element.getAttributes()); @@ -489,7 +523,6 @@ public class RtpDescription extends GenericDescription { return parameter; } } - } public static class SourceGroup extends Element { @@ -525,7 +558,9 @@ public class RtpDescription extends GenericDescription { public static SourceGroup upgrade(final Element element) { Preconditions.checkArgument("ssrc-group".equals(element.getName())); - Preconditions.checkArgument(Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES.equals(element.getNamespace())); + Preconditions.checkArgument( + Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES.equals( + element.getNamespace())); final SourceGroup group = new SourceGroup(); group.setChildren(element.getChildren()); group.setAttributes(element.getAttributes()); @@ -533,15 +568,18 @@ public class RtpDescription extends GenericDescription { } } - public static RtpDescription of(final SessionDescription sessionDescription, final SessionDescription.Media media) { + public static RtpDescription of( + final SessionDescription sessionDescription, final SessionDescription.Media media) { final RtpDescription rtpDescription = new RtpDescription(media.media); final Map> parameterMap = new HashMap<>(); - final ArrayListMultimap feedbackNegotiationMap = ArrayListMultimap.create(); - final ArrayListMultimap sourceParameterMap = ArrayListMultimap.create(); - final Set attributes = Sets.newHashSet(Iterables.concat( - sessionDescription.attributes.keySet(), - media.attributes.keySet() - )); + final ArrayListMultimap feedbackNegotiationMap = + ArrayListMultimap.create(); + final ArrayListMultimap sourceParameterMap = + ArrayListMultimap.create(); + final Set attributes = + Sets.newHashSet( + Iterables.concat( + sessionDescription.attributes.keySet(), media.attributes.keySet())); for (final String rtcpFb : media.attributes.get("rtcp-fb")) { final String[] parts = rtcpFb.split(" "); if (parts.length >= 2) { @@ -550,7 +588,10 @@ public class RtpDescription extends GenericDescription { final String subType = parts.length >= 3 ? parts[2] : null; if ("trr-int".equals(type)) { if (subType != null) { - feedbackNegotiationMap.put(id, new FeedbackNegotiationTrrInt(SessionDescription.ignorantIntParser(subType))); + feedbackNegotiationMap.put( + id, + new FeedbackNegotiationTrrInt( + SessionDescription.ignorantIntParser(subType))); } } else { feedbackNegotiationMap.put(id, new FeedbackNegotiation(type, subType)); @@ -602,7 +643,8 @@ public class RtpDescription extends GenericDescription { rtpDescription.addChild(new SourceGroup(semantics, builder.build())); } } - for (Map.Entry> source : sourceParameterMap.asMap().entrySet()) { + for (Map.Entry> source : + sourceParameterMap.asMap().entrySet()) { rtpDescription.addChild(new Source(source.getKey(), source.getValue())); } if (media.attributes.containsKey("rtcp-mux")) { From c178e9ad33ae3dd09621d5b11c704e0661951285 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 28 Nov 2022 11:39:26 +0100 Subject: [PATCH 092/101] add switch to video menu item to call --- .../conversations/ui/RtpSessionActivity.java | 139 +++++++++++++++--- .../res/drawable/ic_baseline_check_24.xml | 5 + src/main/res/menu/activity_rtp_session.xml | 3 + src/main/res/values/strings.xml | 4 + 4 files changed, 132 insertions(+), 19 deletions(-) create mode 100644 src/main/res/drawable/ic_baseline_check_24.xml diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index f9c7177a2d6a5d65c9c550aaf34ffd77403f187d..b91269c25ef54fec6a430a9fa212058608685bf2 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -65,6 +65,7 @@ import eu.siacs.conversations.utils.TimeFrameUtils; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection; +import eu.siacs.conversations.xmpp.jingle.ContentAddition; import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; import eu.siacs.conversations.xmpp.jingle.Media; @@ -101,9 +102,12 @@ public class RtpSessionActivity extends XmppActivity Arrays.asList( RtpEndUserState.CONNECTING, RtpEndUserState.CONNECTED, - RtpEndUserState.RECONNECTING); + RtpEndUserState.RECONNECTING, + RtpEndUserState.INCOMING_CONTENT_ADD); private static final List STATES_CONSIDERED_CONNECTED = - Arrays.asList(RtpEndUserState.CONNECTED, RtpEndUserState.RECONNECTING); + Arrays.asList( + RtpEndUserState.CONNECTED, + RtpEndUserState.RECONNECTING); private static final List STATES_SHOWING_PIP_PLACEHOLDER = Arrays.asList( RtpEndUserState.ACCEPTING_CALL, @@ -111,6 +115,8 @@ public class RtpSessionActivity extends XmppActivity RtpEndUserState.RECONNECTING); private static final String PROXIMITY_WAKE_LOCK_TAG = "conversations:in-rtp-session"; private static final int REQUEST_ACCEPT_CALL = 0x1111; + private static final int REQUEST_ACCEPT_CONTENT = 0x1112; + private static final int REQUEST_ADD_CONTENT = 0x1113; private WeakReference rtpConnectionReference; private ActivityRtpSessionBinding binding; @@ -164,8 +170,10 @@ public class RtpSessionActivity extends XmppActivity getMenuInflater().inflate(R.menu.activity_rtp_session, menu); final MenuItem help = menu.findItem(R.id.action_help); final MenuItem gotoChat = menu.findItem(R.id.action_goto_chat); + final MenuItem switchToVideo = menu.findItem(R.id.action_switch_to_video); help.setVisible(Config.HELP != null && isHelpButtonVisible()); gotoChat.setVisible(isSwitchToConversationVisible()); + switchToVideo.setVisible(isSwitchToVideoVisible()); return super.onCreateOptionsMenu(menu); } @@ -203,6 +211,15 @@ public class RtpSessionActivity extends XmppActivity && STATES_SHOWING_SWITCH_TO_CHAT.contains(connection.getEndUserState()); } + private boolean isSwitchToVideoVisible() { + final JingleRtpConnection connection = + this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; + if (connection == null) { + return false; + } + return Media.audioOnly(connection.getMedia()) && STATES_CONSIDERED_CONNECTED.contains(connection.getEndUserState()); + } + private void switchToConversation() { final Contact contact = getWith(); final Conversation conversation = @@ -215,10 +232,13 @@ public class RtpSessionActivity extends XmppActivity switch (item.getItemId()) { case R.id.action_help: launchHelpInBrowser(); - break; + return true; case R.id.action_goto_chat: switchToConversation(); - break; + return true; + case R.id.action_switch_to_video: + requestPermissionAndSwitchToVideo(); + return true; } return super.onOptionsItemSelected(item); } @@ -272,9 +292,60 @@ public class RtpSessionActivity extends XmppActivity requestPermissionsAndAcceptCall(); } + private void acceptContentAdd() { + try { + requireRtpConnection() + .acceptContentAdd(requireRtpConnection().getPendingContentAddition().summary); + } catch (final IllegalStateException e) { + Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show(); + } + } + + private void requestPermissionAndSwitchToVideo() { + final List permissions = permissions(ImmutableSet.of(Media.VIDEO, Media.AUDIO)); + if (PermissionUtils.hasPermission(this, permissions, REQUEST_ADD_CONTENT)) { + switchToVideo(); + } + } + + private void switchToVideo() { + try { + requireRtpConnection().addMedia(Media.VIDEO); + } catch (final IllegalStateException e) { + Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show(); + } + } + + private void acceptContentAdd(final ContentAddition contentAddition) { + if (contentAddition == null || contentAddition.direction != ContentAddition.Direction.INCOMING) { + Log.d(Config.LOGTAG,"ignore press on content-accept button"); + return; + } + requestPermissionAndAcceptContentAdd(contentAddition); + } + + private void requestPermissionAndAcceptContentAdd(final ContentAddition contentAddition) { + final List permissions = permissions(contentAddition.media()); + if (PermissionUtils.hasPermission(this, permissions, REQUEST_ACCEPT_CONTENT)) { + requireRtpConnection().acceptContentAdd(contentAddition.summary); + } + } + + private void rejectContentAdd(final View view) { + requireRtpConnection().rejectContentAdd(); + } + private void requestPermissionsAndAcceptCall() { + final List permissions = permissions(getMedia()); + if (PermissionUtils.hasPermission(this, permissions, REQUEST_ACCEPT_CALL)) { + putScreenInCallMode(); + checkRecorderAndAcceptCall(); + } + } + + private List permissions(final Set media) { final ImmutableList.Builder permissions = ImmutableList.builder(); - if (getMedia().contains(Media.VIDEO)) { + if (media.contains(Media.VIDEO)) { permissions.add(Manifest.permission.CAMERA).add(Manifest.permission.RECORD_AUDIO); } else { permissions.add(Manifest.permission.RECORD_AUDIO); @@ -282,10 +353,7 @@ public class RtpSessionActivity extends XmppActivity if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { permissions.add(Manifest.permission.BLUETOOTH_CONNECT); } - if (PermissionUtils.hasPermission(this, permissions.build(), REQUEST_ACCEPT_CALL)) { - putScreenInCallMode(); - checkRecorderAndAcceptCall(); - } + return permissions.build(); } private void checkRecorderAndAcceptCall() { @@ -516,6 +584,10 @@ public class RtpSessionActivity extends XmppActivity if (PermissionUtils.allGranted(permissionResult.grantResults)) { if (requestCode == REQUEST_ACCEPT_CALL) { checkRecorderAndAcceptCall(); + } else if (requestCode == REQUEST_ACCEPT_CONTENT) { + acceptContentAdd(); + } else if (requestCode == REQUEST_ADD_CONTENT) { + switchToVideo(); } } else { @StringRes int res; @@ -598,8 +670,8 @@ public class RtpSessionActivity extends XmppActivity private boolean isConnected() { final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; - return connection != null - && STATES_CONSIDERED_CONNECTED.contains(connection.getEndUserState()); + final RtpEndUserState endUserState = connection == null ? null : connection.getEndUserState(); + return STATES_CONSIDERED_CONNECTED.contains(endUserState) || endUserState == RtpEndUserState.INCOMING_CONTENT_ADD; } private boolean switchToPictureInPicture() { @@ -691,6 +763,7 @@ public class RtpSessionActivity extends XmppActivity return true; } final Set media = getMedia(); + final ContentAddition contentAddition = getPendingContentAddition(); if (currentState == RtpEndUserState.INCOMING_CALL) { getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } @@ -700,9 +773,9 @@ public class RtpSessionActivity extends XmppActivity } setWidth(currentState); updateVideoViews(currentState); - updateStateDisplay(currentState, media); + updateStateDisplay(currentState, media, contentAddition); updateVerifiedShield(verified && STATES_SHOWING_SWITCH_TO_CHAT.contains(currentState)); - updateButtonConfiguration(currentState, media); + updateButtonConfiguration(currentState, media, contentAddition); updateIncomingCallScreen(currentState); invalidateOptionsMenu(); return false; @@ -753,10 +826,10 @@ public class RtpSessionActivity extends XmppActivity } private void updateStateDisplay(final RtpEndUserState state) { - updateStateDisplay(state, Collections.emptySet()); + updateStateDisplay(state, Collections.emptySet(), null); } - private void updateStateDisplay(final RtpEndUserState state, final Set media) { + private void updateStateDisplay(final RtpEndUserState state, final Set media, final ContentAddition contentAddition) { switch (state) { case INCOMING_CALL: Preconditions.checkArgument(media.size() > 0, "Media must not be empty"); @@ -766,6 +839,13 @@ public class RtpSessionActivity extends XmppActivity setTitle(R.string.rtp_state_incoming_call); } break; + case INCOMING_CONTENT_ADD: + if (contentAddition != null && contentAddition.media().contains(Media.VIDEO)) { + setTitle(R.string.rtp_state_content_add_video); + } else { + setTitle(R.string.rtp_state_content_add); + } + break; case CONNECTING: setTitle(R.string.rtp_state_connecting); break; @@ -857,12 +937,16 @@ public class RtpSessionActivity extends XmppActivity return requireRtpConnection().getMedia(); } + public ContentAddition getPendingContentAddition() { + return requireRtpConnection().getPendingContentAddition(); + } + private void updateButtonConfiguration(final RtpEndUserState state) { - updateButtonConfiguration(state, Collections.emptySet()); + updateButtonConfiguration(state, Collections.emptySet(), null); } @SuppressLint("RestrictedApi") - private void updateButtonConfiguration(final RtpEndUserState state, final Set media) { + private void updateButtonConfiguration(final RtpEndUserState state, final Set media, final ContentAddition contentAddition) { if (state == RtpEndUserState.ENDING_CALL || isPictureInPicture()) { this.binding.rejectCall.setVisibility(View.INVISIBLE); this.binding.endCall.setVisibility(View.INVISIBLE); @@ -877,6 +961,16 @@ public class RtpSessionActivity extends XmppActivity this.binding.acceptCall.setOnClickListener(this::acceptCall); this.binding.acceptCall.setImageResource(R.drawable.ic_call_white_48dp); this.binding.acceptCall.setVisibility(View.VISIBLE); + } else if (state == RtpEndUserState.INCOMING_CONTENT_ADD) { + this.binding.rejectCall.setContentDescription(getString(R.string.reject_switch_to_video)); + this.binding.rejectCall.setOnClickListener(this::rejectContentAdd); + this.binding.rejectCall.setImageResource(R.drawable.ic_clear_white_48dp); + this.binding.rejectCall.setVisibility(View.VISIBLE); + this.binding.endCall.setVisibility(View.INVISIBLE); + this.binding.acceptCall.setContentDescription(getString(R.string.accept)); + this.binding.acceptCall.setOnClickListener((v -> acceptContentAdd(contentAddition))); + this.binding.acceptCall.setImageResource(R.drawable.ic_baseline_check_24); + this.binding.acceptCall.setVisibility(View.VISIBLE); } else if (state == RtpEndUserState.DECLINED_OR_BUSY) { this.binding.rejectCall.setContentDescription(getString(R.string.exit)); this.binding.rejectCall.setOnClickListener(this::exit); @@ -1051,6 +1145,12 @@ public class RtpSessionActivity extends XmppActivity } private void disableVideo(View view) { + final JingleRtpConnection rtpConnection = requireRtpConnection(); + final ContentAddition pending = rtpConnection.getPendingContentAddition(); + if (pending != null && pending.direction == ContentAddition.Direction.OUTGOING) { + rtpConnection.retractContentAdd(); + return; + } requireRtpConnection().setVideoEnabled(false); updateInCallButtonConfigurationVideo(false, requireRtpConnection().isCameraSwitchable()); } @@ -1279,6 +1379,7 @@ public class RtpSessionActivity extends XmppActivity final AbstractJingleConnection.Id id = requireRtpConnection().getId(); final boolean verified = requireRtpConnection().isVerified(); final Set media = getMedia(); + final ContentAddition contentAddition = getPendingContentAddition(); final Contact contact = getWith(); if (account == id.account && id.with.equals(with) && id.sessionId.equals(sessionId)) { if (state == RtpEndUserState.ENDED) { @@ -1287,10 +1388,10 @@ public class RtpSessionActivity extends XmppActivity } runOnUiThread( () -> { - updateStateDisplay(state, media); + updateStateDisplay(state, media, contentAddition); updateVerifiedShield( verified && STATES_SHOWING_SWITCH_TO_CHAT.contains(state)); - updateButtonConfiguration(state, media); + updateButtonConfiguration(state, media, contentAddition); updateVideoViews(state); updateIncomingCallScreen(state, contact); invalidateOptionsMenu(); diff --git a/src/main/res/drawable/ic_baseline_check_24.xml b/src/main/res/drawable/ic_baseline_check_24.xml new file mode 100644 index 0000000000000000000000000000000000000000..2501e9fd9fdde21ecf8372a6a9471c91590c1ea3 --- /dev/null +++ b/src/main/res/drawable/ic_baseline_check_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/src/main/res/menu/activity_rtp_session.xml b/src/main/res/menu/activity_rtp_session.xml index 04756490ab87c062ab6cd44a434920b05eda16c6..8768c29066583bb248d05805d8684fe81321def9 100644 --- a/src/main/res/menu/activity_rtp_session.xml +++ b/src/main/res/menu/activity_rtp_session.xml @@ -13,4 +13,7 @@ android:icon="?attr/icon_goto_chat" android:title="@string/switch_to_conversation" app:showAsAction="always" /> + \ No newline at end of file diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index d14a0c97152c464011a51eecb8746537be6a2f5c..3fc93601fdd4c04c5a910d609a905a2ccb3e04c8 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -909,6 +909,8 @@ Make call Incoming call Incoming video call + Switch to video call? + Add additional tracks? Connecting Connected Reconnecting @@ -995,5 +997,7 @@ Temporary authentication failure Delete avatar Calls are disabled when using Tor + Switch to video + Reject switch to video request From b374feccbd3e13bab51610026d5204e12f8a76af Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 30 Nov 2022 11:55:11 +0100 Subject: [PATCH 093/101] pulled translations from transifex --- .../res/values-zh-rTW/strings.xml | 10 +- src/main/res/values-de/strings.xml | 17 +- src/main/res/values-es/strings.xml | 18 + src/main/res/values-gl/strings.xml | 15 + src/main/res/values-pl/strings.xml | 21 ++ src/main/res/values-pt-rBR/strings.xml | 18 + src/main/res/values-ro-rRO/strings.xml | 18 + src/main/res/values-tr-rTR/strings.xml | 20 ++ src/main/res/values-zh-rCN/strings.xml | 12 + src/main/res/values-zh-rTW/strings.xml | 328 +++++++++++++++++- src/quicksy/res/values-zh-rTW/strings.xml | 4 + 11 files changed, 474 insertions(+), 7 deletions(-) diff --git a/src/conversations/res/values-zh-rTW/strings.xml b/src/conversations/res/values-zh-rTW/strings.xml index a5ba73d42d4d6ae97ce46021774d9f63b559e125..8f1828bf62b65a31e116e3b5c1d83e9a63325396 100644 --- a/src/conversations/res/values-zh-rTW/strings.xml +++ b/src/conversations/res/values-zh-rTW/strings.xml @@ -3,6 +3,14 @@ 挑選您的 XMPP 提供者 使用 conversations.im 建立新帳戶 + 您已經擁有一個 XMPP 賬戶嗎?如果您之前使用過其他 XMPP 客戶端或 Conversations 的話,那麼您已經擁有 XMPP 賬戶了。如果沒有賬戶的話,您可以現在建立一個。\n提示:有些電子郵件服務供應商也會提供 XMPP 賬戶。 + XMPP 是提供者無關的即時訊息網絡。 任何你選擇的 XMPP 伺服器都可在此客戶端上使用。\n不過,我們令它在 Coversations.im 中建立帳戶變得更方便; Conversations.im 是特別適合 Conversations 的提供者 + 你已受邀參加 %1$s 。 我們將指導你完成建立帳戶的過程。選擇 %1$s 作爲提供者後,你可以將你完整的 XMPP 地址交給使用其他提供者的用戶,你便能與他們交流。 + 您已被邀請參加 %1$s 。 我們已經爲你選擇了一個用戶名。 我們將指導你完成建立帳戶的過程。\n將你完整的 XMPP 地址交給使用其他提供者的用戶後,你便能與他們交流。 您的伺服器邀請 - 分享邀請至… + 配置代碼格式不正確 + 輕觸分享按鍵向您的聯絡人發送加入 %1$s 的邀請。 + 如果你的聯絡人就在附近,他們也可以掃描下面的代碼來接受你的邀請。 + 加入 %1$s 和我聊天:%2$s + 分享邀請到... \ No newline at end of file diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index 7fbc7ec10cc95d43417efe8e098a35270a76269c..c97286d864905350c8f6e077d87ad9c39cc3a574 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -170,6 +170,7 @@ Domain nicht überprüfbar Verstoß gegen die Richtlinien Inkompatibler Server + Inkompatibler Client Stream-Fehler Fehler beim Öffnen des Streams Unverschlüsselt @@ -363,7 +364,7 @@ Geräte entfernen Bist du sicher, dass du alle anderen Geräte aus der OMEMO-Bekanntmachung entfernen willst? Die Bekanntmachung wird bei der nächsten Verbindung erneuert aber möglicherweise werden keine zwischenzeitlich gesendeten Nachrichten empfangen. Für diesen Kontakt sind keine nutzbaren Schlüssel verfügbar.\nEs konnten keine neuen Schlüssel vom Server abgerufen werden. Gibt es vielleicht ein Problem mit dem Server deines Kontaktes? - Für diesen Kontakt sind keine benutzbaren Schlüssel verfügbar.\nStelle sicher, dass ihre beide gegenseitig den Online-Status aktiviert habt. + Für diesen Kontakt sind keine benutzbaren Schlüssel verfügbar.\nStelle sicher, dass ihr beide gegenseitig den Online-Status aktiviert habt. Etwas ist schief gelaufen Lade Chatverlauf vom Server Keine weiteren Nachrichten vorhanden @@ -767,6 +768,7 @@ Nachrichten Eingehende Anrufe Laufende Anrufe + Entgangene Anrufe Lautlose Nachrichten Diese Benachrichtigungsart wird verwendet, um Benachrichtigungen anzuzeigen, die keinen Ton auslösen sollen. Zum Beispiel, wenn du auf einem anderen Gerät aktiv bist (Schonfrist). Fehlgeschlagene Zustellungen @@ -934,6 +936,18 @@ Ausgehender Anruf Ausgehender Anruf · %s Entgangener Anruf + + %1$d entgangener Anruf von %2$s + %1$d entgangene Anrufe von %2$s + + + %d entgangener Anruf + %d entgangene Anrufe + + + %1$d entgangener Anruf von %2$d Kontakt + %1$d entgangene Anrufe von %2$d Kontakten + Audioanruf Videoanruf Hilfe @@ -980,4 +994,5 @@ Keine XMPP-Adresse gefunden Temporärer Authentifizierungsfehler Profilbild löschen + Anrufe sind bei der Verwendung von Tor deaktiviert diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index 04a5cc17039e6f5140e3be003255a73352c07f01..0b59a88d13a61e60f88f863fca4a70744ac684d0 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -173,6 +173,7 @@ Dominio no verificable Policy violation Servidor incompatible + Cliente incompatible Error de flujo Error al abrir la secuencia Sin cifrado @@ -778,6 +779,7 @@ Mensajes Llamadas entrantes Llamadas salientes + Llamadas perdidas Mensajes sin sonido Este grupo de notificaciones se usa para mostrar notificaciones que no deberían emitir ningún sonido. Por ejemplo, cuando estás activo en otro dispositivo (periodo de gracia). Envíos fallidos @@ -945,6 +947,21 @@ Llamada saliente Video llamada saliente · %s Llamada perdida + + %1$d llamada perdida de %2$s + %1$d llamadas perdidas de %2$s + %1$d llamadas perdidas de %2$s + + + %d llamada perdida + %d llamadas perdidas + %d llamadas perdidas + + + %1$d llamadas perdidas de %2$d contacto + %1$d llamadas perdidas de %2$d contacto + %1$d llamadas perdidas de %2$d contactos + Audio llamada Video llamada Ayuda @@ -993,4 +1010,5 @@ Dirección XMPP no encontrada Fallo temporal de autenticación Eliminar imagen de perfil + Las llamadas están deshabilitadas cuando se usa Tor diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index 8c8c00f60fd944e5a7b76c892bf4c6b14750c53c..24b3d3ffbb93faa64dc032a3d04dd95997acb990 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -170,6 +170,7 @@ Dominio non verificable Violación da política Servidor incompatible + Cliente non compatible Erro de fluxo Fallo ao abrir o fluxo Non cifrado @@ -767,6 +768,7 @@ Mensaxes Chamadas recibidas Chamadas realizadas + Chamadas perdidas Mensaxes acalados Este grupo de notificacións é utilizado para mostrar notificacións que non debería activar ningún son. Por exemplo, cando está activo en outro dispositivo (Período de Graza). Entregas fallidas @@ -934,6 +936,18 @@ Chamada realizada Conversa de · %s Chamada perdida + + %1$d chamada perdida de %2$s + %1$d chamadas perdidas de %2$s + + + %d chamada perdida + %d chamadas perdidas + + + %1$d chamadas perdidas de %2$d contacto + %1$d chamadas perdidas de %2$d contactos + Chamada de audio Chamada de vídeo Axuda @@ -980,4 +994,5 @@ Non se atopa un enderezo XMPP Fallo temporal da autenticación Eliminar avatar + As chamadas están desactivadas cando usas Tor diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index 75c46057a0de5f167695e382dd25e38141f4b76d..2952e156fa41c37109b2430e83ec8a0534d25fdb 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -176,6 +176,7 @@ Nie można zweryfikować tej domeny Naruszenie zasad Serwer niekompatybilny + Niekompatybilny klient Błąd strumienia Błąd otwierania strumienia Bez szyfrowania @@ -790,6 +791,7 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Wiadomości Połączenia przychodzące Połączenia wychodzące + Nieodebrane rozmowy Ciche wiadomości Ta kategoria powiadomień jest używana aby wyświetlać powiadomienia które nie powodują żadnych dźwięków. Na przykład w ciągu aktywności na innym urządzeniu (okres karencji). Nie dostarczone wiadomości @@ -957,6 +959,24 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Połączenie wychodzące Połączenie wychodzące · %s Nieodebrane połączenie + + %1$d nieodebrana rozmowa od %2$s + %1$d nieodebrane rozmowy od %2$s + %1$d nieodebranych rozmów od %2$s + %1$d nieodebranych rozmów od %2$s + + + %d nieodebrana rozmowa + %d nieodebrane rozmowy + %d nieodebranych rozmów + %d nieodebranych rozmów + + + %1$d nieodebrana rozmowa od %2$d kontaktu + %1$d nieodebrane rozmowy od %2$d kontaktu + %1$d nieodebranych rozmów od %2$d kontaktów + %1$d nieodebranych rozmów od %2$d kontaktów + Połączenie audio Połączenie wideo Pomoc @@ -1007,4 +1027,5 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Nie znaleziono adresu XMPP Tymczasowy błąd uwierzytelniania Usuń awatar + Dzwonienie jest wyłączone podczas używania Tora diff --git a/src/main/res/values-pt-rBR/strings.xml b/src/main/res/values-pt-rBR/strings.xml index 58e17a785f5af4515630262faff3a860daeba618..a5c8c8bed541a89399ee95aac02630808be22567 100644 --- a/src/main/res/values-pt-rBR/strings.xml +++ b/src/main/res/values-pt-rBR/strings.xml @@ -173,6 +173,7 @@ Domínio não verificável Violação de política Servidor incompatível + Cliente incompatível Erro de fluxo Erro na abertura do fluxo Descriptografada @@ -778,6 +779,7 @@ Mensagens Chamadas recebidas Chamadas em andamento + Chamadas perdidas Silenciar mensagens Essa categoria de notificação é utilizada para exibir notificações que não deveriam gerar nenhum som. Por exemplo, quando estiver ativo em outro dispositivo (Período de Espera). Entregas não efetuadas @@ -945,6 +947,21 @@ Chamada realizada Chamada realizada · %s Chamada perdida + + %1$d chamada perdida para %2$s + %1$d chamadas perdidas para %2$s + %1$d chamadas perdidas para %2$s + + + %d chamada perdida + %d chamadas perdidas + %d chamadas perdidas + + + %1$d chamadas perdidas de %2$d contato + %1$d chamadas perdidas de %2$d contatos + %1$d chamadas perdidas de %2$d contatos + Chamada de áudio Chamada de vídeo Ajuda @@ -993,4 +1010,5 @@ Não foi encontrado nenhum endereço XMPP Falha temporária na autenticação Excluir avatar + As chamadas estão desabilitadas ao usar Tor diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index 2f35075a5d2295b93f498292fc793b90486eb842..908d29fdf39fec5e7794ed46ba5fc299599dd05b 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -173,6 +173,7 @@ Domeniul nu se poate verifica Încălcare condiții furnizare serviciu Server incompatibil + Client incompatibil Eroare de date Eroare deschidere flux de date Necriptat @@ -778,6 +779,7 @@ Mesaje Apeluri primite Apeluri în curs + Apeluri pierdute Mesaje silențioase Acest grup de notificări este folosit pentru a arăta notificări care nu emit sunete. De exemplu atunci când sunteți activi pe un alt dispozitiv (Perioada de grație). Trimiteri eșuate @@ -945,6 +947,21 @@ Apel efectuat Apel efectuat · %s Apel pierdut + + %1$d apel pierdut de la %2$s + %1$d apeluri pierdute de la %2$s + %1$d de apeluri pierdute de la %2$s + + + %d apel pierdut + %d apeluri pierdute + %d de apeluri pierdute + + + %1$d apel pierdut de la %2$d contact + %1$d apeluri pierdute de la %2$d contact + %1$d de apeluri pierdute de la %2$d contacte + Apel audio Apel video Ajutor @@ -993,4 +1010,5 @@ Nu a fost găsită o adresă XMPP Eroare temporară de autentificare Șterge avatar + Apelurile sunt dezactivate atunci când utilizați Tor diff --git a/src/main/res/values-tr-rTR/strings.xml b/src/main/res/values-tr-rTR/strings.xml index 683901d9fd6af7939f5e0961f67af01b8de761c5..62d8966b538eaf1b8df67dd1251e317f92bf460c 100644 --- a/src/main/res/values-tr-rTR/strings.xml +++ b/src/main/res/values-tr-rTR/strings.xml @@ -170,6 +170,7 @@ Alan adı doğrulanamıyor Politika ihlali Sunucu uyuşmazlığı + Uyumsuz istemci Akış hatası Akış açılım hatası Şifrelenmemiş @@ -294,6 +295,7 @@ Sessiz saatleri etkinleştir Bildirimler sessiz saatler boyunca sessize alınacaktır Diğer + Yer imleriyle senkronize et OMEMO parmak izi panoya kopyalandı Bu grup konuşmasından menedildiniz Bu grup konuşması yalnızca üyeleri içindir @@ -301,6 +303,7 @@ Bu grup konuşmasından atıldınız Grup konuşması kapatıldı Artık bu grup konuşmasında değilsiniz + Teknik sebeplerden dolayı bu grup sohbetinden ayrıldınız %s hesabını kullanarak %sev sahipliğinde HTTP sunucusundaki %s denetleniyor @@ -415,6 +418,7 @@ video görüntü Vektör grafik + Multimedya dosyası PDF belgesi Android uygulaması Kişi @@ -763,6 +767,7 @@ İletiler Gelen aramalar Yapılan aramalar + Cevapsız aramalar Sessiz iletiler Bu bildirim grubu, bildirimlerin herhangi bir ses çıkarmaması gerektiğini belirtmekte kullanılır. Mesela başka bir cihazda aktif olunduğunda (Mühlet) Başarısız gönderiler @@ -930,6 +935,18 @@ Yapılan arama Yapılan arama. %s Cevapsız arama + + %2$s tarafından %1$d cevapsız çağrı + %2$s tarafından %1$d cevapsız çağrı + + + %d cevapsız çağrı + %d cevapsız çağrı + + + %2$d tarafından %1$d cevapsız çağrı + %2$d kişi tarafından %1$d cevapsız çağrı + Sesli arama Görüntülü arama Yardım @@ -974,4 +991,7 @@ Düz metin dosyası Hesap kayıtları desteklenmemektedir. Herhangi bir XMPP adresi bulunamadı + Geçici doğrulama hatası + Avatar\'ı sil + Tor kullanırken çağrılar devre dışı diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index cca20889f8f9c5682cf6a2366b886683bc6b0f9f..edd1df560eee6dad706860a71aff4a292846f7f0 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -167,6 +167,7 @@ 域名不可验证 违反政策 服务器不兼容 + 不兼容的客户端 流错误 流打开错误 未加密 @@ -756,6 +757,7 @@ 消息 来电 正在进行的通话 + 未接来电 无声消息 此通知组用于显示不应触发任何声音的通知。 例如,当在另一个设备上激活时(宽限期)。 发送失败 @@ -923,6 +925,15 @@ 去电 去电 · %s 未接电话 + + %1$d 错过了来自 %2$s 的电话 + + + %d 个未接电话 + + + %1$d 个未接电话,来自 %2$d 位联系人 + 语音通话 视频通话 帮助 @@ -967,4 +978,5 @@ 未找到 XMPP 地址 临时认证失败 删除群聊 + 使用 Tor 时通话被禁用 diff --git a/src/main/res/values-zh-rTW/strings.xml b/src/main/res/values-zh-rTW/strings.xml index 5c21adb4f764a45d28665f1222aa6bfd1a467ac3..45af4864bdf480b72e6d98f2473188d5a8d86a8c 100644 --- a/src/main/res/values-zh-rTW/strings.xml +++ b/src/main/res/values-zh-rTW/strings.xml @@ -124,8 +124,11 @@ 防止截圖 在多工畫面隱藏應用程式聯絡人並且封鎖螢幕截圖 UI + OpenKeychain 產生一個錯誤。 + 錯誤加密金鑰 接受 產生了一個錯誤 + 錯誤 你的帳戶 發送線上連絡人列表更新 接收線上連絡人列表更新 @@ -134,6 +137,7 @@ 照相 預先同意訂閱請求 選擇的檔案不是一張圖片 + 無法轉換圖片檔案 找不到檔案 常規的 I/O 錯誤。可能是存儲空間不足? 未知 @@ -149,9 +153,13 @@ 註冊完成 伺服器不支援註冊 無效的註冊權杖 + TLS 協商失敗 + 網域不可驗證 違反政策 伺服器不相容 + 不兼容的客戶端 串流錯誤 + 串流開啟錯誤 未加密 OTR OpenPGP @@ -162,6 +170,7 @@ 發佈 OpenPGP 公開金鑰 移除 OpenPGP 公開金鑰 確定要移除上線狀態中的 OpenPGP 公開金鑰嗎?\n這樣一來,你的聯絡人就無法傳送以 OpenPGP 加密的訊息給你了。 + OpenPGP 公開金鑰已發佈 啟用帳戶 確定? 刪除帳戶將清除您全部的會話記錄 @@ -188,11 +197,14 @@ 無效 缺少公開金鑰通知 剛剛查看過 + 一分鐘前查看過 %d 分鐘前查看過 一小時前查看過 %d 小時前查看過 一天前查看過 %d 天前查看過 + 訊息已加密。請安裝 OpenKeychain 以解密該訊息。 + 發現新的 OpenPGP 加密訊息 OpenPGP 金鑰 ID OMEMO 指紋 v\\OMEMO 指紋 @@ -218,17 +230,28 @@ channel@conference.example.com 儲存為書籤 刪除書籤 + 解散群組聊天 + 解散頻道 + 不能解散群組聊天 + 無法解散頻道 + 編輯群組聊天主題 主旨 正在加入群組聊天… 離開 聯絡人已新增至你的聯絡人清單 新增回 %s 已讀此句 + %s 已讀到這裏 + %1$s 和其他 %2$d 位已經讀到這裏 + 所有人已讀到這裏 發佈 + 輕觸頭像以從相片庫中選擇相片 正在發佈… 伺服器拒絕了您的發佈請求 + 無法轉換你的相片 不能將頭像保存至磁片 (或長按按鈕將返回預設頭像) + 你的伺服器不支援發佈頭像 私聊 至 %s 送私密訊息給 %s @@ -255,13 +278,24 @@ 啟用靜默時間段 在靜默時間段內通知將保持靜音 其他 + 同步處理書籤 + OMEMO 指紋已複製到剪貼簿 + 你已被這群組聊天封鎖 + 這群組聊天只有會員可以加入 + 資源限制 + 你已被踢出群組聊天 + 群組聊天已被關閉 + 你已不在該群組聊天 + 出於技術性原因,你離開了群組聊天 用帳戶 %s + 託管於 %s 正在 HTTP 伺服器中檢查 %s 你沒有連接。請稍後重試 檢查 %s 大小 在 %2$s 上檢查 %1$s 的大小 訊息選項 引用 + 作為引用貼上 拷貝原始URL 再次發送 檔案 URL @@ -275,6 +309,7 @@ 帳戶詳情 確認 再試一遍 + 前臺服務 防止作業系統中斷你的連接 建立備份 備份檔案將被儲存至 %s @@ -295,13 +330,21 @@ 可以下載 %s 取消傳送 無法分享檔案 + 檔案傳輸已取消 檔案已刪除 + 沒有可以打開檔案的應用程式 + 沒有可以打開連結的應用程式 + 沒有可以查看聯絡人的應用程式 + 動態標簽 在連絡人下方顯示唯讀標籤 啟用通知 + 未找到群組聊天伺服器 + 未能建立群組聊天 帳戶頭像 拷貝 OMEMO 指紋到剪貼板 重新生成 OMEMO 金鑰 清除設備 + 出錯了 從伺服器獲取歷史記錄 伺服器上沒有更多歷史記錄 更新中… @@ -310,21 +353,37 @@ 修改密碼 當前密碼 新密碼 + 密碼不能留空 啟用所有帳戶 禁用所有帳戶 選擇一個操作 沒有從屬關係 離線 拋棄 - 成員 + 會員 進階模式 + 授予會員許可權 + 撤銷會員許可權 授予管理員許可權 吊銷管理員許可權 + 授予擁有者許可權 + 撤銷擁有者許可權 + 從群組聊天移除 + 從頻道中移除 不能修改 %s 的從屬關係 - 現在遮罩 + 從群組聊天封鎖 + 從頻道中封鎖 + 你正在嘗試從公用頻道中移除 %s。只有永遠封鎖此用戶方能做到。 + 立即封鎖 不能修改 %s 的角色 - 私密,只有成員可以加入 + 設置私人群組聊天 + 設置公用頻道 + 私密,只有會員可以加入 + 令所有人可以看見 XMPP 地址 + 使頻道受到管理 您尚未參與 + 成功修改群組聊天選項! + 無法修改群組聊天選項 從不 直到新的通知 延遲 @@ -357,6 +416,8 @@ 找不到可以顯示位置的應用程式 位置 Conversation 已關閉 + 離開私人群組聊天 + 離開了公用頻道 不信任系統的憑證機構 所有證書必須人工通過 移除證書 @@ -368,11 +429,15 @@ %d 個證書已被刪除 + 以快速動作代替「發送」按鈕 快速動作 最近使用過的 選擇快速動作 + 搜尋聯絡人 + 搜尋書籤 送私密訊息 + %1$s 離開了群組聊天 用戶名 用戶名 該用戶名無效 @@ -380,17 +445,31 @@ 下載失敗:找不到檔案 下載失敗:無法連接到伺服器 下載失敗:無法寫入檔案 + 下載失敗:無效的檔案 Tor network 不可用 綁定失敗 + 伺服器不負責此網域名稱 損壞 + 在線狀態 + 裝置上鎖時離開 + 裝置上鎖時顯示為離開 + 靜音模式時忙碌 + 靜音模式時顯示為忙碌 靜音模式開啟振動 + 裝置振動時顯示為忙碌 高級連接設置 註冊帳戶時顯示主機名稱和埠 xmpp.example.com + 以證書登入 + 無法解析證書 壓縮設置 服務端壓縮設置 正在獲取壓縮設置。請稍後... + 無法獲取封存設置 + 需要 CAPTCHA 輸入上圖中的文字 + 未受信任的證書鏈 + XMPP 地址與證書不相符 更新證書 獲取 OMEMO 金鑰錯誤! 請用證書驗證 OMEMO 金鑰! @@ -400,6 +479,7 @@ 所有連接使用 Tor 網路傳輸,需要 Orbot 主機名稱 + 伺服器- 或 .orion- 地址 該埠號無效 該主機名稱無效 %2$d 個中的 %1$d 個帳戶已連接 @@ -409,11 +489,18 @@ 載入更多訊息 與 %s 分享的檔案 與 %s 分享的圖片 + 與 %s 分享的圖片 + 與 %s 分享的文字 + 授予 %1$s 存取外部儲存 + 授予 %1$s 存取相機 與連絡人同步 為所有訊息顯示通知 + 只在被提到時通知 關閉通知 暫停通知 + 圖像壓縮 總是 + 只限大圖片 啟用節電模式 禁用 選擇區域過大 @@ -422,10 +509,17 @@ 更正訊息 發送更正後的訊息 你已經禁用了此帳戶 + 安全性錯誤:無效的檔案存取! + 找不到可以分享 URI 的應用程式 分享網址(URI)… - 創建帳戶 + 同意並繼續 + 此指引將爲你在conversations.im¹上建立一個賬戶。\n使用 conversations.im 為你的提供者,再將你完整的 XMPP 地址交給使用其他提供者的用戶後,你便能與他們進行交流。 + 您的 XMPP 完整地址將會是: %s + 建立帳戶 使用我自己的服務端 輸入您的用戶名 + 手動更改在線狀態 + 在編輯你的狀態訊息時設立你的在線狀態 狀態訊息 免費聊天室 線上 @@ -437,6 +531,7 @@ 註冊失敗:請重試 註冊失敗:密碼太弱 選擇成員 + 正在建立群組聊天... 重新邀請 禁用 @@ -445,8 +540,12 @@ 隱私 主題 選擇調色板 + 自動 + 明亮 + 深色 綠色背景 接收到的訊息使用綠色背景 + 無法連接到 OpenKeychain 此設備不再使用 電腦 行動電話 @@ -454,19 +553,27 @@ 流覽器 控制台 需要付款 + 允計互聯網存取權 連絡人請求線上訂閱 允許 沒有訪問 %s 的許可 找不到遠端伺服器 + 遠端伺服器超時 + 無法更新帳戶 + 舉報此 XMPP 地址發送垃圾信息 刪除 OMEMO 身份 刪除選擇的金鑰 你需要連接才能發佈頭像 顯示錯誤訊息 錯誤訊息 省流量模式已啟動 + 該設備不支援對 %1$s 禁用節省流量模式 + 無法建立暫存檔案 已經驗證這個設備了 複製指紋 + 你已驗證了你擁有的所有 OMEMO 密鑰 + 條碼中沒有這個會話的指紋。 驗證過的指紋 使用相機來掃描聯絡人的條碼 取得金鑰中,請稍後 @@ -475,18 +582,38 @@ 分享網頁連結 在驗證前總是信任 不可信任 - 二維條碼不合格 + 二維條碼無效 清理快取資料 清理私人空間 清理儲存檔案的私人空間(檔案還可以從伺服器重新下載) 我使用來源可信任的連結 點了連結以後將會驗證 %1$s 的 OMEMO 金鑰。這個行為只有在該連結的來源可信任,並且只有 %2$s 可以提供該連結的情況下,才是安全無虞的。 + 繼續 驗證 OMEMO 金鑰 停止信任設備 + + %d 秒 + + + %d 分鐘 + + + %d 小時 + + + %d 天 + + + %d 星期 + + + %d 月 + 自動刪除訊息 自動從這個設備刪除比設定的時間區間還舊的訊息。 訊息加密中 訊息的時間因為超過本機保留區間而沒有下載。 + 壓縮影片中 關閉相關的對話了。 已經封鎖聯絡人了。 陌生人訊息通知 @@ -496,9 +623,17 @@ 剛剛上線了 再試解密ㄧ次 通訊對話錯誤 + 已降級的 SASL 機制 + 伺服器要求在網站上註冊 + 開啟網站 + 沒有可以打開網站的應用程式 頭條通知 + 顯示頭條通知 今天 昨天 + 以 DNSSEC 驗證主機名稱 + 證書不包含 XMPP 地址 + 部份 錄製影片 複製到剪貼簿 訊息已複製到剪貼簿 @@ -506,8 +641,12 @@ 私密訊息已停用 受保護的應用程式 接受未知憑證? + 伺服器證書未由已知證書機構簽發 + 接受不相符的伺服器名稱? + 你仍然想連線嗎? 憑證詳細資料: 僅一次 + 二維條碼掃描器需要相機權限 捲動至底部 傳送訊息後向下捲動 編輯狀態訊息 @@ -515,7 +654,9 @@ 停用加密 無法擷取裝置清單 無法擷取加密金鑰 + 提示:某些情況下,將對方加入聯絡人列表,便可以解決此問題。 立即停用 + 草稿: OMEMO 加密 一對一以及私人群組的聊天一定會用 OMEMO 新的對話預設會用 OMEMO 加密 @@ -528,6 +669,8 @@ + 訊息未在此裝置加密 + OMEMO 訊息解密失敗 復原 位置分享已停用 固定位置 @@ -548,71 +691,246 @@ 使用分享位置外掛程式而非內建地圖 複製網站位址 複製 XMPP 位址 + 用於 S3 的 HTTP 檔案分享 直接搜尋 + 在「開始對話」版面上打開鍵盤並將遊標放在搜尋列 + 群組聊天頭像 + 主機不支援群組聊天頭像 + 只有擁有者才能變更群組聊天頭像 + 聯絡人名稱 暱稱 名稱 + 可選擇提供名稱 聊天群組名稱 + 此群組聊天已被解散 無法儲存錄製 + 前臺服務 + 此通知類別用於顯示 %1$s 正在運行永久通知。 狀態資訊 + 連接問題 + 此通知類別用於顯示帳戶連接問題的通知。 訊息 通話 訊息 來電 正在進行的通話 + 未接來電 無聲訊息 + 傳送失敗 訊息通知設定 來電通知設定 + 重要程度,聲音,振動 影片壓縮 檢視媒體 成員 媒體瀏覽器 + 由於違反安全規定,你的檔案已被刪除。 影片質量 低質量意味這更小的檔案 中 (360P) 高 (720P) 已取消 + 你已經在起草一條訊息。 + 沒有此功能 無效的國家碼 選擇國家 電話號碼 驗證電話號碼 + Quicksy 將發送短訊(營運商可能收費)以驗證你的電話號碼。輸入國家地區代碼和手機號碼: + %s 不是有效的電話號碼 請輸入您的電話號碼。 搜尋國家 驗證 %s + %s。]]> + 我們已向你發出另一個包含六位數字代碼的簡訊。 + 請在下面輸入六位數字的 PIN 碼。 重新傳送簡訊 重新傳送簡訊 (%s) 請等候 (%s) 返回 + 已自動從剪貼簿貼上可能的 PIN 碼 + 請輸入六位數字的 PIN 碼。 + 你確定要終止註冊? 正在驗證… 正在要求簡訊… + 你輸入的 PIN 碼不正確。 + 我們向你發出的 PIN 碼已經過期。 未知網路錯誤。 + 伺服器的未知回應。 + 無法與伺服器連接。 + 無法建立安全連線。 + 找不到伺服器 + 處理你的請求時出錯 + 無效的用戶輸入 + 暫時無法連接,請稍候再試。 沒有網路連線。 + 請在 %s 後再次嘗試 + 你的頻率已被限制 + 太多的嘗試 + 你正在使用此應用程式的過時版本。 更新 + 此電話號碼已在其他裝置上登錄 + 請輸入您的名稱,使那些沒有把你加入通訊錄的人也知道你是誰。 你的名稱 輸入你的名稱 + 用編輯按鍵設立你的名稱 拒絕要求 + 安裝 Orbot + 啟動 Orbot + 沒有安裝軟件商店 + 這頻道將會公開你的 XMPP 地址 電子書 + 原始(未壓縮) 開啟為… + Conversations 設定檔圖片 選擇帳戶 還原備份 還原 + 輸入帳戶 %s 的密碼以恢復備份。 + 請勿使用恢復備份功能來嘗試複製安裝(即同時運行)。恢復備份功能應只在遷移裝置或丟失裝置的情況下才使用。 + 無法恢復備份。 + 無法為備份解密。密碼是不正確? 備份與還原 + 輸入 XMPP 地址 建立群組聊天 加入公用頻道 建立私人群組聊天 建立公用頻道 頻道名稱 XMPP 位址 + 請為頻道提供一個名稱 + 請提供 XMPP 地址 + 這是一個 XMPP 地址。請提供名稱。 + 正在建立公用頻道... + 此頻道已經存在 + 你已加入一個已經存在的頻道廿 + 無法儲存頻道設置 + 允許所有人編輯主題 + 允許所有人邀請其他人 + 所有人都可以編輯主題 + 擁有人可以編輯主題 + 管理員可以編輯主題 + 擁有人可以邀請其他人 + 所有人都可以邀請其他人 + 管理員可以看見此 XMPP 地址 + 所有人可以看見 XMPP 地址 + 此公開頻道沒有成員。邀請聯絡人或使用分享按鍵傳播 XMPP 地址。 + 此私人群組聊天沒有成員 + 管理許可權 + 搜尋成員 + 檔案太大 + 附加 + 探索頻道 + 搜尋頻道 + 可能侵犯私隱! + search.jabber.network

的第三方服務。使用此功能會將你的IP地址和搜尋字詞傳輸到該服務。 有關更多資訊,請參閱其私隱政策。]]> + 我已經有一個帳戶 + 添加已有帳戶 + 註冊新帳戶 + 這看似是一個網域地址 + 仍然添加 + 這看似是一個頻道地址 + 分享備份檔案 + Conversations 備份 活動 開啟備份 + 你選擇的並不是 Conversations 的備份檔案 + 此帳戶已設置 + 請輸入此帳戶的密碼 + 無法執行此操作 + 加入公用頻道... + 分享程式沒有存取檔案的權限 + 本機伺服器 + 大多數用戶應該選擇 “jabber.network” 以從整個公開的 XMPP 生態系統中獲得更好的建議。 + 頻道探索方法 + 備份 關於 + 請啟用一個帳戶 + 進行通話 + 來電 + 視像通話來電 + 正在連接 + 已接通 + 正在重新連接 + 正在接通來電 + 終止通話 + 接聽 + 拒接 + 正在探索裝置 + 正在響鈴 忙碌 + 無法連接通話 + 連接失敗 + 通話已撤銷 + 程式錯誤 + 驗証問題 + 掛斷 + 正在進行的通話 + 打出視像通話 + 重新連接通話 + 視像通話重新連接中 + 關閉 Tor 以進行通話 + 來電 + 來電 %s + 未接來電 %s + 撥出通話 + 撥出通話 %s + 未接來電 + + 來自 %2$s 的 %1$d 個未接來電 + + + %d 未接來電 + + + 來自 %2$d 個聯絡人的 %1$d 個未接來電 + + 語音通話 + 視像通話 說明 + 切換到會話 + 你的麥克風未能使用 + 你同時只能有一個通話 + 返回正在進行的通話 + 無法切換鏡頭 釘選 取消釘選 + GPX 追綜 + 無法更正訊息 + 所有會話 + 這會話 + 你的頭像 + %s 的頭像 + 以 OMEMO 加密 + 以 OpenPGP 加密 + 沒有加密 離開 + 錄製語音訊息 播放音訊 + 暫停音訊 + 添加聯絡人, 建立或加入群組聊天, 或探索頻道 + + 查看 %1$d 成員 + + + 有些訊息無法傳送 + + 傳送失敗 更多選項 + 沒有找到應用程式 + 邀請到 Conversations + 無法解析邀請 + 伺服器不支援產生邀請 + 沒有活躍帳戶支持此功能 + 已開始進行備份。完成後你會收到一則通知。 + 無法啓用視訊 + 純文字檔案 + 不支援帳戶註冊 + 未找到 XMPP 地址 + 臨時驗證失敗 + 刪除頭像 + 使用 Tor 時不能進行通話 diff --git a/src/quicksy/res/values-zh-rTW/strings.xml b/src/quicksy/res/values-zh-rTW/strings.xml index d4b37b79204765bbf648c13f3883f5b3b42a6040..9846922b11f6cc749568fa920e9c5b42dc84b430 100644 --- a/src/quicksy/res/values-zh-rTW/strings.xml +++ b/src/quicksy/res/values-zh-rTW/strings.xml @@ -1,5 +1,9 @@ + 發現在其它設備上的活動後,Quicksy 保持安靜的時間 + 發送堆疊跟蹤説明以幫助 Quicksy 持續開發 + 讓你的所有聯絡人知道你何時使用 Quicksy + 爲了在螢幕關閉時也能收到通知,你需要將 Quicksy 加入受保護的應用程式列表。 Quicksy 設定檔圖片 Quicksy 在您的國家無法使用。 無法驗證伺服器身分。 From 16f140572f65a48016164cbfd13c66e9fe2d99c1 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 30 Nov 2022 11:55:34 +0100 Subject: [PATCH 094/101] version bump to 2.11.0-beta.2 --- CHANGELOG.md | 1 + build.gradle | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bed8eedb3ead266e73bc66f73837c4581ab71b0..ca90749baf3c5ca01135a3c71e06566ca6ff3d7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * Implement Extensible SASL Profile, Bind 2.0 and Fast for faster reconnects * Implement Channel Binding +* Add ability to switch from audio call to video call * Add ability to delete own avatar * Add notification for missed calls diff --git a/build.gradle b/build.gradle index f5c5fda514c6739a0ec9b63b5d7b5602befd20fd..c5f1ba1d8cf9ec8fa33e545eea68a720e2ade2a5 100644 --- a/build.gradle +++ b/build.gradle @@ -93,8 +93,8 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion 32 - versionCode 42039 - versionName "2.11.0-beta" + versionCode 42040 + versionName "2.11.0-beta.2" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId From 80d195d35eba1d1b12f9efbb86734c33d234ad61 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 30 Nov 2022 17:32:46 +0100 Subject: [PATCH 095/101] avoid race condition when restarting ICE --- .../xmpp/jingle/WebRTCWrapper.java | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 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 b5ccf5c41520ff536eacefdf4886dc7fa6d5a874..4bb26be72326736efb34b9400407306a98868979 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -415,12 +415,20 @@ public class WebRTCWrapper { } void restartIce() { - executorService.execute(() -> { - final PeerConnection peerConnection = requirePeerConnection(); - setIsReadyToReceiveIceCandidates(false); - peerConnection.restartIce(); - requirePeerConnection().restartIce();} - ); + executorService.execute( + () -> { + final PeerConnection peerConnection; + try { + peerConnection = requirePeerConnection(); + } catch (final PeerConnectionNotInitialized e) { + Log.w( + EXTENDED_LOGGING_TAG, + "PeerConnection vanished before we could execute restart"); + return; + } + setIsReadyToReceiveIceCandidates(false); + peerConnection.restartIce(); + }); } public void setIsReadyToReceiveIceCandidates(final boolean ready) { From 2c7c44e957edb7df8ab63f628c4233073bfe374e Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 1 Dec 2022 20:46:18 +0100 Subject: [PATCH 096/101] null PeerConnection reference before disposing; otherwise getState() might be issued against disposed object --- .../java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java | 3 ++- 1 file changed, 2 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 4bb26be72326736efb34b9400407306a98868979..b929e9509356c3f3cbb1066b69ee78074451cee2 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -445,8 +445,8 @@ public class WebRTCWrapper { final AppRTCAudioManager audioManager = this.appRTCAudioManager; final EglBase eglBase = this.eglBase; if (peerConnection != null) { - dispose(peerConnection); this.peerConnection = null; + dispose(peerConnection); } if (audioManager != null) { toneManager.setAppRtcAudioManagerHasControl(false); @@ -467,6 +467,7 @@ public class WebRTCWrapper { this.eglBase = null; } if (peerConnectionFactory != null) { + this.peerConnectionFactory = null; peerConnectionFactory.dispose(); } } From 542afe2cb0258bef64d55ea478fa794bb9adb701 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 2 Dec 2022 19:40:11 +0100 Subject: [PATCH 097/101] pulled translations from transifex --- src/main/res/values-de/strings.xml | 7 ++++++- src/main/res/values-gl/strings.xml | 7 ++++++- src/main/res/values-it/strings.xml | 25 ++++++++++++++++++++++++- src/main/res/values-pl/strings.xml | 7 ++++++- src/main/res/values-ro-rRO/strings.xml | 7 ++++++- src/main/res/values-zh-rCN/strings.xml | 7 ++++++- 6 files changed, 54 insertions(+), 6 deletions(-) diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index c97286d864905350c8f6e077d87ad9c39cc3a574..8a38abbc44f22aecb5fc81b2fa77a238893f3178 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -909,6 +909,8 @@ Anrufen Eingehender Anruf Eingehender Videoanruf + Umschalten auf Videoanruf? + Zusätzliche Audiospuren hinzufügen? Verbinden Verbunden Erneut verbinden @@ -995,4 +997,7 @@ Temporärer Authentifizierungsfehler Profilbild löschen Anrufe sind bei der Verwendung von Tor deaktiviert - + Umschalten auf Video + Umschalten auf Video ablehnen + + diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index 24b3d3ffbb93faa64dc032a3d04dd95997acb990..32dce7a18f0010126f1a5b7ca1f3df3b9fa7f2a3 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -909,6 +909,8 @@ Facer unha chamada Chamada entrante Videochamada entrante + Cambiar a unha chamada de vídeo? + Engadir pistas adicionais? Conectando Conectado Reconectando @@ -995,4 +997,7 @@ Fallo temporal da autenticación Eliminar avatar As chamadas están desactivadas cando usas Tor - + Cambiar a vídeo + Rexeitar a solicitude para cambiar a vídeo + + diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml index df1331f462954b83e5d14741a8d871e0db737b3b..ab03fb074e809b49f769d6ecb3be644d164abfcc 100644 --- a/src/main/res/values-it/strings.xml +++ b/src/main/res/values-it/strings.xml @@ -173,6 +173,7 @@ Dominio non verificabile Violazione della policy Server non compatibile + Client non compatibile Errore di stream Errore apertura flusso Non cifrato @@ -778,6 +779,7 @@ Messaggi Chiamate in arrivo Chiamate in uscita + Chiamate perse Messaggi silenziosi Questo gruppo di notifiche è usato per mostrare notifiche che non devono riprodurre alcun suono. Ad esempio mentre si è attivi su un altro dispositivo (Periodo di grazia). Recapiti falliti @@ -918,6 +920,8 @@ Chiama Chiamata in arrivo Chiamata video in arrivo + Passare a una videochiamata? + Aggiungere altre tracce? Connessione Connesso Riconnessione @@ -945,6 +949,21 @@ Chiamata in uscita Chiamata in uscita · %s Chiamata persa + + %1$d chiamata persa da %2$s + %1$d chiamate perse da %2$s + %1$d chiamate perse da %2$s + + + %d chiamata persa + %d chiamate perse + %d chiamate perse + + + %1$d chiamate perse da %2$d contatto + %1$d chiamate perse da %2$d contatti + %1$d chiamate perse da %2$d contatti + Chiamata vocale Chiamata video Aiuto @@ -993,4 +1012,8 @@ Nessun indirizzo XMPP trovato Errore di autenticazione temporaneo Elimina avatar - + Le chiamate sono disattivate quando si usa Tor + Passa al video + Rifiuta richiesta di passare al video + + diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index 2952e156fa41c37109b2430e83ec8a0534d25fdb..b4e03f700fe07d9269ec25910ae72938065b39f9 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -932,6 +932,8 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Zadzwoń Połączenie przychodzące Wideorozmowa przychodząca + Przełączyć na rozmowę wideo? + Włączyć dodatkowe ścieżki? Łączenie Połączony Ponowne łączenie @@ -1028,4 +1030,7 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Tymczasowy błąd uwierzytelniania Usuń awatar Dzwonienie jest wyłączone podczas używania Tora - + Przełącz na wideo + Odrzuć prośbę przełączenia na wideo + + diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index 908d29fdf39fec5e7794ed46ba5fc299599dd05b..ad37ec02eb8e546329e2275055213bf2521b1665 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -920,6 +920,8 @@ Apelează Apel primit Apel video primit + Comută la apel video? + Adăugați canale suplimentare? Conectare Conectat Reconectare @@ -1011,4 +1013,7 @@ Eroare temporară de autentificare Șterge avatar Apelurile sunt dezactivate atunci când utilizați Tor - + Comută la video + Respinge solicitarea de comutare la video + + diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index edd1df560eee6dad706860a71aff4a292846f7f0..960e1ec418ea72e0b187469fec557db97f0e2137 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -898,6 +898,8 @@ 进行通话 来电 视频来电 + 切换到视频通话? + 添加额外轨道? 正在连接 已连接 重新连接 @@ -979,4 +981,7 @@ 临时认证失败 删除群聊 使用 Tor 时通话被禁用 - + 切换到视频 + 拒绝切换到视频的请求 + + From a27f6210dfe9dd0ec2d26451fa9fcdb38f36cbac Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 5 Dec 2022 09:45:56 +0100 Subject: [PATCH 098/101] pulled translations from transifex --- src/conversations/res/values-zh-rCN/strings.xml | 12 ++++++------ src/main/res/values-pt-rBR/strings.xml | 7 ++++++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/conversations/res/values-zh-rCN/strings.xml b/src/conversations/res/values-zh-rCN/strings.xml index 39254955a82c5fece02cdd9f625d3e3cc0d9d3ac..dfe82d428f63f940bd60b097e555ff5836e2bd87 100644 --- a/src/conversations/res/values-zh-rCN/strings.xml +++ b/src/conversations/res/values-zh-rCN/strings.xml @@ -1,12 +1,12 @@ - 选择您的XMPP提供者 - 使用conversations.im + 选择您的 XMPP 提供者 + 使用 conversations.im 创建新账户 - 您已经拥有一个XMPP账户了吗?如果您之前使用过其他的XMPP客户端的话,那么您已经拥有这种账户了。如果没有账户的话,您可以现在创建一个。\n提示:有些电子邮件服务也提供XMPP账户。 - XMPP是独立于提供程序的即时消息网络。 您可以将此客户端与所选的任何XMPP服务器一起使用。\ n不过,为了您的方便,我们很容易在对话中创建帐户。im; 特别适合与“对话”配合使用的提供商。 - 您已受邀参加%1$s。 我们将指导您完成创建帐户的过程。\n选择%1$s作为提供者后,您可以通过提供其他人的完整XMPP地址与其他提供者的用户进行交流。 - 您已受邀参加%1$s。 已经为您选择了一个用户名。 我们将指导您完成创建帐户的过程。\n您可以通过向其他提供商的用户提供完整的XMPP地址来与他们进行交流。 + 您有 XMPP 账户吗?如果您之前使用过其他的 XMPP 客户端,那么您已经拥有这种账户了。如果没有的话,您现在可以创建一个。\n提示:有些电子邮件服务也提供XMPP账户。 + XMPP 是独立于提供者的即时消息网络。您可以将此客户端与任意 XMPP 服务器一同使用。\n不过,您可以很容易地在 conversations.im 上创建账户;它是特别适合与“Conversations”一起使用的提供者。 + 您已受邀加入 %1$s。我们将指导您完成创建帐户的过程。\n使用 %1$s 作为提供者时,您可以通过您的完整 XMPP 地址与其他提供者的用户进行交流。 + 您已受邀加入 %1$s。已为您选择了一个用户名。我们将指导您完成创建帐户的过程。\n您可以使用完整的 XMPP 地址来与其他提供者的用户进行交流。 你的服务器邀请 格式不正确的配置代码 点击分享按钮向您的联系人发送加入 %1$s 的邀请。 diff --git a/src/main/res/values-pt-rBR/strings.xml b/src/main/res/values-pt-rBR/strings.xml index a5c8c8bed541a89399ee95aac02630808be22567..67ffdbedf3275827b582db9f2995f78a9f7f37d4 100644 --- a/src/main/res/values-pt-rBR/strings.xml +++ b/src/main/res/values-pt-rBR/strings.xml @@ -920,6 +920,8 @@ Fazer chamada Recebendo chamada Recebendo chamada de vídeo + Mudar para videochamada? + Adicionar outras trilhas? Conectando Conectado Reconectando @@ -1011,4 +1013,7 @@ Falha temporária na autenticação Excluir avatar As chamadas estão desabilitadas ao usar Tor - + Mudar para vídeo + Recusar requisição de mudança para vídeo + + From bb52962f0d6f7c8dcf36db69231e7a0f640a4a8e Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 5 Dec 2022 15:40:07 +0100 Subject: [PATCH 099/101] delay candidates until after session-init/accept --- .../siacs/conversations/xmpp/jingle/JingleRtpConnection.java | 5 ++--- 1 file changed, 2 insertions(+), 3 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 6e14fc56e50fd34a848d8e39fe47338776ea44f8..2581088ca53ae7a4b860c3b6de0f09ee28d8f159 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -1262,7 +1262,6 @@ public class JingleRtpConnection extends AbstractJingleConnection final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription, false); this.responderRtpContentMap = respondingRtpContentMap; storePeerDtlsSetup(respondingRtpContentMap.getDtlsSetup().flip()); - webRTCWrapper.setIsReadyToReceiveIceCandidates(true); final ListenableFuture outgoingContentMapFuture = prepareOutgoingContentMap(respondingRtpContentMap); Futures.addCallback( @@ -1271,6 +1270,7 @@ public class JingleRtpConnection extends AbstractJingleConnection @Override public void onSuccess(final RtpContentMap outgoingContentMap) { sendSessionAccept(outgoingContentMap); + webRTCWrapper.setIsReadyToReceiveIceCandidates(true); } @Override @@ -1713,8 +1713,6 @@ public class JingleRtpConnection extends AbstractJingleConnection SessionDescription.parse(webRTCSessionDescription.description); final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription, true); this.initiatorRtpContentMap = rtpContentMap; - //TODO delay ready to receive ice until after session-init - this.webRTCWrapper.setIsReadyToReceiveIceCandidates(true); final ListenableFuture outgoingContentMapFuture = encryptSessionInitiate(rtpContentMap); Futures.addCallback( @@ -1723,6 +1721,7 @@ public class JingleRtpConnection extends AbstractJingleConnection @Override public void onSuccess(final RtpContentMap outgoingContentMap) { sendSessionInitiate(outgoingContentMap, targetState); + webRTCWrapper.setIsReadyToReceiveIceCandidates(true); } @Override From 0a133b6c4c07e0402500c5c00ea0a437a9fcb50e Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 10 Dec 2022 08:50:52 +0100 Subject: [PATCH 100/101] =?UTF-8?q?temporarily=20use=20Snikket=E2=80=99s?= =?UTF-8?q?=20build=20of=20WebRTC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index c5f1ba1d8cf9ec8fa33e545eea68a720e2ade2a5..936c4b706090efda995bc1e68c33ab9447d79648 100644 --- a/build.gradle +++ b/build.gradle @@ -76,7 +76,8 @@ dependencies { implementation 'com.google.guava:guava:31.1-android' quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.12.49' - freeImplementation 'ch.threema:webrtc-android:100.0.0' + // temporarily use Snikket’s build of WebRTC. The next release will use our own build + freeImplementation 'org.snikket:webrtc-android:107.0.0' playstoreImplementation fileTree(include: ['libwebrtc-m107.aar'], dir: 'libs') } From f8517612524426821357ea71d34a9e04ec654a70 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 10 Dec 2022 08:51:26 +0100 Subject: [PATCH 101/101] version bump to 2.11.0 --- build.gradle | 4 ++-- fastlane/metadata/android/en-US/changelogs/42041.txt | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/42041.txt diff --git a/build.gradle b/build.gradle index 936c4b706090efda995bc1e68c33ab9447d79648..fa4a0aca0da87a8cfca4f2d9d0cae2e8de8cdedb 100644 --- a/build.gradle +++ b/build.gradle @@ -94,8 +94,8 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion 32 - versionCode 42040 - versionName "2.11.0-beta.2" + versionCode 42041 + versionName "2.11.0" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId diff --git a/fastlane/metadata/android/en-US/changelogs/42041.txt b/fastlane/metadata/android/en-US/changelogs/42041.txt new file mode 100644 index 0000000000000000000000000000000000000000..302e9719b1b0c4db9306a087930f7e0911b7948c --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/42041.txt @@ -0,0 +1,5 @@ +* Implement Extensible SASL Profile, Bind 2.0 and Fast for faster reconnects +* Implement Channel Binding +* Add ability to switch from audio call to video call +* Add ability to delete own avatar +* Add notification for missed calls