diff --git a/.travis.yml b/.travis.yml index fb2c6866e..ac5f1ff91 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,6 +10,7 @@ android: licenses: - '.+' before_script: + - mkdir libs - wget -O libs/libwebrtc-m81.aar http://gultsch.de/files/libwebrtc-m81.aar script: - ./gradlew assembleConversationsFreeSystemRelease diff --git a/CHANGELOG.md b/CHANGELOG.md index fd5c26115..009e193ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +### Version 2.8.5 + +* Reduce echo during calls on some devices +* Fix login when passwords contains special characters +* Play dial and busy tones on speaker during video calls + ### Version 2.8.4 * Rework Login with certificate UI diff --git a/README.md b/README.md index 5b4f53089..e43c9fe4b 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ * End-to-end encryption with [OMEMO](http://conversations.im/omemo/) or [OpenPGP](http://openpgp.org/about/) * Send and receive images as well as other kind of files -* Make audio and video calls +* Encrypted audio and video calls (DLTS-SRTP) * Share your location * Send voice messages * Indication when your contact has read your message @@ -361,7 +361,7 @@ There are XMPP Clients available for all major platforms. #### Windows / Linux For your desktop computer we recommend that you use [Gajim](https://gajim.org). You need to install the plugins `OMEMO`, `HTTP Upload` and `URL image preview` to get the best compatibility with Conversations. Plugins can be installed from within the app. #### iOS -Unfortunately we don‘t have a recommendation for iPhones right now. There are two clients available [ChatSecure](https://chatsecure.org/) and [Monal](https://monal.im/). Both with their own pros and cons. +Unfortunately we don‘t have a recommendation for iPhones right now. There are three clients available [Siskin](https://siskin.im/), [ChatSecure](https://chatsecure.org/) and [Monal](https://monal.im/). Each with their own pros and cons. ### Development @@ -385,25 +385,6 @@ There are two build flavors available. *free* and *playstore*. Unless you know w [![Build Status](https://dev.sum7.eu/sum7/Conversations/badges/develop/build.svg)](https://dev.sum7.eu/sum7/Conversations/pipelines) -#### How do I update/add external libraries? - -If the library you want to update is in Maven Central or JCenter (or has its own -Maven repo), add it or update its version in `build.gradle`. If the library is -in the `libs/` directory, you can update it using a subtree merge by doing the -following (using `minidns` as an example): - - git remote add minidns https://github.com/rtreffer/minidns.git - git fetch minidns - git merge -s subtree minidns master - -To add a new dependency to the `libs/` directory (replacing "name", "branch" and -"url" as necessary): - - git remote add name url - git merge -s ours --no-commit name/branch - git read-tree --prefix=libs/name -u name/branch - git commit -m "Subtree merged in name" - #### How do I debug Conversations If something goes wrong Conversations usually exposes very little information in diff --git a/build.gradle b/build.gradle index eb4cf02a5..2bdfc67c3 100644 --- a/build.gradle +++ b/build.gradle @@ -77,7 +77,7 @@ dependencies { implementation "com.squareup.retrofit2:retrofit:2.6.4" implementation "com.squareup.retrofit2:converter-gson:2.6.4" //okhttp needs to stick with 3.12.x - implementation 'com.squareup.okhttp3:okhttp:3.12.10' + implementation 'com.squareup.okhttp3:okhttp:3.12.12' implementation 'com.google.guava:guava:27.1-android' quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.11.1' //implementation fileTree(include: ['libwebrtc-m81.aar'], dir: 'libs') @@ -96,8 +96,8 @@ android { defaultConfig { minSdkVersion 16 targetSdkVersion 25 - versionCode 387 - versionName "2.8.4" + versionCode 388 + versionName "2.8.5" archivesBaseName += "-$versionName" applicationId "eu.sum7.conversations" resValue "string", "applicationId", applicationId diff --git a/metadata/en-US/changelogs/388.txt b/metadata/en-US/changelogs/388.txt new file mode 100644 index 000000000..3dc6a9b9d --- /dev/null +++ b/metadata/en-US/changelogs/388.txt @@ -0,0 +1,3 @@ +• Reduce echo during calls on some devices +• Fix login when passwords contains special characters +• Play dial and busy tones on speaker during video calls diff --git a/metadata/en-US/description.txt b/metadata/en-US/description.txt index 51cffed45..432235d8d 100644 --- a/metadata/en-US/description.txt +++ b/metadata/en-US/description.txt @@ -4,8 +4,7 @@ Changes to origin: • replace the hardcoded IPv4 preference to easy Happy Eyeball, for faster connection and fair to both IP version. • rebrands it as chat.sum7.eu (to run both version together) -Easy to use, reliable, battery friendly. With built-in support for images, group -chats and e2e encryption. +Easy to use, reliable, battery friendly. With built-in support for images, group chats and e2e encryption. Design principles: @@ -18,6 +17,7 @@ Features: • End-to-end encryption with either OMEMO or OpenPGP • Sending and receiving images +• Encrypted audio and video calls (DLTS-SRTP) • Intuitive UI that follows Android Design guidelines • Pictures / Avatars for your Contacts • Syncs with desktop client @@ -26,19 +26,11 @@ Features: • Multiple accounts / unified inbox • Very low impact on battery life -Conversations makes it very easy to create an account on the chat.sum7.eu -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. +Conversations makes it very easy to create an account on the chat.sum7.eu 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: -Conversations works with every XMPP server out there. However XMPP is an -extensible protocol. These extensions are standardized as well in so called -XEP’s. Conversations supports a couple of those to make the overall user -experience better. There is a chance that your current XMPP server does not -support these extensions. Therefore to get the most out of Conversations you -should consider either switching to an XMPP server that does or - even better - -run your own XMPP server for you and your friends. +Conversations works with every XMPP server out there. However XMPP is an extensible protocol. These extensions are standardized as well in so called XEP’s. Conversations supports a couple of those to make the overall user experience better. There is a chance that your current XMPP server does not support these extensions. Therefore to get the most out of Conversations you should consider either switching to an XMPP server that does or - even better - run your own XMPP server for you and your friends. These XEPs are - as of now: diff --git a/src/conversations/java/eu/siacs/conversations/ui/ManageAccountActivity.java b/src/conversations/java/eu/siacs/conversations/ui/ManageAccountActivity.java index 86bdd5501..1c127a877 100644 --- a/src/conversations/java/eu/siacs/conversations/ui/ManageAccountActivity.java +++ b/src/conversations/java/eu/siacs/conversations/ui/ManageAccountActivity.java @@ -85,7 +85,7 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda String jid = savedInstanceState.getString(STATE_SELECTED_ACCOUNT); if (jid != null) { try { - this.selectedAccountJid = Jid.of(jid); + this.selectedAccountJid = Jid.ofEscaped(jid); } catch (IllegalArgumentException e) { this.selectedAccountJid = null; } @@ -111,7 +111,7 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda @Override public void onSaveInstanceState(final Bundle savedInstanceState) { if (selectedAccount != null) { - savedInstanceState.putString(STATE_SELECTED_ACCOUNT, selectedAccount.getJid().asBareJid().toString()); + savedInstanceState.putString(STATE_SELECTED_ACCOUNT, selectedAccount.getJid().asBareJid().toEscapedString()); } super.onSaveInstanceState(savedInstanceState); } @@ -286,7 +286,7 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda private void publishAvatar(Account account) { Intent intent = new Intent(getApplicationContext(), PublishProfilePictureActivity.class); - intent.putExtra(EXTRA_ACCOUNT, account.getJid().toString()); + intent.putExtra(EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString()); startActivity(intent); } diff --git a/src/conversations/java/eu/siacs/conversations/ui/WelcomeActivity.java b/src/conversations/java/eu/siacs/conversations/ui/WelcomeActivity.java index daf6dd995..d2b2e58a1 100644 --- a/src/conversations/java/eu/siacs/conversations/ui/WelcomeActivity.java +++ b/src/conversations/java/eu/siacs/conversations/ui/WelcomeActivity.java @@ -62,7 +62,7 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi if (xmppUri.isAction(XmppUri.ACTION_REGISTER)) { intent = SignupUtils.getTokenRegistrationIntent(this, jid, preauth); } else if (xmppUri.isAction(XmppUri.ACTION_ROSTER) && "y".equals(xmppUri.getParameter("ibr"))) { - intent = SignupUtils.getTokenRegistrationIntent(this, Jid.ofDomain(jid.getDomain()), preauth); + intent = SignupUtils.getTokenRegistrationIntent(this, jid.getDomain(), preauth); intent.putExtra(StartConversationActivity.EXTRA_INVITE_URI, xmppUri.toString()); } else { intent = 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 f502ad9b8..6949daec4 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java @@ -34,11 +34,11 @@ abstract class ScramMechanism extends SaslMechanism { // Map keys are "bytesToHex(JID),bytesToHex(password),bytesToHex(salt),iterations,SASL-Mechanism". // Changing any of these values forces a cache miss. `CryptoHelper.bytesToHex()' // is applied to prevent commas in the strings breaking things. - final String[] kparts = k.split(",", 5); + final String[] kParts = k.split(",", 5); try { final byte[] saltedPassword, serverKey, clientKey; - saltedPassword = hi(CryptoHelper.hexToString(kparts[1]).getBytes(), - Base64.decode(CryptoHelper.hexToString(kparts[2]), Base64.DEFAULT), Integer.valueOf(kparts[3])); + saltedPassword = hi(CryptoHelper.hexToString(kParts[1]).getBytes(), + Base64.decode(CryptoHelper.hexToString(kParts[2]), Base64.DEFAULT), Integer.parseInt(kParts[3])); serverKey = hmac(saltedPassword, SERVER_KEY_BYTES); clientKey = hmac(saltedPassword, CLIENT_KEY_BYTES); @@ -173,10 +173,10 @@ abstract class ScramMechanism extends SaslMechanism { // Map keys are "bytesToHex(JID),bytesToHex(password),bytesToHex(salt),iterations,SASL-Mechanism". final KeyPair keys = CACHE.get( - CryptoHelper.bytesToHex(account.getJid().asBareJid().toEscapedString().getBytes()) + "," - + CryptoHelper.bytesToHex(account.getPassword().getBytes()) + "," + CryptoHelper.bytesToHex(CryptoHelper.saslPrep(account.getJid().asBareJid().toEscapedString()).getBytes()) + "," + + CryptoHelper.bytesToHex(CryptoHelper.saslPrep(account.getPassword()).getBytes()) + "," + CryptoHelper.bytesToHex(salt.getBytes()) + "," - + String.valueOf(iterationCount) + "," + + iterationCount + "," + getMechanism() ); if (keys == null) { diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java index d95dae80b..d37646f78 100644 --- a/src/main/java/eu/siacs/conversations/entities/Contact.java +++ b/src/main/java/eu/siacs/conversations/entities/Contact.java @@ -143,6 +143,16 @@ public class Contact implements ListItem, Blockable { } } + public String getPublicDisplayName() { + if (!TextUtils.isEmpty(this.presenceName)) { + return this.presenceName; + } else if (jid.getLocal() != null) { + return JidHelper.localPartOrFallback(jid); + } else { + return jid.getDomain().toEscapedString(); + } + } + public String getProfilePhoto() { return this.photoUri; } @@ -468,7 +478,7 @@ public class Contact implements ListItem, Blockable { } boolean isOwnServer() { - return account.getJid().getDomain().equals(jid.asBareJid().toString()); + return account.getJid().getDomain().equals(jid.asBareJid()); } public void setCommonName(String cn) { diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index 567d694eb..4263a6ef8 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -643,8 +643,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable !this.isOOb() && !message.treatAsDownloadable() && !this.treatAsDownloadable() && - !message.getBody().startsWith(ME_COMMAND) && - !this.getBody().startsWith(ME_COMMAND) && + !message.hasMeCommand() && + !this.hasMeCommand() && !this.bodyIsOnlyEmojis() && !message.bodyIsOnlyEmojis() && ((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint())) && diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index 1a4454cc5..e05cbae71 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -217,7 +217,7 @@ public class MessageGenerator extends AbstractGenerator { Element x = new Element("x"); x.setAttribute("xmlns", "http://jabber.org/protocol/muc#user"); Element invite = new Element("invite"); - invite.setAttribute("to", contact.asBareJid().toString()); + invite.setAttribute("to", contact.asBareJid()); x.addChild(invite); packet.addChild(x); return packet; diff --git a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java index 5136481a7..6068af5ad 100644 --- a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java +++ b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java @@ -42,7 +42,7 @@ public abstract class AbstractParser { for(Element child : element.getChildren()) { if ("delay".equals(child.getName()) && "urn:xmpp:delay".equals(child.getNamespace())) { final Jid f = to == null ? null : InvalidJid.getNullForInvalid(child.getAttributeAsJid("from")); - if (f != null && (to.asBareJid().equals(f) || to.getDomain().equals(f.toString()))) { + if (f != null && (to.asBareJid().equals(f) || to.getDomain().equals(f))) { continue; } final String stamp = child.getAttribute("stamp"); diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 40bd1b370..37cf7ed33 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -452,7 +452,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece } if ((body != null || pgpEncrypted != null || (axolotlEncrypted != null && axolotlEncrypted.hasChild("payload")) || oobUrl != null || xP1S3 != null) && !isMucStatusMessage) { - final boolean conversationIsProbablyMuc = isTypeGroupChat || mucUserElement != null || account.getXmppConnection().getMucServersWithholdAccount().contains(counterpart.getDomain()); + final boolean conversationIsProbablyMuc = isTypeGroupChat || mucUserElement != null || account.getXmppConnection().getMucServersWithholdAccount().contains(counterpart.getDomain().toEscapedString()); final Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, counterpart.asBareJid(), conversationIsProbablyMuc, false, query, false); final boolean conversationMultiMode = conversation.getMode() == Conversation.MODE_MULTI; @@ -837,13 +837,13 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece if (Namespace.JINGLE_MESSAGE.equals(child.getNamespace()) && JINGLE_MESSAGE_ELEMENT_NAMES.contains(child.getName())) { final String action = child.getName(); if (query == null) { - if (!account.getJid().asBareJid().equals(from.asBareJid())) { - processMessageReceipts(account, packet, query); - } if (serverMsgId == null) { serverMsgId = extractStanzaId(account, packet); } mXmppConnectionService.getJingleConnectionManager().deliverMessage(account, packet.getTo(), packet.getFrom(), child, remoteMsgId, serverMsgId, timestamp); + if (!account.getJid().asBareJid().equals(from.asBareJid())) { + processMessageReceipts(account, packet, query); + } } else if (query.isCatchup()) { final String sessionId = child.getAttribute("id"); if (sessionId == null) { diff --git a/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java b/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java index c65f6a17b..144341b8e 100644 --- a/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java +++ b/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java @@ -51,7 +51,6 @@ public class AppRTCAudioManager { @Nullable private AudioManagerEvents audioManagerEvents; private AudioManagerState amState; - private int savedAudioMode = AudioManager.MODE_INVALID; private boolean savedIsSpeakerPhoneOn; private boolean savedIsMicrophoneMute; private boolean hasWiredHeadset; @@ -178,21 +177,17 @@ public class AppRTCAudioManager { } @SuppressWarnings("deprecation") - // TODO(henrika): audioManager.requestAudioFocus() is deprecated. public void start(AudioManagerEvents audioManagerEvents) { - Log.d(Config.LOGTAG, "start"); + Log.d(Config.LOGTAG, AppRTCAudioManager.class.getName() + ".start()"); ThreadUtils.checkIsOnMainThread(); if (amState == AudioManagerState.RUNNING) { Log.e(Config.LOGTAG, "AudioManager is already active"); return; } awaitMicrophoneLatch(); - // TODO(henrika): perhaps call new method called preInitAudio() here if UNINITIALIZED. - Log.d(Config.LOGTAG, "AudioManager starts..."); this.audioManagerEvents = audioManagerEvents; amState = AudioManagerState.RUNNING; // Store current audio state so we can restore it when stop() is called. - savedAudioMode = audioManager.getMode(); savedIsSpeakerPhoneOn = audioManager.isSpeakerphoneOn(); savedIsMicrophoneMute = audioManager.isMicrophoneMute(); hasWiredHeadset = hasWiredHeadset(); @@ -280,9 +275,8 @@ public class AppRTCAudioManager { } @SuppressWarnings("deprecation") - // TODO(henrika): audioManager.abandonAudioFocus() is deprecated. public void stop() { - Log.d(Config.LOGTAG, "stop"); + Log.d(Config.LOGTAG, AppRTCAudioManager.class.getName() + ".stop()"); ThreadUtils.checkIsOnMainThread(); if (amState != AudioManagerState.RUNNING) { Log.e(Config.LOGTAG, "Trying to stop AudioManager in incorrect state: " + amState); @@ -294,7 +288,7 @@ public class AppRTCAudioManager { // Restore previously stored audio states. setSpeakerphoneOn(savedIsSpeakerPhoneOn); setMicrophoneMute(savedIsMicrophoneMute); - audioManager.setMode(savedAudioMode); + audioManager.setMode(AudioManager.MODE_NORMAL); // Abandon audio focus. Gives the previous focus owner, if any, focus. audioManager.abandonAudioFocus(audioFocusChangeListener); audioFocusChangeListener = null; @@ -304,7 +298,6 @@ public class AppRTCAudioManager { proximitySensor = null; } audioManagerEvents = null; - Log.d(Config.LOGTAG, "AudioManager stopped"); } /** @@ -318,11 +311,7 @@ public class AppRTCAudioManager { setSpeakerphoneOn(true); break; case EARPIECE: - setSpeakerphoneOn(false); - break; case WIRED_HEADSET: - setSpeakerphoneOn(false); - break; case BLUETOOTH: setSpeakerphoneOn(false); break; diff --git a/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java b/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java index dd390eb1e..19c0085a0 100644 --- a/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java +++ b/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java @@ -222,7 +222,7 @@ public class ChannelDiscoveryService { continue; } for (final String mucService : xmppConnection.getMucServers()) { - Jid jid = Jid.of(mucService); + Jid jid = Jid.ofEscaped(mucService); if (!localMucServices.containsKey(jid)) { localMucServices.put(jid, account); } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 694a49bed..8ea3ed2ea 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -1710,12 +1710,7 @@ public class XmppConnectionService extends Service { for (Bookmark bookmark : account.getBookmarks()) { storage.addChild(bookmark); } - pushNodeAndEnforcePublishOptions(account, Namespace.BOOKMARKS, storage, PublishOptions.persistentWhitelistAccess()); - - } - - private void pushNodeAndEnforcePublishOptions(final Account account, final String node, final Element element, final Bundle options) { - pushNodeAndEnforcePublishOptions(account, node, element, null, options, true); + pushNodeAndEnforcePublishOptions(account, Namespace.BOOKMARKS, storage, "current", PublishOptions.persistentWhitelistAccess()); } diff --git a/src/main/java/eu/siacs/conversations/ui/BlockContactDialog.java b/src/main/java/eu/siacs/conversations/ui/BlockContactDialog.java index f9858874a..2e3db1730 100644 --- a/src/main/java/eu/siacs/conversations/ui/BlockContactDialog.java +++ b/src/main/java/eu/siacs/conversations/ui/BlockContactDialog.java @@ -29,9 +29,9 @@ public final class BlockContactDialog { builder.setTitle(isBlocked ? R.string.action_unblock_participant : R.string.action_block_participant); value = blockable.getJid().toEscapedString(); res = isBlocked ? R.string.unblock_contact_text : R.string.block_contact_text; - } else if (blockable.getJid().getLocal() == null || blockable.getAccount().isBlocked(Jid.ofDomain(blockable.getJid().getDomain()))) { + } else if (blockable.getJid().getLocal() == null || blockable.getAccount().isBlocked(blockable.getJid().getDomain())) { builder.setTitle(isBlocked ? R.string.action_unblock_domain : R.string.action_block_domain); - value = Jid.ofDomain(blockable.getJid().getDomain()).toString(); + value =blockable.getJid().getDomain().toEscapedString(); res = isBlocked ? R.string.unblock_domain_text : R.string.block_domain_text; } else { int resBlockAction = blockable instanceof Conversation && ((Conversation) blockable).isWithStranger() ? R.string.block_stranger : R.string.action_block_contact; diff --git a/src/main/java/eu/siacs/conversations/ui/BlocklistActivity.java b/src/main/java/eu/siacs/conversations/ui/BlocklistActivity.java index f0aef4741..201c5e7b7 100644 --- a/src/main/java/eu/siacs/conversations/ui/BlocklistActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/BlocklistActivity.java @@ -35,7 +35,7 @@ public class BlocklistActivity extends AbstractSearchableListItemActivity implem @Override public void onBackendConnected() { for (final Account account : xmppConnectionService.getAccounts()) { - if (account.getJid().toString().equals(getIntent().getStringExtra(EXTRA_ACCOUNT))) { + if (account.getJid().toEscapedString().equals(getIntent().getStringExtra(EXTRA_ACCOUNT))) { this.account = account; break; } diff --git a/src/main/java/eu/siacs/conversations/ui/ChooseAccountForProfilePictureActivity.java b/src/main/java/eu/siacs/conversations/ui/ChooseAccountForProfilePictureActivity.java index a7e0e5fcb..af1fb7656 100644 --- a/src/main/java/eu/siacs/conversations/ui/ChooseAccountForProfilePictureActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ChooseAccountForProfilePictureActivity.java @@ -73,7 +73,7 @@ public class ChooseAccountForProfilePictureActivity extends XmppActivity { final Uri uri = startIntent == null ? null : startIntent.getData(); if (uri != null) { Intent intent = new Intent(this, PublishProfilePictureActivity.class); - intent.putExtra(EXTRA_ACCOUNT, account.getJid().asBareJid().toString()); + intent.putExtra(EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString()); intent.setData(uri); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); try { diff --git a/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java b/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java index afda66709..bdd19a623 100644 --- a/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java @@ -75,7 +75,7 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity im intent.putExtra(EXTRA_CONVERSATION, conversation.getUuid()); intent.putExtra(EXTRA_SELECT_MULTIPLE, true); intent.putExtra(EXTRA_SHOW_ENTER_JID, true); - intent.putExtra(EXTRA_ACCOUNT, conversation.getAccount().getJid().asBareJid().toString()); + intent.putExtra(EXTRA_ACCOUNT, conversation.getAccount().getJid().asBareJid().toEscapedString()); return intent; } @@ -321,7 +321,7 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity im final Intent request = getIntent(); final Intent data = new Intent(); data.putExtra("contact", contactJid.toString()); - data.putExtra(EXTRA_ACCOUNT, accountJid.toString()); + data.putExtra(EXTRA_ACCOUNT, accountJid.toEscapedString()); data.putExtra(EXTRA_SELECT_MULTIPLE, false); copy(request, data); setResult(RESULT_OK, data); @@ -401,7 +401,7 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity im data.putExtra("contact", item.getJid().toString()); String account = request.getStringExtra(EXTRA_ACCOUNT); if (account == null && item instanceof Contact) { - account = ((Contact) item).getAccount().getJid().asBareJid().toString(); + account = ((Contact) item).getAccount().getJid().asBareJid().toEscapedString(); } data.putExtra(EXTRA_ACCOUNT, account); data.putExtra(EXTRA_SELECT_MULTIPLE, false); diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index caeb1e6e2..15190d08c 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -188,11 +188,11 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp showInactiveOmemo = savedInstanceState != null && savedInstanceState.getBoolean("show_inactive_omemo", false); if (getIntent().getAction().equals(ACTION_VIEW_CONTACT)) { try { - this.accountJid = Jid.of(getIntent().getExtras().getString(EXTRA_ACCOUNT)); + this.accountJid = Jid.ofEscaped(getIntent().getExtras().getString(EXTRA_ACCOUNT)); } catch (final IllegalArgumentException ignored) { } try { - this.contactJid = Jid.of(getIntent().getExtras().getString("contact")); + this.contactJid = Jid.ofEscaped(getIntent().getExtras().getString("contact")); } catch (final IllegalArgumentException ignored) { } } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 56c16b31f..6308d74eb 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -772,7 +772,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke contacts[i] = targets.get(i).toString(); } intent.putExtra("contacts", contacts); - intent.putExtra(EXTRA_ACCOUNT, conversation.getAccount().getJid().asBareJid().toString()); + intent.putExtra(EXTRA_ACCOUNT, conversation.getAccount().getJid().asBareJid().toEscapedString()); intent.putExtra("conversation", conversation.getUuid()); startActivityForResult(intent, requestCode); return true; @@ -1313,6 +1313,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke final boolean pinned = conversation.getBooleanAttribute(Conversation.ATTRIBUTE_PINNED_ON_TOP, false); conversation.setAttribute(Conversation.ATTRIBUTE_PINNED_ON_TOP, !pinned); activity.xmppConnectionService.updateConversation(conversation); + activity.invalidateOptionsMenu(); } private void checkPermissionAndTriggerAudioCall() { @@ -2205,7 +2206,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke updateSnackBar(conversation); return true; case R.id.block_domain: - blockable = conversation.getAccount().getRoster().getContact(Jid.ofDomain(jid.getDomain())); + blockable = conversation.getAccount().getRoster().getContact(jid.getDomain()); break; default: blockable = conversation; diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index 89de15d0b..d192fa0c6 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -121,7 +121,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat public void onClick(final View view) { if (mAccount != null) { final Intent intent = new Intent(getApplicationContext(), PublishProfilePictureActivity.class); - intent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().asBareJid().toString()); + intent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().asBareJid().toEscapedString()); startActivity(intent); } } @@ -409,7 +409,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat if (mAccount.isOptionSet(Account.OPTION_FIXED_USERNAME)) { preset = jid.asBareJid(); } else { - preset = Jid.ofDomain(jid.getDomain()); + preset = jid.getDomain(); } final Intent intent = SignupUtils.getTokenRegistrationIntent(this, preset, mAccount.getKey(Account.PRE_AUTH_REGISTRATION_TOKEN)); StartConversationActivity.addInviteUri(intent, getIntent()); @@ -445,7 +445,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat intent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().asBareJid().toEscapedString()); } else { intent = new Intent(getApplicationContext(), PublishProfilePictureActivity.class); - intent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().asBareJid().toString()); + intent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().asBareJid().toEscapedString()); intent.putExtra("setup", true); } if (wasFirstAccount) { @@ -693,7 +693,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat recreate(); } else if (intent != null) { try { - this.jidToEdit = Jid.of(intent.getStringExtra("jid")); + this.jidToEdit = Jid.ofEscaped(intent.getStringExtra("jid")); } catch (final IllegalArgumentException | NullPointerException ignored) { this.jidToEdit = null; } @@ -754,7 +754,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat @Override public void onSaveInstanceState(final Bundle savedInstanceState) { if (mAccount != null) { - savedInstanceState.putString("account", mAccount.getJid().asBareJid().toString()); + savedInstanceState.putString("account", mAccount.getJid().asBareJid().toEscapedString()); savedInstanceState.putBoolean("initMode", mInitMode); savedInstanceState.putBoolean("showMoreTable", binding.serverInfoMore.getVisibility() == View.VISIBLE); } @@ -765,7 +765,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat boolean init = true; if (mSavedInstanceAccount != null) { try { - this.mAccount = xmppConnectionService.findAccountByJid(Jid.of(mSavedInstanceAccount)); + this.mAccount = xmppConnectionService.findAccountByJid(Jid.ofEscaped(mSavedInstanceAccount)); this.mInitMode = mSavedInstanceInit; init = false; } catch (IllegalArgumentException e) { @@ -833,7 +833,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat break; case R.id.action_show_block_list: final Intent showBlocklistIntent = new Intent(this, BlocklistActivity.class); - showBlocklistIntent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().toString()); + showBlocklistIntent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().toEscapedString()); startActivity(showBlocklistIntent); break; case R.id.action_server_info_show_more: @@ -882,7 +882,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat private void gotoChangePassword(String newPassword) { final Intent changePasswordIntent = new Intent(this, ChangePasswordActivity.class); - changePasswordIntent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().toString()); + changePasswordIntent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().toEscapedString()); if (newPassword != null) { changePasswordIntent.putExtra("password", newPassword); } diff --git a/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java b/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java index 6446f5db6..3589bb41e 100644 --- a/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java @@ -165,7 +165,7 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer final Conversation conversation; Account account; try { - account = xmppConnectionService.findAccountByJid(Jid.of(share.account)); + account = xmppConnectionService.findAccountByJid(Jid.ofEscaped(share.account)); } catch (final IllegalArgumentException e) { account = null; } diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java index 57cc492e0..4d31d74d6 100644 --- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java @@ -1000,7 +1000,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne intent.putExtra(ChooseContactActivity.EXTRA_SHOW_ENTER_JID, false); intent.putExtra(ChooseContactActivity.EXTRA_SELECT_MULTIPLE, true); intent.putExtra(ChooseContactActivity.EXTRA_GROUP_CHAT_NAME, name.trim()); - intent.putExtra(ChooseContactActivity.EXTRA_ACCOUNT, account.getJid().asBareJid().toString()); + intent.putExtra(ChooseContactActivity.EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString()); intent.putExtra(ChooseContactActivity.EXTRA_TITLE_RES_ID, R.string.choose_participants); startActivityForResult(intent, REQUEST_CREATE_CONFERENCE); } diff --git a/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java b/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java index 0472429f6..ae63f493a 100644 --- a/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java @@ -100,7 +100,7 @@ public class UriHandlerActivity extends AppCompatActivity { return; } if (xmppUri.isAction(XmppUri.ACTION_ROSTER) && "y".equals(xmppUri.getParameter("ibr"))) { - intent = SignupUtils.getTokenRegistrationIntent(this, Jid.ofDomain(jid.getDomain()), preauth); + intent = SignupUtils.getTokenRegistrationIntent(this, jid.getDomain(), preauth); intent.putExtra(StartConversationActivity.EXTRA_INVITE_URI, xmppUri.toString()); startActivity(intent); return; diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java index 887d3374b..93196a8b0 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -522,8 +522,8 @@ public abstract class XmppActivity extends ActionBarActivity { public void switchToContactDetails(Contact contact, String messageFingerprint) { Intent intent = new Intent(this, ContactDetailsActivity.class); intent.setAction(ContactDetailsActivity.ACTION_VIEW_CONTACT); - intent.putExtra(EXTRA_ACCOUNT, contact.getAccount().getJid().asBareJid().toString()); - intent.putExtra("contact", contact.getJid().toString()); + intent.putExtra(EXTRA_ACCOUNT, contact.getAccount().getJid().asBareJid().toEscapedString()); + intent.putExtra("contact", contact.getJid().toEscapedString()); intent.putExtra("fingerprint", messageFingerprint); startActivity(intent); } @@ -538,7 +538,7 @@ public abstract class XmppActivity extends ActionBarActivity { public void switchToAccount(Account account, boolean init, String fingerprint) { Intent intent = new Intent(this, EditAccountActivity.class); - intent.putExtra("jid", account.getJid().asBareJid().toString()); + intent.putExtra("jid", account.getJid().asBareJid().toEscapedString()); intent.putExtra("init", init); if (init) { intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NO_ANIMATION); 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 4218a1c3b..e9daee6df 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -75,8 +75,8 @@ import eu.siacs.conversations.utils.MessageUtils; import eu.siacs.conversations.utils.StylingHelper; import eu.siacs.conversations.utils.TimeFrameUtils; import eu.siacs.conversations.utils.UIHelper; -import eu.siacs.conversations.xmpp.mam.MamReference; import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.mam.MamReference; public class MessageAdapter extends ArrayAdapter implements CopyTextView.CopyHandler { @@ -117,6 +117,10 @@ public class MessageAdapter extends ArrayAdapter implements CopyTextVie activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } + public void setVolumeControl(final int stream) { + activity.setVolumeControlStream(stream); + } + public void setOnContactPictureClicked(OnContactPictureClicked listener) { this.mOnContactPictureClickedListener = listener; } diff --git a/src/main/java/eu/siacs/conversations/ui/service/AudioPlayer.java b/src/main/java/eu/siacs/conversations/ui/service/AudioPlayer.java index 64c180c38..785f0ac01 100644 --- a/src/main/java/eu/siacs/conversations/ui/service/AudioPlayer.java +++ b/src/main/java/eu/siacs/conversations/ui/service/AudioPlayer.java @@ -363,12 +363,13 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti if (AudioPlayer.player == null || !AudioPlayer.player.isPlaying()) { return; } - int streamType; + final int streamType; if (event.values[0] < 5f && event.values[0] != proximitySensor.getMaximumRange()) { streamType = AudioManager.STREAM_VOICE_CALL; } else { streamType = AudioManager.STREAM_MUSIC; } + messageAdapter.setVolumeControl(streamType); double position = AudioPlayer.player.getCurrentPosition(); double duration = AudioPlayer.player.getDuration(); double progress = position / duration; @@ -407,6 +408,7 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti wakeLock.release(); } } + messageAdapter.setVolumeControl(AudioManager.STREAM_MUSIC); } private ViewHolder getCurrentViewHolder() { diff --git a/src/main/java/eu/siacs/conversations/utils/MessageUtils.java b/src/main/java/eu/siacs/conversations/utils/MessageUtils.java index e5e4fbd97..e8ae07e68 100644 --- a/src/main/java/eu/siacs/conversations/utils/MessageUtils.java +++ b/src/main/java/eu/siacs/conversations/utils/MessageUtils.java @@ -29,10 +29,13 @@ package eu.siacs.conversations.utils; +import com.google.common.base.Strings; + import java.net.MalformedURLException; import java.net.URL; import java.util.regex.Pattern; +import eu.siacs.conversations.entities.Conversational; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.http.AesGcmURLStreamHandler; import eu.siacs.conversations.http.P1S3UrlStreamHandler; @@ -45,7 +48,22 @@ public class MessageUtils { public static String prepareQuote(Message message) { final StringBuilder builder = new StringBuilder(); - final String body = message.getMergedBody().toString(); + final String body; + if (message.hasMeCommand()) { + final String nick; + if (message.getStatus() == Message.STATUS_RECEIVED) { + if (message.getConversation().getMode() == Conversational.MODE_MULTI) { + nick = Strings.nullToEmpty(message.getCounterpart().getResource()); + } else { + nick = message.getContact().getPublicDisplayName(); + } + } else { + nick = UIHelper.getMessageDisplayName(message); + } + body = nick + " " + message.getBody().substring(Message.ME_COMMAND.length()); + } else { + body = message.getMergedBody().toString();; + } for (String line : body.split("\n")) { if (line.length() <= 0) { continue; diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 7172be812..aa6323f68 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -111,7 +111,7 @@ public class XmppConnection implements Runnable { public void onIqPacketReceived(Account account, IqPacket 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"); + 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( @@ -272,7 +272,7 @@ 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) { @@ -356,12 +356,8 @@ public class XmppConnection implements Runnable { this.changeStatus(Account.State.SERVER_NOT_FOUND); } catch (final SocksSocketFactory.SocksProxyNotFoundException e) { this.changeStatus(Account.State.TOR_NOT_AVAILABLE); - } catch (final IOException e) { - Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": socket io :" + e.getMessage()); - this.changeStatus(Account.State.OFFLINE); - this.attempt = Math.max(0, this.attempt - 1); - } catch (final XmlPullParserException e) { - Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": xml parser :" + e.getMessage()); + } catch (final IOException | XmlPullParserException e) { + Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": " + e.getMessage()); this.changeStatus(Account.State.OFFLINE); this.attempt = Math.max(0, this.attempt - 1); } finally { @@ -567,7 +563,7 @@ 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); @@ -812,7 +808,7 @@ public class XmppConnection implements Runnable { 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)) { @@ -831,7 +827,7 @@ public class XmppConnection implements Runnable { if (this.streamFeatures.hasChild("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); } } @@ -847,7 +843,7 @@ public class XmppConnection implements Runnable { saslMechanism = new ScramSha256(tagWriter, account, mXmppConnectionService.getRNG()); } else if (mechanisms.contains("SCRAM-SHA-1")) { saslMechanism = new ScramSha1(tagWriter, account, mXmppConnectionService.getRNG()); - } else if (mechanisms.contains("PLAIN") && !account.getJid().getDomain().equals("nimbuzz.com")) { + } else if (mechanisms.contains("PLAIN") && !account.getJid().getDomain().toEscapedString().equals("nimbuzz.com")) { saslMechanism = new Plain(tagWriter, account); } else if (mechanisms.contains("DIGEST-MD5")) { saslMechanism = new DigestMd5(tagWriter, account, mXmppConnectionService.getRNG()); @@ -870,7 +866,7 @@ public class XmppConnection implements Runnable { } tagWriter.writeElement(auth); } else { - 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); } } @@ -895,7 +891,7 @@ public class XmppConnection implements Runnable { sendRegistryRequest(); } else { final Element error = response.getError(); - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": failed to pre auth. "+error); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": failed to pre auth. " + error); throw new StateChangingError(Account.State.REGISTRATION_INVALID_TOKEN); } }, true); @@ -1132,7 +1128,7 @@ 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())) { + if (smVersion == 0 || 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 { @@ -1218,10 +1214,10 @@ public class XmppConnection implements Runnable { 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")); - } + 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")); + } }); } @@ -1315,7 +1311,8 @@ public class XmppConnection implements Runnable { } else if (streamError.hasChild("policy-violation")) { this.lastConnect = SystemClock.elapsedRealtime(); final String text = streamError.findChildContent("text"); - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": policy violation. "+text); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": policy violation. " + text); + failPendingMessages(text); throw new StateChangingException(Account.State.POLICY_VIOLATION); } else { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": stream error " + streamError.toString()); @@ -1323,6 +1320,24 @@ public class XmppConnection implements Runnable { } } + private void failPendingMessages(final String error) { + synchronized (this.mStanzaQueue) { + for (int i = 0; i < mStanzaQueue.size(); ++i) { + final AbstractAcknowledgeableStanza stanza = mStanzaQueue.valueAt(i); + if (stanza instanceof MessagePacket) { + 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); + } + } + } + } + private void sendStartStream() throws IOException { final Tag stream = Tag.start("stream:stream"); stream.setAttribute("to", account.getServer()); @@ -1880,7 +1895,7 @@ public class XmppConnection implements Runnable { } public boolean externalServiceDiscovery() { - return hasDiscoFeature(account.getDomain(),Namespace.EXTERNAL_SERVICE_DISCOVERY); + return hasDiscoFeature(account.getDomain(), Namespace.EXTERNAL_SERVICE_DISCOVERY); } } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 9ac971c2d..d8be779ef 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -38,6 +38,7 @@ import eu.siacs.conversations.services.AbstractConnectionManager; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; +import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.OnIqPacketReceived; import eu.siacs.conversations.xmpp.jingle.stanzas.Content; import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription; @@ -48,11 +49,10 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.Reason; import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; import eu.siacs.conversations.xmpp.stanzas.IqPacket; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; -import eu.siacs.conversations.xmpp.Jid; public class JingleConnectionManager extends AbstractConnectionManager { static final ScheduledExecutorService SCHEDULED_EXECUTOR_SERVICE = Executors.newSingleThreadScheduledExecutor(); - final ToneManager toneManager = new ToneManager(); + final ToneManager toneManager; private final HashMap rtpSessionProposals = new HashMap<>(); private final ConcurrentHashMap connections = new ConcurrentHashMap<>(); @@ -64,6 +64,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { public JingleConnectionManager(XmppConnectionService service) { super(service); + this.toneManager = new ToneManager(service); } static String nextRandomId() { @@ -333,11 +334,11 @@ public class JingleConnectionManager extends AbstractConnectionManager { } } } else if (addressedDirectly && "reject".equals(message.getName())) { - final RtpSessionProposal proposal = new RtpSessionProposal(account, from.asBareJid(), sessionId); + final RtpSessionProposal proposal = getRtpSessionProposal(account, from.asBareJid(), sessionId); synchronized (rtpSessionProposals) { - if (rtpSessionProposals.remove(proposal) != null) { + if (proposal != null && rtpSessionProposals.remove(proposal) != null) { writeLogMissedOutgoing(account, proposal.with, proposal.sessionId, serverMsgId, timestamp); - toneManager.transition(RtpEndUserState.DECLINED_OR_BUSY); + toneManager.transition(RtpEndUserState.DECLINED_OR_BUSY, proposal.media); mXmppConnectionService.notifyJingleRtpConnectionUpdate(account, proposal.with, proposal.sessionId, RtpEndUserState.DECLINED_OR_BUSY); } else { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": no rtp session proposal found for " + from + " to deliver reject"); @@ -511,7 +512,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { private void retractSessionProposal(RtpSessionProposal rtpSessionProposal) { final Account account = rtpSessionProposal.account; - toneManager.transition(RtpEndUserState.ENDED); + toneManager.transition(RtpEndUserState.ENDED, rtpSessionProposal.media); Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": retracting rtp session proposal with " + rtpSessionProposal.with); this.rtpSessionProposals.remove(rtpSessionProposal); final MessagePacket messagePacket = mXmppConnectionService.getMessageGenerator().sessionRetract(rtpSessionProposal); @@ -527,7 +528,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { final DeviceDiscoveryState preexistingState = entry.getValue(); if (preexistingState != null && preexistingState != DeviceDiscoveryState.FAILED) { final RtpEndUserState endUserState = preexistingState.toEndUserState(); - toneManager.transition(endUserState); + toneManager.transition(endUserState, media); mXmppConnectionService.notifyJingleRtpConnectionUpdate( account, with, @@ -623,7 +624,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { } this.rtpSessionProposals.put(sessionProposal, target); final RtpEndUserState endUserState = target.toEndUserState(); - toneManager.transition(endUserState); + toneManager.transition(endUserState, sessionProposal.media); mXmppConnectionService.notifyJingleRtpConnectionUpdate(account, sessionProposal.with, sessionProposal.sessionId, endUserState); Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": flagging session " + sessionId + " as " + target); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java index b4e67cd36..60ac67f14 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java @@ -1,10 +1,10 @@ package eu.siacs.conversations.xmpp.jingle; +import android.content.Context; import android.media.AudioManager; import android.media.ToneGenerator; import android.util.Log; -import java.util.Collections; import java.util.Set; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -16,27 +16,23 @@ import static java.util.Arrays.asList; class ToneManager { private final ToneGenerator toneGenerator; + private final Context context; private ToneState state = null; private ScheduledFuture currentTone; + private ScheduledFuture currentResetFuture; + private boolean appRtcAudioManagerHasControl = false; - ToneManager() { + ToneManager(final Context context) { ToneGenerator toneGenerator; try { - toneGenerator = new ToneGenerator(AudioManager.STREAM_MUSIC, 35); + toneGenerator = new ToneGenerator(AudioManager.STREAM_VOICE_CALL, 60); } catch (final RuntimeException e) { Log.e(Config.LOGTAG, "unable to instantiate ToneGenerator", e); toneGenerator = null; } this.toneGenerator = toneGenerator; - } - - void transition(final RtpEndUserState state) { - transition(of(true, state, Collections.emptySet())); - } - - void transition(final boolean isInitiator, final RtpEndUserState state, final Set media) { - transition(of(isInitiator, state, media)); + this.context = context; } private static ToneState of(final boolean isInitiator, final RtpEndUserState state, final Set media) { @@ -65,7 +61,15 @@ class ToneManager { return ToneState.NULL; } - private synchronized void transition(ToneState state) { + void transition(final RtpEndUserState state, final Set media) { + transition(of(true, state, media), media); + } + + void transition(final boolean isInitiator, final RtpEndUserState state, final Set media) { + transition(of(isInitiator, state, media), media); + } + + private synchronized void transition(ToneState state, final Set media) { if (this.state == state) { return; } @@ -74,6 +78,9 @@ class ToneManager { } cancelCurrentTone(); Log.d(Config.LOGTAG, getClass().getName() + ".transition(" + state + ")"); + if (state != ToneState.NULL) { + configureAudioManagerForCall(media); + } switch (state) { case RINGING: scheduleWaitingTone(); @@ -87,10 +94,21 @@ class ToneManager { case ENDING_CALL: scheduleEnding(); break; + case NULL: + if (noResetScheduled()) { + resetAudioManager(); + } + break; + default: + throw new IllegalStateException("Unable to handle transition to "+state); } this.state = state; } + void setAppRtcAudioManagerHasControl(final boolean appRtcAudioManagerHasControl) { + this.appRtcAudioManagerHasControl = appRtcAudioManagerHasControl; + } + private void scheduleConnected() { this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(() -> { startTone(ToneGenerator.TONE_PROP_PROMPT, 200); @@ -101,12 +119,14 @@ class ToneManager { this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(() -> { startTone(ToneGenerator.TONE_CDMA_CALLDROP_LITE, 375); }, 0, TimeUnit.SECONDS); + this.currentResetFuture = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(this::resetAudioManager, 375, TimeUnit.MILLISECONDS); } private void scheduleBusy() { this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(() -> { startTone(ToneGenerator.TONE_CDMA_NETWORK_BUSY, 2500); }, 0, TimeUnit.SECONDS); + this.currentResetFuture = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(this::resetAudioManager, 2500, TimeUnit.MILLISECONDS); } private void scheduleWaitingTone() { @@ -115,6 +135,10 @@ class ToneManager { }, 0, 3, TimeUnit.SECONDS); } + private boolean noResetScheduled() { + return this.currentResetFuture == null || this.currentResetFuture.isDone(); + } + private void cancelCurrentTone() { if (currentTone != null) { currentTone.cancel(true); @@ -132,6 +156,35 @@ class ToneManager { } } + private void configureAudioManagerForCall(final Set media) { + if (appRtcAudioManagerHasControl) { + Log.d(Config.LOGTAG, ToneManager.class.getName() + ": do not configure audio manager because RTC has control"); + return; + } + final AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + if (audioManager == null) { + return; + } + final boolean isSpeakerPhone = media.contains(Media.VIDEO); + Log.d(Config.LOGTAG, ToneManager.class.getName() + ": putting AudioManager into communication mode. speaker=" + isSpeakerPhone); + audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); + audioManager.setSpeakerphoneOn(isSpeakerPhone); + } + + private void resetAudioManager() { + if (appRtcAudioManagerHasControl) { + Log.d(Config.LOGTAG, ToneManager.class.getName() + ": do not reset audio manager because RTC has control"); + return; + } + final AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + if (audioManager == null) { + return; + } + Log.d(Config.LOGTAG, ToneManager.class.getName() + ": putting AudioManager back into normal mode"); + audioManager.setMode(AudioManager.MODE_NORMAL); + audioManager.setSpeakerphoneOn(false); + } + private enum ToneState { NULL, RINGING, CONNECTED, BUSY, ENDING_CALL } 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 96c98181a..4e5fc2641 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -8,7 +8,6 @@ import android.util.Log; import com.google.common.base.Optional; import com.google.common.base.Preconditions; -import com.google.common.base.Throwables; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.util.concurrent.Futures; @@ -41,6 +40,8 @@ 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; @@ -52,11 +53,28 @@ import javax.annotation.Nullable; import eu.siacs.conversations.Config; import eu.siacs.conversations.services.AppRTCAudioManager; +import eu.siacs.conversations.services.XmppConnectionService; public class WebRTCWrapper { private static final String EXTENDED_LOGGING_TAG = WebRTCWrapper.class.getSimpleName(); + //we should probably keep this in sync with: https://github.com/signalapp/Signal-Android/blob/master/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java#L296 + 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") + .build(); + private static final int CAPTURING_RESOLUTION = 1920; private static final int CAPTURING_MAX_FRAME_RATE = 30; @@ -157,6 +175,7 @@ public class WebRTCWrapper { private PeerConnection peerConnection = null; private AudioTrack localAudioTrack = null; private AppRTCAudioManager appRTCAudioManager = null; + private ToneManager toneManager = null; private Context context = null; private EglBase eglBase = null; private CapturerChoice capturerChoice; @@ -165,18 +184,44 @@ public class WebRTCWrapper { this.eventCallback = eventCallback; } - public void setup(final Context context, final AppRTCAudioManager.SpeakerPhonePreference speakerPhonePreference) throws InitializationException { + private static void dispose(final PeerConnection peerConnection) { + try { + peerConnection.dispose(); + } catch (final IllegalStateException e) { + Log.e(Config.LOGTAG, "unable to dispose of peer connection", e); + } + } + + @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; + } + + public void setup(final XmppConnectionService service, final AppRTCAudioManager.SpeakerPhonePreference speakerPhonePreference) throws InitializationException { try { PeerConnectionFactory.initialize( - PeerConnectionFactory.InitializationOptions.builder(context).createInitializationOptions() + PeerConnectionFactory.InitializationOptions.builder(service).createInitializationOptions() ); } catch (final UnsatisfiedLinkError e) { throw new InitializationException(e); } this.eglBase = EglBase.create(); - this.context = context; + this.context = service; + this.toneManager = service.getJingleConnectionManager().toneManager; mainHandler.post(() -> { - appRTCAudioManager = AppRTCAudioManager.create(context, speakerPhonePreference); + appRTCAudioManager = AppRTCAudioManager.create(service, speakerPhonePreference); + toneManager.setAppRtcAudioManagerHasControl(true); appRTCAudioManager.start(audioManagerEvents); eventCallback.onAudioDeviceChanged(appRTCAudioManager.getSelectedAudioDevice(), appRTCAudioManager.getAudioDevices()); }); @@ -186,9 +231,15 @@ public class WebRTCWrapper { 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(); @@ -221,6 +272,7 @@ public class WebRTCWrapper { 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; final PeerConnection peerConnection = peerConnectionFactory.createPeerConnection(rtcConfig, peerConnectionObserver); if (peerConnection == null) { throw new InitializationException("Unable to create PeerConnection"); @@ -241,6 +293,7 @@ public class WebRTCWrapper { this.peerConnection = null; } if (audioManager != null) { + toneManager.setAppRtcAudioManagerHasControl(false); mainHandler.post(audioManager::stop); } this.localVideoTrack = null; @@ -258,14 +311,6 @@ public class WebRTCWrapper { } } - private static void dispose(final PeerConnection peerConnection) { - try { - peerConnection.dispose(); - } catch (final IllegalStateException e) { - Log.e(Config.LOGTAG, "unable to dispose of peer connection", e); - } - } - synchronized void verifyClosed() { if (this.peerConnection != null || this.eglBase != null @@ -469,22 +514,6 @@ 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; - } - public PeerConnection.PeerConnectionState getState() { return requirePeerConnection().connectionState(); } diff --git a/src/main/res/values-fr/strings.xml b/src/main/res/values-fr/strings.xml index 4d4929cbc..8a5dfb6b6 100644 --- a/src/main/res/values-fr/strings.xml +++ b/src/main/res/values-fr/strings.xml @@ -469,6 +469,7 @@ Paramètres de connexion avancés Montrer le nom d\'hôte et le port lors du paramétrage d\'un compte xmpp.example.com + Se connecter avec certificat Impossible d\'analyser le certificat Paramètres d\'archivage Paramètres d\'archivage du serveur diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index 676a8966b..f937fce9c 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -209,12 +209,12 @@ Mensaxe cifrada. Instala OpenKeychain para descifrala. Atoparonse novas mensaxes cifradas cn OpenPGP ID da chave OpenPGP - Pegada OMEMO - v\\pegada OMEMO - Pegada OMEMO da mensaxe - v\\Pegada OMEMO da mensaxe + Impresión dixital OMEMO + v\\impresión OMEMO + Impresión OMEMO da mensaxe + v\\Impresión OMEMO da mensaxe Outros dispositivos - Confiar en pegadas OMEMO + Confiar en impresións dixitais OMEMO Obtendo chaves... Feito Descifrar @@ -287,7 +287,7 @@ Outro Sincronizar cos marcadores Unirte as conversas en grupo automáticamente se o marcador así o indica - Copiouse a pegada dixital OMEMO ao portapapeis + Copiouse a impresión dixital OMEMO ao portapapeis Non podes acceder a esta conversa en grupo Esta conversa en grupo é so para membros Restrición do recurso @@ -348,7 +348,7 @@ Non se atopou ningún servidor de conversa en grupo Non se puido crear a conversa en grupo Avatar da conta - Copiar pegada OMEMO ao portapapeis + Copiar impresión OMEMO ao portapapeis Rexenerar a chave OMEMO Limplar dispositivos Tes a certeza de que queres eliminar os outros dispositivos OMEMO publicados? A próxima vez que un dos teus dispositivos se conecte, deberá voltar a anunciarse, mais podería non recibir mensaxes mentras tanto. @@ -523,7 +523,7 @@ Este campo é requerido Correxir mensaxe Enviar mensaxe correxida - Xa validaches as pegadas destas persoas de xeito seguro para confiar nelas. Ao escoller \"Feito\" estás simplemente confirmando que %s é parte desta conversa en grupo. + Xa validaches as impresións dixitais destas persoas de xeito seguro para confiar nelas. Ao escoller \"Feito\" estás simplemente confirmando que %s é parte desta conversa en grupo. Desactivou esta conta Fallo de seguridade: Acceso non válido ao ficheiro! Non se atopou unha app para compartir URI @@ -592,10 +592,10 @@ O seu dispositivo non admite deshabilitar o aforro de datos para Conversations. Non se puido crear o ficheiro temporal Este dispositivo foi verificado - Copiar pegada dixital + Copiar impresión dixital Verificaches todas as chaves OMEMO no teu poder - O código de barras non contén pegadas dixitais para esta conversa. - Pegadas dixitais verificadas + O código de barras non contén impresións dixitais para esta conversa. + Impresións dixitais verificadas Utilice a cámara para escanear o código de barras do contacto Por favor agarde mentras se obteñen as chaves Compartir como código de barras diff --git a/src/main/res/values-nl/strings.xml b/src/main/res/values-nl/strings.xml index 8db430c65..b4ddb26d9 100644 --- a/src/main/res/values-nl/strings.xml +++ b/src/main/res/values-nl/strings.xml @@ -41,12 +41,14 @@ Moderator Deelnemer Bezoeker + Wil je %s uit je contactenlijst verwijderen? De gesprekken met deze contactpersoon zullen niet worden verwijderd. Wil je alle berichten van %s blokkeren? Wil je %s deblokkeren en er weer berichten van kunnen ontvangen? Alle contacten van %s blokkeren? Alle contacten van %s deblokkeren? Contact geblokkeerd Geblokkeerd + Wil je %s als bladwijzer verwijderen? De gesprekken met deze bladwijzer zullen niet worden verwijderd. Nieuwe account op server registreren Wachtwoord op server veranderen Delen met… @@ -65,11 +67,18 @@ Opslaan Oké Conversations is gecrasht + Door crashrapportages via uw XMPP account te sturen help je de ontwikkeling van Conversations. Nu versturen Niet opnieuw vragen + Verbinding maken met account mislukt + Verbinden met meerdere accounts mislukt + Tik hier op om accounts te beheren Bestand bijvoegen + Wil je dit ontbrekende contact toevoegen aan je contactenlijst? Contact toevoegen afleveren mislukt + Voorbereiden om afbeelding te sturen + Voorbereiden om afbeeldingen te sturen Bestanden delen. Even geduld… Geschiedenis wissen Gespreksgeschiedenis wissen @@ -102,12 +111,14 @@ Trillen wanneer een nieuw bericht ontvangen wordt LED-melding Meldingslicht knipperen wanneer een nieuw bericht ontvangen wordt + Beltoon voor inkomende gesprekken Uitstelperiode Geavanceerd Verstuur nooit crashrapportages Bevestig berichten Laat je contacten weten wanneer je hun berichten ontvangen en gelezen hebt Gebruikersomgeving + OpenKeychain veroorzaakte een fout. Slechte sleutel voor versleuteling. Aanvaarden Er is een fout opgetreden @@ -120,6 +131,7 @@ Foto nemen Op voorhand toestemming verlenen voor abonneren Het bestand dat je gekozen hebt is geen afbeelding + Kon de afbeelding niet converteren Bestand niet gevonden Algemene I/O-fout. Misschien is er geen opslagruimte meer beschikbaar? Onbekend diff --git a/src/main/res/values-pt-rBR/strings.xml b/src/main/res/values-pt-rBR/strings.xml index 104efb29d..1c943f0e2 100644 --- a/src/main/res/values-pt-rBR/strings.xml +++ b/src/main/res/values-pt-rBR/strings.xml @@ -918,6 +918,8 @@ Você só pode ter uma chamada de cada vez Retornar para a chamada em andamento Não foi possível trocar a câmera + Adicionar aos favoritos + Remover dos favoritos Ver %1$d participante Ver %1$d participantes diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml index 3abe96768..effaf0ef9 100644 --- a/src/main/res/values-ru/strings.xml +++ b/src/main/res/values-ru/strings.xml @@ -239,8 +239,11 @@ Уведомления будут отключены во время «тихих часов» Другие Синхронизировать с закладками + Автоматически заходить в конференции при установленном флаге в настройках закладки + OMEMO-отпечаток скопирован в буфер обмена Вы заблокированы из этой конференции Эта конференция — только для участников + Ресурсное ограничение Вас выгнали из этой конференции Конференция была остановлена Вы больше не состоите в этой конференции @@ -265,6 +268,7 @@ Сведения об учётной записи Подтвердить Повторить + Процесс переднего плана Не позволяет операционной системе закрыть ваше соединение Создать резервную копию Файлы резервной копии будут сохранены в %s @@ -281,17 +285,27 @@ файл Открыть %s отправка (%1$d%% выполнено) + Файл готовится для передачи %s предлагается скачать Отменить передачу + передача файла не удалась передача файла отменена + Файл был удалён + Не найдено приложения для открытия файла + Не найдено приложения, способного открыть эту ссылку + Не найдено приложения для просмотра контакта Динамические тэги Отображать теги только для чтения под контактами Включить уведомления Сервер конференции не найден + Не удалось создать конференцию Аватар аккаунта Скопировать OMEMO-отпечаток в буфер обмена Создать ключ OMEMO заново Очистить устройства + Вы уверены, что хотите очистить все остальные устройства из анонса ключей OMEMO? При соединении устройств в следующий раз новые ключи анонсируются автоматически, но устройства могут не получить сообщения, посланные до этого. + Для этого контакта нет доступных ключей.\nНе удалось получить новые ключи от сервера. Возможно, что-то не так с сервером вашего собеседника. + Нет доступных ключей для данного контакта.\nУбедитесь, что у вас обоих есть подписка на присутствие. Что-то пошло не так Получение истории с сервера На сервере больше нет истории @@ -301,6 +315,7 @@ Изменить пароль Текущий пароль Новый пароль + Пароль не может быть пустым Включить все аккаунты Отключить все аккаунты Взаимодействовать с @@ -309,22 +324,31 @@ Заблокирован Участник Расширенный режим + Предоставить права участника + Снять права участника Назначить администратором Снять административные права + Назначить администратором + Снять права администратора Убрать из конференции + Исключить Не удалось изменить принадлежность %s Заблокировать в конференции + Заблокировать + Вы пытаетесь исключить %s из публичного канала. Единственный способ это сделать — навсегда заблокировать этого пользователя. Заблокировать Не удалось сменить роль %s Настройки приватной конференции Настройки публичного канала Приватная Сделать XMPP адрес видимым для всех + Сделать канал модерируемым Вы не участвуете Настройки конференции изменены! Не удалось изменить настройки конференции Никогда До следующего уведомления + Повтор Ответить Прочитано Ввод @@ -350,6 +374,7 @@ Позволяет вашим контактам видеть, когда вы пишете им новое сообщение Отправить местоположение Показать местоположение + Не найдено приложения для отображения местоположения Местоположение Беседа окончена Покинул приватную конференцию @@ -368,6 +393,7 @@ Удалено %d сертификатов Удалено %d сертификатов + Заменить кнопку \"Отправить\" кнопкой быстрого действия Быстрое действие Нет Последнее выбранное @@ -375,6 +401,7 @@ Поиск контактов Поиск закладок Отправить личное сообщение + %1$s покинул конференцию Имя пользователя Имя пользователя Недопустимое имя пользователя @@ -384,6 +411,7 @@ Загрузка не удалась: ошибка записи файла Сеть Tor недоступна Ошибка связывания + Сервер не ответственен за этот домен Повреждено Доступность \"Отошёл\" когда экран выключен @@ -395,10 +423,16 @@ Расширенные настройки подключения Показывать имя сервера и порт в настройках аккаунтов xmpp.example.com + Авторизироваться с помощью сертификата + Не удалось прочитать сертификат Настройки архивирования Настройки архивирования на сервере Получение настроек архивирования. Пожалуйста, подождите… + Не удалось получить настройки архивирования + Необходима проверка CAPTCHA Введите текст с изображения + Ненадежная цепь сертификатов + XMPP-адрес не соответствует сертификату Обновить сертификат Ошибка при получении OMEMO ключа! Ключ OMEMO проверен с сертификатом! @@ -408,6 +442,7 @@ Направить все соединения через сеть Tor. Требуется Orbot Имя сервера Порт + Сервер- или .onion-адрес Это недопустимый номер порта Это недопустимое имя сервера %1$d из %2$d аккаунтов соединены @@ -418,23 +453,40 @@ %d сообщений Загрузить больше сообщений + Файл отправлен %s + Изображение отправлено %s + Изображения отправлены %s + Текст отправлен %s + Предоставить Conversations разрешение на использование внешнего накопителя + Предоставить Conversations разрешение на использование камеры Синхронизировать с контактами + Conversations нужны права на доступ к вашим контактам, чтобы соотнести лист XMPP-контактов с основным листом контактов устройства и отобразить полные имена и аватары.\n\nЭта операция не передаст информации о контактах на сервер. +
Мы не будем хранить у себя копии этих номеров.\n\nДля более подробной информации читайте нашу политику конфиденциальности.

Сейчас будет сделан запрос на разрешение доступа к контактам.]]>
Все сообщения Уведомлять только при упоминании Без уведомления Уведомления приостановлены Сжатие изображений + Подсказка: используйте ‘Выбрать файл’ вместо ‘Выбрать изображение’, чтобы отправлять изображения в несжатом виде, независимо от этой опции. Всегда Только большие изображения Оптимизации энергопотребления разрешены + Ваше устройство использует агрессивную оптимизацию энергопотребления Conversations, что может привести к задержке уведомлений и даже потере сообщений.\nРекомендуем ее отключить. + Ваше устройство использует агрессивную оптимизацию энергопотребления Conversations, что может привести к задержке уведомлений и даже потере сообщений.\nСейчас появится предложение ее отключить. Запретить Выбранная область слишком большая (Нет активных аккаунтов) Незаполненное поле Исправить сообщение Отправить исправленное сообщение + Вы уже подтвердили, что электронный отпечаток принадлежит этому человеку. Выбрав \"Готово\", вы только подтвердите, что %s является участником конференции. Вы отключили этот аккаунт + Ошибка безопасности: недействительный доступ к файлу + Не найдено приложения для передачи URI Отправить URI… +
После авторизации по номеру телефона Quicksy автоматически, основываясь на вашей адресной книге, предложит добавить возможные контакты.

Регистрируясь, вы соглашаетесь с нашей политикой конфиденциальности.]]>
+ Согласиться и продолжить + Мы поможем Вам создать аккаунт на conversations.im¹.\nВыбрав conversations.im в качестве провайдера, вы сможете общаться с пользователями других провайдеров, сообщив им свой полный XMPP-адрес. Ваш полный XMPP-адрес будет: %s Создать аккаунт Использовать свой провайдер @@ -458,12 +510,17 @@ Короткий Средний Длинный + Оповещать других об использовании + Позволяет вашим контактам видеть, когда вы используете Conversations Приватность Тема Выбрать цветовую палитру Автоматически + Светлая + Темная Зелёный фон Использовать зелёный фон для полученных сообщений + Не удалось подключиться к OpenKeyChain Данное устройство больше не используется Компьютер Телефон @@ -471,20 +528,29 @@ Веб-браузер Консоль Требуется оплата + Предоставить доступ к Интернету Я Контакт запрашивает подписку Разрешить Нет доступа к %s Удалённый сервер не найден + Время ожидания удаленного сервера истекло + Не удалось обновить учетную запись + Отправить жалобу на спам от этого XMPP-адреса. Удалить OMEMO ключи + Создать заново OMEMO-ключи. Вашим контактам потребуется повторно подтвердить ваши ключи. Используйте только в крайнем случае. Удалить отмеченные Вы должны подключиться для публикации аватара. Показать текст ошибки Текст ошибки Режим экономии трафика включен + Ваша операционная система не позволяет Conversations получать доступ в Интернет в фоновом режиме. Для получения уведомлений вы должны дать Conversations неограниченный доступ в режиме экономии трафика.\nConversations постарается экономить трафик по возможности. Ваше устройство не поддерживает отключение режима экономии трафика для Conversations. + Не удалось создать временный файл Это устройство было подтверждено Копировать отпечаток + Все имеющиеся у вас OMEMO-ключи были подтверждены + Штрих-код не содержит цифрового отпечатка для этой беседы. Подтверждённые отпечатки Используйте камеру для сканирования штрихкода контакта Подождите получения ключей @@ -492,8 +558,11 @@ Отправить XMPP URI Отправить HTTP ссылку Слепое доверие перед подтверждением + Автоматически доверять всем новым устройствам контактов, которые не были подтверждены ранее, но запрашивать ручное подтверждение каждый раз, когда подтвержденный контакт добавляет новое устройство. + Принятие OMEMO-ключей вслепую. Это означает, что собеседник может оказаться недоверенным лицом. Недоверенный Некорректный 2D штрихкод + Очистить кэш (используется камерой) Очистить кэш Очистить приватное хранилище. Очистить закрытое хранилище, где хранятся файлы (Файлы можно заново скачать с сервера) @@ -503,6 +572,7 @@ Показывать неактивные Скрыть неактивные Прекратить доверять устройству + Вы действительно хотите удалить устройство из доверенных?\Устройство и сообщения, полученные с этого устройства, будут помечаться как недоверенные. %d секунда %d секунды @@ -547,32 +617,49 @@ Соответствующие беседы закрыты. Контакт заблокирован Уведомления от неизвестных контактов + Уведомлять о сообщениях и звонках от незнакомых контактов. Получено сообщение от неизвестного контакта Заблокировать неизвестный контакт Заблокировать весь домен сейчас онлайн Повторить расшифровку Сбой сеанса + Устарелый механизм SASL Сервер требует регистрации на сайте Открыть сайт + Не найдено приложения, способного открыть этот веб-сайт Экранные уведомления + Показывать экранные уведомления Сегодня Вчера Проверить имя сервера с помощью DNSSEC Серверные сертификаты, содержащие проверенное имя хоста, считаются проверенными + Сертификат не содержит XMPP-адрес частичный Записать видео Скопировать в буфер обмена Сообщение скопировано в буфер обмена Сообщение Личные сообщения выключены + Защищенные приложения + Чтобы продолжать получать уведомления, даже если экран выключен, вам необходимо добавить Conversations в список защищенных приложений. Принять Неизвестный Сертификат? + Этот сертификат сервера не подписан ни одним из известных центров сертификации. + Принять несовпадающее имя сервера? + Серверу не удалось аутентифицироваться в качестве \"%s\". Сертификат подходит только для: Вы все равно хотите подключиться? + Детали сертификата: + Сканеру QR-кода необходим доступ к камере Прокручивать вниз Прокручивать вниз после отправки сообщения Редактировать статусное сообщение Редактировать статусное сообщение Отключить шифрование + Conversations не удалось отправить зашифрованные сообщения для %1$s. Причиной этому может быть использование получателем устаревшего клиента, который не работает с OMEMO. + Не удалось получить список устройств + Не удалось получить ключи шифрования + Подсказка: в некоторых случаях это может исправлено добавлением друг друга в список контактов. + Вы уверены, что хотите выключить OMEMO-шифрование для этой беседы?\nЭто позволит администратору сервера читать ваши сообщения, но также это может быть единственным способом связи с людьми, использующими устаревшие клиенты. Отключить сейчас Черновик: OMEMO-шифрование @@ -590,53 +677,208 @@ Сообщение не зашифровано для этого устройства. Не удалось расшифровать OMEMO-сообщение. отменить + Обмен информацией о местонахождении отключен + Закрепить позицию + Открепить позицию Копировать местоположение Поделиться местоположением Поделиться местоположением Показать местоположение Поделиться + Не удалось начать запись Пожалуйста, подождите… + Предоставить Conversations разрешение на использование микрофона Поиск сообщений + GIF Посмотреть беседу + Расширение для обмена информацией о местонахождении + Используйте расширение для обмена информацией о местонахождении вместо встроенной карты Копировать веб-адрес Копировать XMPP-адрес + Файлообмен по HTTP для S3 Быстрый поиск На экране \"Начать беседу\" открывать клавиатуру и ставить курсор в поле поиска Аватар конференции + Сервер не поддерживает наличие аватар у конференций + Только владелец может менять аватар конференции Имя контакта Никнейм Название + Предоставление имени необязательно Название конференции Эта конференция была уничтожена + Не удалось сохранить запись + Процесс переднего плана + Эта категория уведомлений используется для постоянного отображения оповещения о том, что Conversations запущен. + Информация о статусе Проблемы с подключением + Эта категория уведомлений используется для отображения оповещений, в случае если есть проблема с соединением. + Сообщения + Звонки + Сообщения + Входящие вызовы + Активные вызовы + Тихие сообщения + Эта группа уведомлений используется для отображения беззвучных оповещений. Например, при активности на другом устройстве (Грейс-период). + Настройки уведомлений о сообщениях + Настройки уведомлений о входящих вызовах + Приоритет, звук, вибрация Сжатие видео Просмотр медиа Участники + Просмотр медиафайлов + Файл не прикреплен из соображений безопасности. Качество видео Низкое качество означает меньшие файлы Среднее (360p) Высокое (720р) отменено + Вы уже пишите черновик сообщения. Функция не реализована Неверный код страны Выберите страну номер телефона + Проверьте ваш номер телефона + Quicksy отправит SMS (оператором может взиматься абонентская плата) для проверки вашего номера телефона. Введите код страны и номер телефона: +
%s

Продолжить или вы желаете изменить номер?]]>
+ %s не является корректным номером телефона. + Пожалуйста, введите ваш номер телефона. + Поиск стран + Подтвердите %s + %s.]]> + Мы отправили вам еще одну SMS с кодом из 6 цифр. + Пожалуйста, введите код из 6 цифр ниже. + Отправьте заново SMS + Отправьте заново SMS (%s) + Пожалуйста, подождите (%s) + назад + Автоматически вставлен возможный код из буфера обмена. + Пожалуйста, введите ваш код из 6 цифр. + Вы уверены, что хотите прервать процедуру регистрации? + Да + Нет + Подтверждение... + Запрос SMS... + Введенный вами код некорректен. + Отправленный вам код просрочен. + Неизвестная ошибка сети. + Неизвестный ответ от сервера. + Не удалось подключиться к серверу. + Не удалось установить безопасное соединение. + Не удалось найти сервер. + Что-то пошло не так с обработкой вашего запроса. + Некорректный ввод + Временно недоступно. Попробуйте снова позже. + Нет подключения к сети. + Пожалуйста, попробуйте еще раз через %s + У вас есть ограничение скорости Слишком много попыток Вы используете устаревшую версию приложения + Этот номер телефона в данный момент авторизирован на другом устройстве. + Пожалуйста, введите ваше имя, чтобы другие люди, у которых нет вас в списке контактов, знали кто вы. Ваше имя Введите ваше имя + Отклонить запрос + Установите Orbot + Запустите Orbot + Не установлен магазин приложений. + Этот канал сделает ваш XMPP-адрес публичным + Электронная книга Оригинал (без сжатия) Открыть с помощью... + Картинка профиля Conversations Выбрать аккаунт Восстановить из резервной копии Восстановить + Введите пароль учетной записи %s для восстановления резервной копии. + Не используйте восстановление резервной копии для дублирования установленного приложения (одновременного исполнения). Восстановление резервной копии нужно лишь для того, чтобы перенести данные на другое устройство или на случай потери своего устройства. + Не удалось восстановить резервную копию. + Не удалось расшифровать резервную копию. Вы ввели верный пароль? + Резервное копирование и восстановление + Введите XMPP-адрес Создать конференцию Присоединиться к каналу Создать закрытую конференцию Создать публичный канал Название канала + XMPP-адрес + Пожалуйста, предоставьте имя для канала + Пожалуйста, предоставьте XMPP-адрес + Это XMPP-адрес. Пожалуйста, предоставьте имя. Создание публичного канала... + Этот канал уже существует + Вы присоединились к существующему каналу + Не удалось сохранить настройки канала + Разрешить всем редактировать тему. + Разрешить всем приглашать других + Кто угодно может редактировать тему. + Владельцы могут редактировать тему. + Администраторы могут редактировать тему. + Владельцы могут приглашать других. + Кто угодно может приглашать других. + XMPP-адреса видимы для администраторов. + XMPP-адреса видимы для всех. + У этого публичного канала нет участников. Пригласите ваших знакомых или нажмите на кнопку \"Поделиться\", чтобы отправить XMPP-адрес. + У этой приватной конференции нет участников. + Управление правами + Поиск участников + Объем файла слишком велик + Прикрепить Найти каналы + Поиск каналов + Возможно нарушение конфиденциальности! + search.jabber.network.

Использование этого сервиса требует передачи вашего IP-адреса и поисковых запросов. Для дополнительной информации смотрите Политику конфиденциальности.]]>
У меня уже есть аккаунт Добавить существующий аккаунт + Зарегистрировать новую учетную запись + Это похоже на имя домена + Добавить все равно + Это похоже на адрес канала + Поделиться резервными копиями + Резервная копия Conversations + Событие + Открыть резервную копию + Выбранный вами файл не является файлом резервной копии Conversations + Эта учетная запись уже настроена + Пожалуйста, введите пароль этой учетной записи + Не удалось совершить это действие + Присоединиться к публичному каналу... + + jabber.network + Локальный сервер + Большиству пользователей следует выбрать ‘jabber.network’ для лучших предложений от всей публичной экосистемы XMPP. + Способ поиска каналов + Резервное копирование + О + Пожалуйста, активируйте учетную запись + Позвонить + Входящий звонок + Входящий видеозвонок + Соединение + Установлено соединение + Принятие звонка + Завершение звонка + Ответить + Отклонить + Поиск устройств + Вызов + Занято + Не удалось установить соединение + Ошибка приложения + Завершить + Активный звонок + Активный видеозвонок + Отключите Tor для совершения звонков + Входящий звонок + Входящий вызов · %s + Исходящий вызов + Исходящий вызов · %s + Пропущен вызов + Аудиозвонок + Видеозвонок + Микрофон недоступен + Вернуться к текущему звонку + Не удалось переключить камеру + Добавить в избранные + Убрать из избранных diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index 2b3223c05..bb08debaf 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -35,20 +35,20 @@ 解密中。请稍候… OpenPGP加密的信息 用户名已存在 - 无效的用户名 + 无效用户名 管理员 所有者 版主 成员 访客 - 将 %s 从XMPP联系人中移除? 与该联系人的会话消息不会清除。 + 将%s从XMPP联系人中移除?与该联系人的会话消息不会清除。 您想封禁%s吗? - 您想解封 %s吗 ? - 封禁 %s 中的所有联系人? - 解封%s 中所有联系人? + 您想解封%s吗? + 封禁%s中的所有联系人? + 解封%s中所有联系人? 联系人已封禁 已封禁 - 从书签中移除 %s ?相关会话消息不会被清除。 + 从书签中移除%s?相关会话消息不会被清除。 在服务器上注册新账户 在服务器上修改密码 分享… @@ -70,11 +70,11 @@ 通过您的账户发送堆栈跟踪,可以帮助畅聊持续发展。 立即发送 不再询问 - 无法连接账户 - 无法连接多个账户 - 点击以管理账户 + 账户无法连接 + 账户无法连接 + 点击管理账户 发送文件 - 该联系人不在您的列表中,需要添加吗 ? + 该联系人不在您的通讯录中,需要添加吗 ? 添加联系人 传递失败 准备发送图片 @@ -89,7 +89,7 @@ 选择设备 发送未加密的信息 发送信息 - 发信息给 %s + 发信息给%s 发送OMEMO加密信息 发送v\\OMEMO加密信息 发送OpenPGP加密信息 @@ -97,15 +97,15 @@ 不加密发送 解密失败,可能是私钥不正确。 OpenKeychain - 畅聊使用了第三方app OpenKeychain 来加密、解密信息并管理您的密钥。\n\nOpenKeychain 遵循 GPLv3 并且可以在 F-Droid 和 Google Play 上获取。\n\n(之后请重启畅聊) + 畅聊使用了第三方程序OpenKeychain来加密、解密信息并管理您的密钥。\n\nOpenKeychain遵循GPLv3并且可以在F-Droid 和Google Play上获取。\n\n(之后请重启畅聊) 重启 安装 请安装OpenKeychain以解密 提供… 等待… - 未发现 OpenPGP 密钥 + 无OpenPGP密钥 因您的联系人未公布公钥,畅聊未能成功加密您的信息。\n\n请通知对方设置OpenPGP。 - 未找到OpenPGP密钥 + 无OpenPGP密钥 因您的联系人未公布公钥,畅聊未能成功加密您的信息。\n\n请通知对方设置OpenPGP. 常规 接收文件 @@ -143,7 +143,7 @@ 您选择的文件不是图像 无法转换图片 未找到文件 - 常规的 I/O 错误。可能是存储空间不足? + 常规I/O错误。可能是存储空间不足? 您用来选择图片的程序没有给予读取权限。\n\n </small>尝试其他文件管理器选择图片</small>。 未知 暂时不可用 @@ -198,7 +198,7 @@ XEP-0357:推送 有效 无效 - 缺少公钥通知 + 缺少公钥 刚来过 一分钟前来过 %d分钟来过 @@ -245,9 +245,9 @@ 离开 联系人已添加你到通讯录 反向添加 - %s 读到这里了 - %s 读到这里了 - %1$s 和另外%2$d人读到这里了 + %s读到这里了 + %s读到这里了 + %1$s和另外%2$d人读到这里了 所有人都读到这里了 发布 点击头像以选择图片 @@ -258,7 +258,7 @@ (长按以恢复默认) 服务器不支持头像 私聊 - 至 %s + 至%s 与%s私聊 连接 该账号已存在 @@ -327,14 +327,14 @@ 备份已恢复 别忘了启用帐号。 选择文件 - 正在接受%1$s (已完成%2$d%%) - 下载 %s - 删除 %s + 正在下载%1$s(已完成%2$d%%) + 下载%s + 删除%s 文件 - 打开 %s + 打开%s 正在发送(已完成%1$d%%) 准备传输文件 - 可以下载 %s + 可以下载%s 取消传输 文件传输失败 文件传输已取消 @@ -351,7 +351,7 @@ 复制OMEMO指纹到剪贴板 重新生成OMEMO密钥 清除设备 - 清除所有其他设备的 OMEMO 通告?下次设备连接时将重新通告,但可能收不到你发送的消息。 + 清除所有其他设备的OMEMO通告?下次设备连接时将重新通告,但可能收不到你发送的消息。 此联系人没有可用的密钥。\n从服务器获取密钥失败。也许你的联系人所在服务器发生问题。 没有可以用于这个账户的密钥。\n请确保你有相互的在线状态的订阅。 出错了 @@ -380,12 +380,12 @@ 吊销所有者权限 从群聊中移除 从频道中移除 - 不能修改 %s 的从属关系 + 不能修改%s的从属关系 从群聊中封禁 从频道中封禁 %s将被从公共频道中移除。只有将此用户封禁才能将他永远移除。 立刻封禁 - 不能修改 %s 的角色 + 不能修改%s的角色 私密群聊设置 公开频道设置 私密,只有成员可以加入 @@ -471,9 +471,9 @@ 用证书登录 无法解析证书 存档设置 - 服务端存档设置 + 服务端聊天历史存档设置 正在获取存档设置。请稍候…… - 无法获取存档配置 + 无法获取存档设置 需要验证码 输入上图文字 证书链不受信任 @@ -490,7 +490,7 @@ 服务器或者.onion地址 该端口号无效 该主机名无效 - %2$d个账户中的%1$d个已连接 + 已连接%2$d个中的%1$d个账户 %d条消息 @@ -512,7 +512,7 @@ 提示:使用“选择文件”发送原图。这将忽略此设置。 总是 仅大图片 - 节电模式已启用 + 已启用节电模式 你的设备正在为畅聊进行电池优化,这可能导致通知的延迟甚至消息的丢失。\n建议禁用电池优化。 你的设备正在为畅聊进行电池优化,这可能导致通知的延迟甚至消息的丢失。\n你将会被提示禁用该功能。 禁用 @@ -670,7 +670,7 @@ 接受未知的证书? 服务器证书未由已知证书机构签发。 接受不匹配的服务器名称? - 由于 “%s”,服务器无法验证。证书仅对此有效: + 由于“%s”,服务器无法验证。证书仅对此有效: 您仍希望连接吗? 证书详情: 仅一次 @@ -684,7 +684,7 @@ 无法获取设备列表 无法获取密钥 提示:某些情况下,可以将对方加入联系人列表,以解决此问题。 - 确认要禁用此会话的 OMEMO 加密吗?\n这会允许您的服务器管理员阅读你们的消息,但这可能是和使用过时客户端的人会话的唯一方式。 + 确认要禁用此会话的OMEMO加密吗?\n这会允许您的服务器管理员阅读你们的消息,但这可能是和使用过时客户端的人会话的唯一方式。 立即禁用 草稿: OMEMO加密 @@ -699,7 +699,7 @@ - 该设备的消息未加密。 + 消息未对本设备加密。 解密OMEMO消息失败 撤销 位置分享已停用 @@ -712,7 +712,7 @@ 显示位置 分享 无法开始录制 - 请等待…… + 请等待… 允许畅聊使用麦克风 搜索消息 GIF动图 @@ -783,7 +783,7 @@ 确定放弃注册? - 正在验证..... + 正在验证... 请求短信... 验证码错误。 验证码已失效 @@ -875,7 +875,7 @@ jabber.network 本地服务器 - 大多数用户应该选择“ jabber.network”以从整个XMPP生态系统中获得更好的建议。 + 大多数用户应该选择“jabber.network”以从整个XMPP生态系统中获得更好的建议。 频道发现方法 备份 关于