Merge tag '2.8.5' into develop

This commit is contained in:
genofire 2020-05-27 13:06:33 +02:00
commit 09c7d8298d
41 changed files with 580 additions and 223 deletions

View File

@ -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

View File

@ -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

View File

@ -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 dont 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 dont 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

View File

@ -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

View File

@ -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

View File

@ -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 <a href="https://conversations.im/omemo/">OMEMO</a> or <a href="https://openpgp.org/about/">OpenPGP</a>
• 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
XEPs. 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 XEPs. 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:

View File

@ -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);
}

View File

@ -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;

View File

@ -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) {

View File

@ -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) {

View File

@ -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())) &&

View File

@ -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;

View File

@ -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");

View File

@ -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) {

View File

@ -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;

View File

@ -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);
}

View File

@ -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());
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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 {

View File

@ -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);

View File

@ -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) {
}
}

View File

@ -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;

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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;

View File

@ -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);

View File

@ -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<Message> implements CopyTextView.CopyHandler {
@ -117,6 +117,10 @@ public class MessageAdapter extends ArrayAdapter<Message> 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;
}

View File

@ -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() {

View File

@ -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;

View File

@ -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<String> 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);
}
}
}

View File

@ -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<RtpSessionProposal, DeviceDiscoveryState> rtpSessionProposals = new HashMap<>();
private final ConcurrentHashMap<AbstractJingleConnection.Id, AbstractJingleConnection> 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);
}

View File

@ -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> media) {
transition(of(isInitiator, state, media));
this.context = context;
}
private static ToneState of(final boolean isInitiator, final RtpEndUserState state, final Set<Media> media) {
@ -65,7 +61,15 @@ class ToneManager {
return ToneState.NULL;
}
private synchronized void transition(ToneState state) {
void transition(final RtpEndUserState state, final Set<Media> media) {
transition(of(true, state, media), media);
}
void transition(final boolean isInitiator, final RtpEndUserState state, final Set<Media> media) {
transition(of(isInitiator, state, media), media);
}
private synchronized void transition(ToneState state, final Set<Media> 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> 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
}

View File

@ -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<String> HARDWARE_AEC_BLACKLIST = new ImmutableSet.Builder<String>()
.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<String> availableCameras) {
final CameraVideoCapturer capturer = enumerator.createCapturer(deviceName, null);
if (capturer == null) {
return null;
}
final ArrayList<CameraEnumerationAndroid.CaptureFormat> 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<String> availableCameras) {
final CameraVideoCapturer capturer = enumerator.createCapturer(deviceName, null);
if (capturer == null) {
return null;
}
final ArrayList<CameraEnumerationAndroid.CaptureFormat> 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();
}

View File

@ -469,6 +469,7 @@
<string name="pref_show_connection_options">Paramètres de connexion avancés</string>
<string name="pref_show_connection_options_summary">Montrer le nom d\'hôte et le port lors du paramétrage d\'un compte</string>
<string name="hostname_example">xmpp.example.com</string>
<string name="action_add_account_with_certificate">Se connecter avec certificat</string>
<string name="unable_to_parse_certificate">Impossible d\'analyser le certificat</string>
<string name="mam_prefs">Paramètres d\'archivage</string>
<string name="server_side_mam_prefs">Paramètres d\'archivage du serveur</string>

View File

@ -209,12 +209,12 @@
<string name="install_openkeychain">Mensaxe cifrada. Instala OpenKeychain para descifrala.</string>
<string name="openpgp_messages_found">Atoparonse novas mensaxes cifradas cn OpenPGP</string>
<string name="openpgp_key_id">ID da chave OpenPGP</string>
<string name="omemo_fingerprint">Pegada OMEMO</string>
<string name="omemo_fingerprint_x509">v\\pegada OMEMO </string>
<string name="omemo_fingerprint_selected_message">Pegada OMEMO da mensaxe</string>
<string name="omemo_fingerprint_x509_selected_message">v\\Pegada OMEMO da mensaxe</string>
<string name="omemo_fingerprint">Impresión dixital OMEMO</string>
<string name="omemo_fingerprint_x509">v\\impresión OMEMO </string>
<string name="omemo_fingerprint_selected_message">Impresión OMEMO da mensaxe</string>
<string name="omemo_fingerprint_x509_selected_message">v\\Impresión OMEMO da mensaxe</string>
<string name="other_devices">Outros dispositivos</string>
<string name="trust_omemo_fingerprints">Confiar en pegadas OMEMO</string>
<string name="trust_omemo_fingerprints">Confiar en impresións dixitais OMEMO</string>
<string name="fetching_keys">Obtendo chaves...</string>
<string name="done">Feito</string>
<string name="decrypt">Descifrar</string>
@ -287,7 +287,7 @@
<string name="pref_expert_options_other">Outro</string>
<string name="pref_autojoin">Sincronizar cos marcadores</string>
<string name="pref_autojoin_summary">Unirte as conversas en grupo automáticamente se o marcador así o indica</string>
<string name="toast_message_omemo_fingerprint">Copiouse a pegada dixital OMEMO ao portapapeis</string>
<string name="toast_message_omemo_fingerprint">Copiouse a impresión dixital OMEMO ao portapapeis</string>
<string name="conference_banned">Non podes acceder a esta conversa en grupo</string>
<string name="conference_members_only">Esta conversa en grupo é so para membros</string>
<string name="conference_resource_constraint">Restrición do recurso</string>
@ -348,7 +348,7 @@
<string name="no_conference_server_found">Non se atopou ningún servidor de conversa en grupo</string>
<string name="conference_creation_failed">Non se puido crear a conversa en grupo</string>
<string name="account_image_description">Avatar da conta</string>
<string name="copy_omemo_clipboard_description">Copiar pegada OMEMO ao portapapeis</string>
<string name="copy_omemo_clipboard_description">Copiar impresión OMEMO ao portapapeis</string>
<string name="regenerate_omemo_key">Rexenerar a chave OMEMO</string>
<string name="clear_other_devices">Limplar dispositivos</string>
<string name="clear_other_devices_desc">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.</string>
@ -523,7 +523,7 @@
<string name="this_field_is_required">Este campo é requerido</string>
<string name="correct_message">Correxir mensaxe</string>
<string name="send_corrected_message">Enviar mensaxe correxida</string>
<string name="no_keys_just_confirm">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.</string>
<string name="no_keys_just_confirm">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.</string>
<string name="this_account_is_disabled">Desactivou esta conta</string>
<string name="security_error_invalid_file_access">Fallo de seguridade: Acceso non válido ao ficheiro!</string>
<string name="no_application_to_share_uri">Non se atopou unha app para compartir URI</string>
@ -592,10 +592,10 @@
<string name="device_does_not_support_data_saver">O seu dispositivo non admite deshabilitar o aforro de datos para Conversations.</string>
<string name="error_unable_to_create_temporary_file">Non se puido crear o ficheiro temporal</string>
<string name="this_device_has_been_verified">Este dispositivo foi verificado</string>
<string name="copy_fingerprint">Copiar pegada dixital</string>
<string name="copy_fingerprint">Copiar impresión dixital</string>
<string name="all_omemo_keys_have_been_verified">Verificaches todas as chaves OMEMO no teu poder</string>
<string name="barcode_does_not_contain_fingerprints_for_this_conversation">O código de barras non contén pegadas dixitais para esta conversa.</string>
<string name="verified_fingerprints">Pegadas dixitais verificadas</string>
<string name="barcode_does_not_contain_fingerprints_for_this_conversation">O código de barras non contén impresións dixitais para esta conversa.</string>
<string name="verified_fingerprints">Impresións dixitais verificadas</string>
<string name="use_camera_icon_to_scan_barcode">Utilice a cámara para escanear o código de barras do contacto</string>
<string name="please_wait_for_keys_to_be_fetched">Por favor agarde mentras se obteñen as chaves</string>
<string name="share_as_barcode">Compartir como código de barras</string>

View File

@ -41,12 +41,14 @@
<string name="moderator">Moderator</string>
<string name="participant">Deelnemer</string>
<string name="visitor">Bezoeker</string>
<string name="remove_contact_text">Wil je %s uit je contactenlijst verwijderen? De gesprekken met deze contactpersoon zullen niet worden verwijderd.</string>
<string name="block_contact_text">Wil je alle berichten van %s blokkeren?</string>
<string name="unblock_contact_text">Wil je %s deblokkeren en er weer berichten van kunnen ontvangen?</string>
<string name="block_domain_text">Alle contacten van %s blokkeren?</string>
<string name="unblock_domain_text">Alle contacten van %s deblokkeren?</string>
<string name="contact_blocked">Contact geblokkeerd</string>
<string name="blocked">Geblokkeerd</string>
<string name="remove_bookmark_text">Wil je %s als bladwijzer verwijderen? De gesprekken met deze bladwijzer zullen niet worden verwijderd.</string>
<string name="register_account">Nieuwe account op server registreren</string>
<string name="change_password_on_server">Wachtwoord op server veranderen</string>
<string name="share_with">Delen met…</string>
@ -65,11 +67,18 @@
<string name="save">Opslaan</string>
<string name="ok">Oké</string>
<string name="crash_report_title">Conversations is gecrasht</string>
<string name="crash_report_message">Door crashrapportages via uw XMPP account te sturen help je de ontwikkeling van Conversations.</string>
<string name="send_now">Nu versturen</string>
<string name="send_never">Niet opnieuw vragen</string>
<string name="problem_connecting_to_account">Verbinding maken met account mislukt</string>
<string name="problem_connecting_to_accounts">Verbinden met meerdere accounts mislukt</string>
<string name="touch_to_fix">Tik hier op om accounts te beheren</string>
<string name="attach_file">Bestand bijvoegen</string>
<string name="not_in_roster">Wil je dit ontbrekende contact toevoegen aan je contactenlijst?</string>
<string name="add_contact">Contact toevoegen</string>
<string name="send_failed">afleveren mislukt</string>
<string name="preparing_image">Voorbereiden om afbeelding te sturen</string>
<string name="preparing_images">Voorbereiden om afbeeldingen te sturen</string>
<string name="sharing_files_please_wait">Bestanden delen. Even geduld…</string>
<string name="action_clear_history">Geschiedenis wissen</string>
<string name="clear_conversation_history">Gespreksgeschiedenis wissen</string>
@ -102,12 +111,14 @@
<string name="pref_vibrate_summary">Trillen wanneer een nieuw bericht ontvangen wordt</string>
<string name="pref_led">LED-melding</string>
<string name="pref_led_summary">Meldingslicht knipperen wanneer een nieuw bericht ontvangen wordt</string>
<string name="pref_call_ringtone_summary">Beltoon voor inkomende gesprekken</string>
<string name="pref_notification_grace_period">Uitstelperiode</string>
<string name="pref_advanced_options">Geavanceerd</string>
<string name="pref_never_send_crash">Verstuur nooit crashrapportages</string>
<string name="pref_confirm_messages">Bevestig berichten</string>
<string name="pref_confirm_messages_summary">Laat je contacten weten wanneer je hun berichten ontvangen en gelezen hebt</string>
<string name="pref_ui_options">Gebruikersomgeving</string>
<string name="openpgp_error">OpenKeychain veroorzaakte een fout.</string>
<string name="bad_key_for_encryption">Slechte sleutel voor versleuteling.</string>
<string name="accept">Aanvaarden</string>
<string name="error">Er is een fout opgetreden</string>
@ -120,6 +131,7 @@
<string name="attach_take_picture">Foto nemen</string>
<string name="preemptively_grant">Op voorhand toestemming verlenen voor abonneren</string>
<string name="error_not_an_image_file">Het bestand dat je gekozen hebt is geen afbeelding</string>
<string name="error_compressing_image">Kon de afbeelding niet converteren</string>
<string name="error_file_not_found">Bestand niet gevonden</string>
<string name="error_io_exception">Algemene I/O-fout. Misschien is er geen opslagruimte meer beschikbaar?</string>
<string name="account_status_unknown">Onbekend</string>

View File

@ -918,6 +918,8 @@
<string name="only_one_call_at_a_time">Você só pode ter uma chamada de cada vez</string>
<string name="return_to_ongoing_call">Retornar para a chamada em andamento</string>
<string name="could_not_switch_camera">Não foi possível trocar a câmera</string>
<string name="add_to_favorites">Adicionar aos favoritos</string>
<string name="remove_from_favorites">Remover dos favoritos</string>
<plurals name="view_users">
<item quantity="one">Ver %1$d participante</item>
<item quantity="other">Ver %1$d participantes</item>

View File

@ -239,8 +239,11 @@
<string name="pref_quiet_hours_summary">Уведомления будут отключены во время «тихих часов»</string>
<string name="pref_expert_options_other">Другие</string>
<string name="pref_autojoin">Синхронизировать с закладками</string>
<string name="pref_autojoin_summary">Автоматически заходить в конференции при установленном флаге в настройках закладки</string>
<string name="toast_message_omemo_fingerprint">OMEMO-отпечаток скопирован в буфер обмена</string>
<string name="conference_banned">Вы заблокированы из этой конференции</string>
<string name="conference_members_only">Эта конференция — только для участников</string>
<string name="conference_resource_constraint">Ресурсное ограничение</string>
<string name="conference_kicked">Вас выгнали из этой конференции</string>
<string name="conference_shutdown">Конференция была остановлена</string>
<string name="conference_unknown_error">Вы больше не состоите в этой конференции</string>
@ -265,6 +268,7 @@
<string name="account_details">Сведения об учётной записи</string>
<string name="confirm">Подтвердить</string>
<string name="try_again">Повторить</string>
<string name="pref_keep_foreground_service">Процесс переднего плана</string>
<string name="pref_keep_foreground_service_summary">Не позволяет операционной системе закрыть ваше соединение</string>
<string name="pref_create_backup">Создать резервную копию</string>
<string name="pref_create_backup_summary">Файлы резервной копии будут сохранены в %s</string>
@ -281,17 +285,27 @@
<string name="file">файл</string>
<string name="open_x_file">Открыть %s</string>
<string name="sending_file">отправка (%1$d%% выполнено)</string>
<string name="preparing_file">Файл готовится для передачи</string>
<string name="x_file_offered_for_download">%s предлагается скачать</string>
<string name="cancel_transmission">Отменить передачу</string>
<string name="file_transmission_failed">передача файла не удалась</string>
<string name="file_transmission_cancelled">передача файла отменена</string>
<string name="file_deleted">Файл был удалён</string>
<string name="no_application_found_to_open_file">Не найдено приложения для открытия файла</string>
<string name="no_application_found_to_open_link">Не найдено приложения, способного открыть эту ссылку</string>
<string name="no_application_found_to_view_contact">Не найдено приложения для просмотра контакта</string>
<string name="pref_show_dynamic_tags">Динамические тэги</string>
<string name="pref_show_dynamic_tags_summary">Отображать теги только для чтения под контактами</string>
<string name="enable_notifications">Включить уведомления</string>
<string name="no_conference_server_found">Сервер конференции не найден</string>
<string name="conference_creation_failed">Не удалось создать конференцию</string>
<string name="account_image_description">Аватар аккаунта</string>
<string name="copy_omemo_clipboard_description">Скопировать OMEMO-отпечаток в буфер обмена</string>
<string name="regenerate_omemo_key">Создать ключ OMEMO заново</string>
<string name="clear_other_devices">Очистить устройства</string>
<string name="clear_other_devices_desc">Вы уверены, что хотите очистить все остальные устройства из анонса ключей OMEMO? При соединении устройств в следующий раз новые ключи анонсируются автоматически, но устройства могут не получить сообщения, посланные до этого.</string>
<string name="error_no_keys_to_trust_server_error">Для этого контакта нет доступных ключей.\nНе удалось получить новые ключи от сервера. Возможно, что-то не так с сервером вашего собеседника.</string>
<string name="error_no_keys_to_trust_presence">Нет доступных ключей для данного контакта.\nУбедитесь, что у вас обоих есть подписка на присутствие.</string>
<string name="error_trustkeys_title">Что-то пошло не так</string>
<string name="fetching_history_from_server">Получение истории с сервера</string>
<string name="no_more_history_on_server">На сервере больше нет истории</string>
@ -301,6 +315,7 @@
<string name="change_password">Изменить пароль</string>
<string name="current_password">Текущий пароль</string>
<string name="new_password">Новый пароль</string>
<string name="password_should_not_be_empty">Пароль не может быть пустым</string>
<string name="enable_all_accounts">Включить все аккаунты</string>
<string name="disable_all_accounts">Отключить все аккаунты</string>
<string name="perform_action_with">Взаимодействовать с</string>
@ -309,22 +324,31 @@
<string name="outcast">Заблокирован</string>
<string name="member">Участник</string>
<string name="advanced_mode">Расширенный режим</string>
<string name="grant_membership">Предоставить права участника</string>
<string name="remove_membership">Снять права участника</string>
<string name="grant_admin_privileges">Назначить администратором</string>
<string name="remove_admin_privileges">Снять административные права</string>
<string name="grant_owner_privileges">Назначить администратором</string>
<string name="remove_owner_privileges">Снять права администратора</string>
<string name="remove_from_room">Убрать из конференции</string>
<string name="remove_from_channel">Исключить</string>
<string name="could_not_change_affiliation">Не удалось изменить принадлежность %s</string>
<string name="ban_from_conference">Заблокировать в конференции</string>
<string name="ban_from_channel">Заблокировать</string>
<string name="removing_from_public_conference">Вы пытаетесь исключить %s из публичного канала. Единственный способ это сделать — навсегда заблокировать этого пользователя. </string>
<string name="ban_now">Заблокировать</string>
<string name="could_not_change_role">Не удалось сменить роль %s</string>
<string name="conference_options">Настройки приватной конференции</string>
<string name="channel_options">Настройки публичного канала</string>
<string name="members_only">Приватная</string>
<string name="non_anonymous">Сделать XMPP адрес видимым для всех</string>
<string name="moderated">Сделать канал модерируемым</string>
<string name="you_are_not_participating">Вы не участвуете</string>
<string name="modified_conference_options">Настройки конференции изменены!</string>
<string name="could_not_modify_conference_options">Не удалось изменить настройки конференции</string>
<string name="never">Никогда</string>
<string name="until_further_notice">До следующего уведомления</string>
<string name="snooze">Повтор</string>
<string name="reply">Ответить</string>
<string name="mark_as_read">Прочитано</string>
<string name="pref_input_options">Ввод</string>
@ -350,6 +374,7 @@
<string name="pref_chat_states_summary">Позволяет вашим контактам видеть, когда вы пишете им новое сообщение</string>
<string name="send_location">Отправить местоположение</string>
<string name="show_location">Показать местоположение</string>
<string name="no_application_found_to_display_location">Не найдено приложения для отображения местоположения</string>
<string name="location">Местоположение</string>
<string name="title_undo_swipe_out_conversation">Беседа окончена</string>
<string name="title_undo_swipe_out_group_chat">Покинул приватную конференцию</string>
@ -368,6 +393,7 @@
<item quantity="many">Удалено %d сертификатов</item>
<item quantity="other">Удалено %d сертификатов</item>
</plurals>
<string name="pref_quick_action_summary">Заменить кнопку \"Отправить\" кнопкой быстрого действия</string>
<string name="pref_quick_action">Быстрое действие</string>
<string name="none">Нет</string>
<string name="recently_used">Последнее выбранное</string>
@ -375,6 +401,7 @@
<string name="search_contacts">Поиск контактов</string>
<string name="search_bookmarks">Поиск закладок</string>
<string name="send_private_message">Отправить личное сообщение</string>
<string name="user_has_left_conference">%1$s покинул конференцию</string>
<string name="username">Имя пользователя</string>
<string name="username_hint">Имя пользователя</string>
<string name="invalid_username">Недопустимое имя пользователя</string>
@ -384,6 +411,7 @@
<string name="download_failed_could_not_write_file">Загрузка не удалась: ошибка записи файла</string>
<string name="account_status_tor_unavailable">Сеть Tor недоступна</string>
<string name="account_status_bind_failure">Ошибка связывания</string>
<string name="account_status_host_unknown">Сервер не ответственен за этот домен</string>
<string name="server_info_broken">Повреждено</string>
<string name="pref_presence_settings">Доступность</string>
<string name="pref_away_when_screen_off">\"Отошёл\" когда экран выключен</string>
@ -395,10 +423,16 @@
<string name="pref_show_connection_options">Расширенные настройки подключения</string>
<string name="pref_show_connection_options_summary">Показывать имя сервера и порт в настройках аккаунтов</string>
<string name="hostname_example">xmpp.example.com</string>
<string name="action_add_account_with_certificate">Авторизироваться с помощью сертификата</string>
<string name="unable_to_parse_certificate">Не удалось прочитать сертификат</string>
<string name="mam_prefs">Настройки архивирования</string>
<string name="server_side_mam_prefs">Настройки архивирования на сервере</string>
<string name="fetching_mam_prefs">Получение настроек архивирования. Пожалуйста, подождите…</string>
<string name="unable_to_fetch_mam_prefs">Не удалось получить настройки архивирования</string>
<string name="captcha_required">Необходима проверка CAPTCHA</string>
<string name="captcha_hint">Введите текст с изображения</string>
<string name="certificate_chain_is_not_trusted">Ненадежная цепь сертификатов</string>
<string name="jid_does_not_match_certificate">XMPP-адрес не соответствует сертификату</string>
<string name="action_renew_certificate">Обновить сертификат</string>
<string name="error_fetching_omemo_key">Ошибка при получении OMEMO ключа!</string>
<string name="verified_omemo_key_with_certificate">Ключ OMEMO проверен с сертификатом!</string>
@ -408,6 +442,7 @@
<string name="pref_use_tor_summary">Направить все соединения через сеть Tor. Требуется Orbot</string>
<string name="account_settings_hostname">Имя сервера</string>
<string name="account_settings_port">Порт</string>
<string name="hostname_or_onion">Сервер- или .onion-адрес</string>
<string name="not_a_valid_port">Это недопустимый номер порта</string>
<string name="not_valid_hostname">Это недопустимое имя сервера</string>
<string name="connected_accounts">%1$d из %2$d аккаунтов соединены</string>
@ -418,23 +453,40 @@
<item quantity="other">%d сообщений</item>
</plurals>
<string name="load_more_messages">Загрузить больше сообщений</string>
<string name="shared_file_with_x">Файл отправлен %s</string>
<string name="shared_image_with_x">Изображение отправлено %s</string>
<string name="shared_images_with_x">Изображения отправлены %s</string>
<string name="shared_text_with_x">Текст отправлен %s</string>
<string name="no_storage_permission">Предоставить Conversations разрешение на использование внешнего накопителя</string>
<string name="no_camera_permission">Предоставить Conversations разрешение на использование камеры</string>
<string name="sync_with_contacts">Синхронизировать с контактами</string>
<string name="sync_with_contacts_long">Conversations нужны права на доступ к вашим контактам, чтобы соотнести лист XMPP-контактов с основным листом контактов устройства и отобразить полные имена и аватары.\n\nЭта операция не передаст информации о контактах на сервер.</string>
<string name="sync_with_contacts_quicksy"><![CDATA[Quicksy необходим доступ к телефонным номерам ваших контактов, для обнаружения тех, кто уже использует Quicksy.<br><br>Мы не будем хранить у себя копии этих номеров.\n\nДля более подробной информации читайте нашу <a href="https://quicksy.im/#privacy">политику конфиденциальности</a>.<br><br>Сейчас будет сделан запрос на разрешение доступа к контактам.]]></string>
<string name="notify_on_all_messages">Все сообщения</string>
<string name="notify_only_when_highlighted">Уведомлять только при упоминании</string>
<string name="notify_never">Без уведомления</string>
<string name="notify_paused">Уведомления приостановлены</string>
<string name="pref_picture_compression">Сжатие изображений</string>
<string name="pref_picture_compression_summary">Подсказка: используйте ‘Выбрать файл’ вместо ‘Выбрать изображение’, чтобы отправлять изображения в несжатом виде, независимо от этой опции.</string>
<string name="always">Всегда</string>
<string name="large_images_only">Только большие изображения</string>
<string name="battery_optimizations_enabled">Оптимизации энергопотребления разрешены</string>
<string name="battery_optimizations_enabled_explained">Ваше устройство использует агрессивную оптимизацию энергопотребления Conversations, что может привести к задержке уведомлений и даже потере сообщений.\nРекомендуем ее отключить.</string>
<string name="battery_optimizations_enabled_dialog">Ваше устройство использует агрессивную оптимизацию энергопотребления Conversations, что может привести к задержке уведомлений и даже потере сообщений.\nСейчас появится предложение ее отключить.</string>
<string name="disable">Запретить</string>
<string name="selection_too_large">Выбранная область слишком большая</string>
<string name="no_accounts">(Нет активных аккаунтов)</string>
<string name="this_field_is_required">Незаполненное поле</string>
<string name="correct_message">Исправить сообщение</string>
<string name="send_corrected_message">Отправить исправленное сообщение</string>
<string name="no_keys_just_confirm">Вы уже подтвердили, что электронный отпечаток принадлежит этому человеку. Выбрав \"Готово\", вы только подтвердите, что %s является участником конференции.</string>
<string name="this_account_is_disabled">Вы отключили этот аккаунт</string>
<string name="security_error_invalid_file_access">Ошибка безопасности: недействительный доступ к файлу</string>
<string name="no_application_to_share_uri">Не найдено приложения для передачи URI</string>
<string name="share_uri_with">Отправить URI…</string>
<string name="welcome_text_quicksy"><![CDATA[Quicksy — это ответвление XMPP-клиента Conversations с автоматическим обнаружением контактов.<br><br>После авторизации по номеру телефона Quicksy автоматически, основываясь на вашей адресной книге, предложит добавить возможные контакты.<br><br>Регистрируясь, вы соглашаетесь с нашей <a href="https://quicksy.im/#privacy">политикой конфиденциальности</a>.]]></string>
<string name="agree_and_continue">Согласиться и продолжить</string>
<string name="magic_create_text">Мы поможем Вам создать аккаунт на conversations.im¹.\nВыбрав conversations.im в качестве провайдера, вы сможете общаться с пользователями других провайдеров, сообщив им свой полный XMPP-адрес.</string>
<string name="your_full_jid_will_be">Ваш полный XMPP-адрес будет: %s</string>
<string name="create_account">Создать аккаунт</string>
<string name="use_own_provider">Использовать свой провайдер</string>
@ -458,12 +510,17 @@
<string name="gp_short">Короткий</string>
<string name="gp_medium">Средний</string>
<string name="gp_long">Длинный</string>
<string name="pref_broadcast_last_activity">Оповещать других об использовании</string>
<string name="pref_broadcast_last_activity_summary">Позволяет вашим контактам видеть, когда вы используете Conversations</string>
<string name="pref_privacy">Приватность</string>
<string name="pref_theme_options">Тема</string>
<string name="pref_theme_options_summary">Выбрать цветовую палитру</string>
<string name="pref_theme_automatic">Автоматически</string>
<string name="pref_theme_light">Светлая</string>
<string name="pref_theme_dark">Темная</string>
<string name="pref_use_green_background">Зелёный фон</string>
<string name="pref_use_green_background_summary">Использовать зелёный фон для полученных сообщений</string>
<string name="unable_to_connect_to_keychain">Не удалось подключиться к OpenKeyChain</string>
<string name="this_device_is_no_longer_in_use">Данное устройство больше не используется</string>
<string name="type_pc">Компьютер</string>
<string name="type_phone">Телефон</string>
@ -471,20 +528,29 @@
<string name="type_web">Веб-браузер</string>
<string name="type_console">Консоль</string>
<string name="payment_required">Требуется оплата</string>
<string name="missing_internet_permission">Предоставить доступ к Интернету</string>
<string name="me">Я</string>
<string name="contact_asks_for_presence_subscription">Контакт запрашивает подписку</string>
<string name="allow">Разрешить</string>
<string name="no_permission_to_access_x">Нет доступа к %s</string>
<string name="remote_server_not_found">Удалённый сервер не найден</string>
<string name="remote_server_timeout">Время ожидания удаленного сервера истекло</string>
<string name="unable_to_update_account">Не удалось обновить учетную запись</string>
<string name="report_jid_as_spammer">Отправить жалобу на спам от этого XMPP-адреса.</string>
<string name="pref_delete_omemo_identities">Удалить OMEMO ключи</string>
<string name="pref_delete_omemo_identities_summary">Создать заново OMEMO-ключи. Вашим контактам потребуется повторно подтвердить ваши ключи. Используйте только в крайнем случае.</string>
<string name="delete_selected_keys">Удалить отмеченные</string>
<string name="error_publish_avatar_offline">Вы должны подключиться для публикации аватара.</string>
<string name="show_error_message">Показать текст ошибки</string>
<string name="error_message">Текст ошибки</string>
<string name="data_saver_enabled">Режим экономии трафика включен</string>
<string name="data_saver_enabled_explained">Ваша операционная система не позволяет Conversations получать доступ в Интернет в фоновом режиме. Для получения уведомлений вы должны дать Conversations неограниченный доступ в режиме экономии трафика.\nConversations постарается экономить трафик по возможности.</string>
<string name="device_does_not_support_data_saver">Ваше устройство не поддерживает отключение режима экономии трафика для Conversations.</string>
<string name="error_unable_to_create_temporary_file">Не удалось создать временный файл</string>
<string name="this_device_has_been_verified">Это устройство было подтверждено</string>
<string name="copy_fingerprint">Копировать отпечаток</string>
<string name="all_omemo_keys_have_been_verified">Все имеющиеся у вас OMEMO-ключи были подтверждены</string>
<string name="barcode_does_not_contain_fingerprints_for_this_conversation">Штрих-код не содержит цифрового отпечатка для этой беседы.</string>
<string name="verified_fingerprints">Подтверждённые отпечатки</string>
<string name="use_camera_icon_to_scan_barcode">Используйте камеру для сканирования штрихкода контакта</string>
<string name="please_wait_for_keys_to_be_fetched">Подождите получения ключей</string>
@ -492,8 +558,11 @@
<string name="share_as_uri">Отправить XMPP URI</string>
<string name="share_as_http">Отправить HTTP ссылку</string>
<string name="pref_blind_trust_before_verification">Слепое доверие перед подтверждением</string>
<string name="pref_blind_trust_before_verification_summary">Автоматически доверять всем новым устройствам контактов, которые не были подтверждены ранее, но запрашивать ручное подтверждение каждый раз, когда подтвержденный контакт добавляет новое устройство.</string>
<string name="blindly_trusted_omemo_keys">Принятие OMEMO-ключей вслепую. Это означает, что собеседник может оказаться недоверенным лицом.</string>
<string name="not_trusted">Недоверенный</string>
<string name="invalid_barcode">Некорректный 2D штрихкод</string>
<string name="pref_clean_cache_summary">Очистить кэш (используется камерой)</string>
<string name="pref_clean_cache">Очистить кэш</string>
<string name="pref_clean_private_storage">Очистить приватное хранилище.</string>
<string name="pref_clean_private_storage_summary">Очистить закрытое хранилище, где хранятся файлы (Файлы можно заново скачать с сервера)</string>
@ -503,6 +572,7 @@
<string name="show_inactive_devices">Показывать неактивные</string>
<string name="hide_inactive_devices">Скрыть неактивные</string>
<string name="distrust_omemo_key">Прекратить доверять устройству</string>
<string name="distrust_omemo_key_text">Вы действительно хотите удалить устройство из доверенных?\Устройство и сообщения, полученные с этого устройства, будут помечаться как недоверенные.</string>
<plurals name="seconds">
<item quantity="one">%d секунда</item>
<item quantity="few">%d секунды</item>
@ -547,32 +617,49 @@
<string name="corresponding_conversations_closed">Соответствующие беседы закрыты.</string>
<string name="contact_blocked_past_tense">Контакт заблокирован</string>
<string name="pref_notifications_from_strangers">Уведомления от неизвестных контактов</string>
<string name="pref_notifications_from_strangers_summary">Уведомлять о сообщениях и звонках от незнакомых контактов.</string>
<string name="received_message_from_stranger">Получено сообщение от неизвестного контакта</string>
<string name="block_stranger">Заблокировать неизвестный контакт</string>
<string name="block_entire_domain">Заблокировать весь домен</string>
<string name="online_right_now">сейчас онлайн</string>
<string name="retry_decryption">Повторить расшифровку</string>
<string name="session_failure">Сбой сеанса</string>
<string name="sasl_downgrade">Устарелый механизм SASL</string>
<string name="account_status_regis_web">Сервер требует регистрации на сайте</string>
<string name="open_website">Открыть сайт</string>
<string name="application_found_to_open_website">Не найдено приложения, способного открыть этот веб-сайт</string>
<string name="pref_headsup_notifications">Экранные уведомления</string>
<string name="pref_headsup_notifications_summary">Показывать экранные уведомления</string>
<string name="today">Сегодня</string>
<string name="yesterday">Вчера</string>
<string name="pref_validate_hostname">Проверить имя сервера с помощью DNSSEC</string>
<string name="pref_validate_hostname_summary">Серверные сертификаты, содержащие проверенное имя хоста, считаются проверенными</string>
<string name="certificate_does_not_contain_jid">Сертификат не содержит XMPP-адрес</string>
<string name="server_info_partial">частичный</string>
<string name="attach_record_video">Записать видео</string>
<string name="copy_to_clipboard">Скопировать в буфер обмена</string>
<string name="message_copied_to_clipboard">Сообщение скопировано в буфер обмена</string>
<string name="message">Сообщение</string>
<string name="private_messages_are_disabled">Личные сообщения выключены</string>
<string name="huawei_protected_apps">Защищенные приложения</string>
<string name="huawei_protected_apps_summary">Чтобы продолжать получать уведомления, даже если экран выключен, вам необходимо добавить Conversations в список защищенных приложений.</string>
<string name="mtm_accept_cert">Принять Неизвестный Сертификат?</string>
<string name="mtm_trust_anchor">Этот сертификат сервера не подписан ни одним из известных центров сертификации.</string>
<string name="mtm_accept_servername">Принять несовпадающее имя сервера?</string>
<string name="mtm_hostname_mismatch">Серверу не удалось аутентифицироваться в качестве \"%s\". Сертификат подходит только для:</string>
<string name="mtm_connect_anyway">Вы все равно хотите подключиться?</string>
<string name="mtm_cert_details">Детали сертификата:</string>
<string name="qr_code_scanner_needs_access_to_camera">Сканеру QR-кода необходим доступ к камере</string>
<string name="pref_scroll_to_bottom">Прокручивать вниз</string>
<string name="pref_scroll_to_bottom_summary">Прокручивать вниз после отправки сообщения</string>
<string name="edit_status_message_title">Редактировать статусное сообщение</string>
<string name="edit_status_message">Редактировать статусное сообщение</string>
<string name="disable_encryption">Отключить шифрование</string>
<string name="error_trustkey_general">Conversations не удалось отправить зашифрованные сообщения для %1$s. Причиной этому может быть использование получателем устаревшего клиента, который не работает с OMEMO.</string>
<string name="error_trustkey_device_list">Не удалось получить список устройств</string>
<string name="error_trustkey_bundle">Не удалось получить ключи шифрования</string>
<string name="error_trustkey_hint_mutual">Подсказка: в некоторых случаях это может исправлено добавлением друг друга в список контактов.</string>
<string name="disable_encryption_message">Вы уверены, что хотите выключить OMEMO-шифрование для этой беседы?\nЭто позволит администратору сервера читать ваши сообщения, но также это может быть единственным способом связи с людьми, использующими устаревшие клиенты.</string>
<string name="disable_now">Отключить сейчас</string>
<string name="draft">Черновик:</string>
<string name="pref_omemo_setting">OMEMO-шифрование</string>
@ -590,53 +677,208 @@
<string name="not_encrypted_for_this_device">Сообщение не зашифровано для этого устройства.</string>
<string name="omemo_decryption_failed">Не удалось расшифровать OMEMO-сообщение.</string>
<string name="undo">отменить</string>
<string name="location_disabled">Обмен информацией о местонахождении отключен</string>
<string name="action_fix_to_location">Закрепить позицию</string>
<string name="action_unfix_from_location">Открепить позицию</string>
<string name="action_copy_location">Копировать местоположение</string>
<string name="action_share_location">Поделиться местоположением</string>
<string name="title_activity_share_location">Поделиться местоположением</string>
<string name="title_activity_show_location">Показать местоположение</string>
<string name="share">Поделиться</string>
<string name="unable_to_start_recording">Не удалось начать запись</string>
<string name="please_wait">Пожалуйста, подождите…</string>
<string name="no_microphone_permission">Предоставить Conversations разрешение на использование микрофона</string>
<string name="search_messages">Поиск сообщений</string>
<string name="gif">GIF</string>
<string name="view_conversation">Посмотреть беседу</string>
<string name="pref_use_share_location_plugin">Расширение для обмена информацией о местонахождении</string>
<string name="pref_use_share_location_plugin_summary">Используйте расширение для обмена информацией о местонахождении вместо встроенной карты</string>
<string name="copy_link">Копировать веб-адрес</string>
<string name="copy_jabber_id">Копировать XMPP-адрес</string>
<string name="p1_s3_filetransfer">Файлообмен по HTTP для S3</string>
<string name="pref_start_search">Быстрый поиск</string>
<string name="pref_start_search_summary">На экране \"Начать беседу\" открывать клавиатуру и ставить курсор в поле поиска</string>
<string name="group_chat_avatar">Аватар конференции</string>
<string name="host_does_not_support_group_chat_avatars">Сервер не поддерживает наличие аватар у конференций</string>
<string name="only_the_owner_can_change_group_chat_avatar">Только владелец может менять аватар конференции</string>
<string name="contact_name">Имя контакта</string>
<string name="nickname">Никнейм</string>
<string name="group_chat_name">Название</string>
<string name="providing_a_name_is_optional">Предоставление имени необязательно</string>
<string name="create_dialog_group_chat_name">Название конференции</string>
<string name="conference_destroyed">Эта конференция была уничтожена</string>
<string name="unable_to_save_recording">Не удалось сохранить запись</string>
<string name="foreground_service_channel_name">Процесс переднего плана</string>
<string name="foreground_service_channel_description">Эта категория уведомлений используется для постоянного отображения оповещения о том, что Conversations запущен.</string>
<string name="notification_group_status_information">Информация о статусе</string>
<string name="error_channel_name">Проблемы с подключением</string>
<string name="error_channel_description">Эта категория уведомлений используется для отображения оповещений, в случае если есть проблема с соединением.</string>
<string name="notification_group_messages">Сообщения</string>
<string name="notification_group_calls">Звонки</string>
<string name="messages_channel_name">Сообщения</string>
<string name="incoming_calls_channel_name">Входящие вызовы</string>
<string name="ongoing_calls_channel_name">Активные вызовы</string>
<string name="silent_messages_channel_name">Тихие сообщения</string>
<string name="silent_messages_channel_description">Эта группа уведомлений используется для отображения беззвучных оповещений. Например, при активности на другом устройстве (Грейс-период).</string>
<string name="pref_message_notification_settings">Настройки уведомлений о сообщениях</string>
<string name="pref_incoming_call_notification_settings">Настройки уведомлений о входящих вызовах</string>
<string name="pref_more_notification_settings_summary">Приоритет, звук, вибрация</string>
<string name="video_compression_channel_name">Сжатие видео</string>
<string name="view_media">Просмотр медиа</string>
<string name="group_chat_members">Участники</string>
<string name="media_browser">Просмотр медиафайлов</string>
<string name="security_violation_not_attaching_file">Файл не прикреплен из соображений безопасности.</string>
<string name="pref_video_compression">Качество видео</string>
<string name="pref_video_compression_summary">Низкое качество означает меньшие файлы</string>
<string name="video_360p">Среднее (360p)</string>
<string name="video_720p">Высокое (720р)</string>
<string name="cancelled">отменено</string>
<string name="already_drafting_message">Вы уже пишите черновик сообщения.</string>
<string name="feature_not_implemented">Функция не реализована</string>
<string name="invalid_country_code">Неверный код страны</string>
<string name="choose_a_country">Выберите страну</string>
<string name="phone_number">номер телефона</string>
<string name="verify_your_phone_number">Проверьте ваш номер телефона</string>
<string name="enter_country_code_and_phone_number">Quicksy отправит SMS (оператором может взиматься абонентская плата) для проверки вашего номера телефона. Введите код страны и номер телефона:</string>
<string name="we_will_be_verifying"><![CDATA[Мы проверим номер телефона<br/><br/><b>%s</b><br/><br/>Продолжить или вы желаете изменить номер?]]></string>
<string name="not_a_valid_phone_number">%s не является корректным номером телефона.</string>
<string name="please_enter_your_phone_number">Пожалуйста, введите ваш номер телефона.</string>
<string name="search_countries">Поиск стран</string>
<string name="verify_x">Подтвердите %s</string>
<string name="we_have_sent_you_an_sms_to_x"><![CDATA[Мы отправили вам SMS на <b>%s</b>.]]></string>
<string name="we_have_sent_you_another_sms">Мы отправили вам еще одну SMS с кодом из 6 цифр.</string>
<string name="please_enter_pin_below">Пожалуйста, введите код из 6 цифр ниже.</string>
<string name="resend_sms">Отправьте заново SMS</string>
<string name="resend_sms_in">Отправьте заново SMS (%s)</string>
<string name="wait_x">Пожалуйста, подождите (%s)</string>
<string name="back">назад</string>
<string name="possible_pin">Автоматически вставлен возможный код из буфера обмена.</string>
<string name="please_enter_pin">Пожалуйста, введите ваш код из 6 цифр.</string>
<string name="abort_registration_procedure">Вы уверены, что хотите прервать процедуру регистрации?</string>
<string name="yes">Да</string>
<string name="no">Нет</string>
<string name="verifying">Подтверждение...</string>
<string name="requesting_sms">Запрос SMS...</string>
<string name="incorrect_pin">Введенный вами код некорректен.</string>
<string name="pin_expired">Отправленный вам код просрочен.</string>
<string name="unknown_api_error_network">Неизвестная ошибка сети.</string>
<string name="unknown_api_error_response">Неизвестный ответ от сервера.</string>
<string name="unable_to_connect_to_server">Не удалось подключиться к серверу.</string>
<string name="unable_to_establish_secure_connection">Не удалось установить безопасное соединение.</string>
<string name="unable_to_find_server">Не удалось найти сервер.</string>
<string name="something_went_wrong_processing_your_request">Что-то пошло не так с обработкой вашего запроса.</string>
<string name="invalid_user_input">Некорректный ввод</string>
<string name="temporarily_unavailable">Временно недоступно. Попробуйте снова позже.</string>
<string name="no_network_connection">Нет подключения к сети.</string>
<string name="try_again_in_x">Пожалуйста, попробуйте еще раз через %s</string>
<string name="rate_limited">У вас есть ограничение скорости</string>
<string name="too_many_attempts">Слишком много попыток</string>
<string name="the_app_is_out_of_date">Вы используете устаревшую версию приложения</string>
<string name="logged_in_with_another_device">Этот номер телефона в данный момент авторизирован на другом устройстве.</string>
<string name="enter_your_name_instructions">Пожалуйста, введите ваше имя, чтобы другие люди, у которых нет вас в списке контактов, знали кто вы.</string>
<string name="your_name">Ваше имя</string>
<string name="enter_your_name">Введите ваше имя</string>
<string name="reject_request">Отклонить запрос</string>
<string name="install_orbot">Установите Orbot</string>
<string name="start_orbot">Запустите Orbot</string>
<string name="no_market_app_installed">Не установлен магазин приложений.</string>
<string name="group_chat_will_make_your_jabber_id_public">Этот канал сделает ваш XMPP-адрес публичным</string>
<string name="ebook">Электронная книга</string>
<string name="video_original">Оригинал (без сжатия)</string>
<string name="open_with">Открыть с помощью...</string>
<string name="set_profile_picture">Картинка профиля Conversations</string>
<string name="choose_account">Выбрать аккаунт</string>
<string name="restore_backup">Восстановить из резервной копии</string>
<string name="restore">Восстановить</string>
<string name="enter_password_to_restore">Введите пароль учетной записи %s для восстановления резервной копии.</string>
<string name="restore_warning">Не используйте восстановление резервной копии для дублирования установленного приложения (одновременного исполнения). Восстановление резервной копии нужно лишь для того, чтобы перенести данные на другое устройство или на случай потери своего устройства.</string>
<string name="unable_to_restore_backup">Не удалось восстановить резервную копию.</string>
<string name="unable_to_decrypt_backup">Не удалось расшифровать резервную копию. Вы ввели верный пароль?</string>
<string name="backup_channel_name">Резервное копирование и восстановление</string>
<string name="enter_jabber_id">Введите XMPP-адрес</string>
<string name="create_group_chat">Создать конференцию</string>
<string name="join_public_channel">Присоединиться к каналу</string>
<string name="create_private_group_chat">Создать закрытую конференцию</string>
<string name="create_public_channel">Создать публичный канал</string>
<string name="create_dialog_channel_name">Название канала</string>
<string name="xmpp_address">XMPP-адрес</string>
<string name="please_enter_name">Пожалуйста, предоставьте имя для канала</string>
<string name="please_enter_xmpp_address">Пожалуйста, предоставьте XMPP-адрес</string>
<string name="this_is_an_xmpp_address">Это XMPP-адрес. Пожалуйста, предоставьте имя.</string>
<string name="creating_channel">Создание публичного канала...</string>
<string name="channel_already_exists">Этот канал уже существует</string>
<string name="joined_an_existing_channel">Вы присоединились к существующему каналу</string>
<string name="unable_to_set_channel_configuration">Не удалось сохранить настройки канала</string>
<string name="allow_participants_to_edit_subject">Разрешить всем редактировать тему.</string>
<string name="allow_participants_to_invite_others">Разрешить всем приглашать других</string>
<string name="anyone_can_edit_subject">Кто угодно может редактировать тему.</string>
<string name="owners_can_edit_subject">Владельцы могут редактировать тему.</string>
<string name="admins_can_edit_subject">Администраторы могут редактировать тему.</string>
<string name="owners_can_invite_others">Владельцы могут приглашать других.</string>
<string name="anyone_can_invite_others">Кто угодно может приглашать других.</string>
<string name="jabber_ids_are_visible_to_admins">XMPP-адреса видимы для администраторов.</string>
<string name="jabber_ids_are_visible_to_anyone">XMPP-адреса видимы для всех.</string>
<string name="no_users_hint_channel">У этого публичного канала нет участников. Пригласите ваших знакомых или нажмите на кнопку \"Поделиться\", чтобы отправить XMPP-адрес.</string>
<string name="no_users_hint_group_chat">У этой приватной конференции нет участников.</string>
<string name="manage_permission">Управление правами</string>
<string name="search_participants">Поиск участников</string>
<string name="file_too_large">Объем файла слишком велик</string>
<string name="attach">Прикрепить</string>
<string name="discover_channels">Найти каналы</string>
<string name="search_channels">Поиск каналов</string>
<string name="channel_discovery_opt_in_title">Возможно нарушение конфиденциальности!</string>
<string name="channel_discover_opt_in_message"><![CDATA[Поиск каналов использует сторонний сервис <a href="https://search.jabber.network">search.jabber.network</a>.<br><br>Использование этого сервиса требует передачи вашего IP-адреса и поисковых запросов. Для дополнительной информации смотрите <a href="https://search.jabber.network/privacy">Политику конфиденциальности</a>.]]></string>
<string name="i_already_have_an_account">У меня уже есть аккаунт</string>
<string name="add_existing_account">Добавить существующий аккаунт</string>
<string name="register_new_account">Зарегистрировать новую учетную запись</string>
<string name="this_looks_like_a_domain">Это похоже на имя домена</string>
<string name="add_anway">Добавить все равно</string>
<string name="this_looks_like_channel">Это похоже на адрес канала</string>
<string name="share_backup_files">Поделиться резервными копиями</string>
<string name="conversations_backup">Резервная копия Conversations</string>
<string name="event">Событие</string>
<string name="open_backup">Открыть резервную копию</string>
<string name="not_a_backup_file">Выбранный вами файл не является файлом резервной копии Conversations</string>
<string name="account_already_setup">Эта учетная запись уже настроена</string>
<string name="please_enter_password">Пожалуйста, введите пароль этой учетной записи</string>
<string name="unable_to_perform_this_action">Не удалось совершить это действие</string>
<string name="open_join_dialog">Присоединиться к публичному каналу...</string>
<string name="group_chats_and_channels"><![CDATA[Группы и каналы]]></string>
<string name="jabber_network">jabber.network</string>
<string name="local_server">Локальный сервер</string>
<string name="pref_channel_discovery_summary">Большиству пользователей следует выбрать jabber.network для лучших предложений от всей публичной экосистемы XMPP.</string>
<string name="pref_channel_discovery">Способ поиска каналов</string>
<string name="backup">Резервное копирование</string>
<string name="category_about">О</string>
<string name="please_enable_an_account">Пожалуйста, активируйте учетную запись</string>
<string name="make_call">Позвонить</string>
<string name="rtp_state_incoming_call">Входящий звонок</string>
<string name="rtp_state_incoming_video_call">Входящий видеозвонок</string>
<string name="rtp_state_connecting">Соединение</string>
<string name="rtp_state_connected">Установлено соединение</string>
<string name="rtp_state_accepting_call">Принятие звонка</string>
<string name="rtp_state_ending_call">Завершение звонка</string>
<string name="answer_call">Ответить</string>
<string name="dismiss_call">Отклонить</string>
<string name="rtp_state_finding_device">Поиск устройств</string>
<string name="rtp_state_ringing">Вызов</string>
<string name="rtp_state_declined_or_busy">Занято</string>
<string name="rtp_state_connectivity_error">Не удалось установить соединение</string>
<string name="rtp_state_application_failure">Ошибка приложения</string>
<string name="hang_up">Завершить</string>
<string name="ongoing_call">Активный звонок</string>
<string name="ongoing_video_call">Активный видеозвонок</string>
<string name="disable_tor_to_make_call">Отключите Tor для совершения звонков</string>
<string name="incoming_call">Входящий звонок</string>
<string name="incoming_call_duration">Входящий вызов · %s</string>
<string name="outgoing_call">Исходящий вызов</string>
<string name="outgoing_call_duration">Исходящий вызов · %s</string>
<string name="missed_call">Пропущен вызов</string>
<string name="audio_call">Аудиозвонок</string>
<string name="video_call">Видеозвонок</string>
<string name="microphone_unavailable">Микрофон недоступен</string>
<string name="return_to_ongoing_call">Вернуться к текущему звонку</string>
<string name="could_not_switch_camera">Не удалось переключить камеру</string>
<string name="add_to_favorites">Добавить в избранные</string>
<string name="remove_from_favorites">Убрать из избранных</string>
</resources>

View File

@ -35,20 +35,20 @@
<string name="message_decrypting">解密中。请稍候…</string>
<string name="pgp_message">OpenPGP加密的信息</string>
<string name="nick_in_use">用户名已存在</string>
<string name="invalid_muc_nick">无效用户名</string>
<string name="invalid_muc_nick">无效用户名</string>
<string name="admin">管理员</string>
<string name="owner">所有者</string>
<string name="moderator">版主</string>
<string name="participant">成员</string>
<string name="visitor">访客</string>
<string name="remove_contact_text"> %s 从XMPP联系人中移除? 与该联系人的会话消息不会清除。</string>
<string name="remove_contact_text">%s从XMPP联系人中移除与该联系人的会话消息不会清除。</string>
<string name="block_contact_text">您想封禁%s吗</string>
<string name="unblock_contact_text">您想解封 %s吗 </string>
<string name="block_domain_text">封禁 %s 中的所有联系人?</string>
<string name="unblock_domain_text">解封%s 中所有联系人?</string>
<string name="unblock_contact_text">您想解封%s吗</string>
<string name="block_domain_text">封禁%s中的所有联系人</string>
<string name="unblock_domain_text">解封%s中所有联系人</string>
<string name="contact_blocked">联系人已封禁</string>
<string name="blocked">已封禁</string>
<string name="remove_bookmark_text">从书签中移除 %s ?相关会话消息不会被清除。</string>
<string name="remove_bookmark_text">从书签中移除%s相关会话消息不会被清除。</string>
<string name="register_account">在服务器上注册新账户</string>
<string name="change_password_on_server">在服务器上修改密码</string>
<string name="share_with">分享…</string>
@ -70,11 +70,11 @@
<string name="crash_report_message">通过您的账户发送堆栈跟踪,可以帮助畅聊持续发展。</string>
<string name="send_now">立即发送</string>
<string name="send_never">不再询问</string>
<string name="problem_connecting_to_account">无法连接账户</string>
<string name="problem_connecting_to_accounts">无法连接多个账户</string>
<string name="touch_to_fix">点击管理账户</string>
<string name="problem_connecting_to_account">账户无法连接</string>
<string name="problem_connecting_to_accounts">账户无法连接</string>
<string name="touch_to_fix">点击管理账户</string>
<string name="attach_file">发送文件</string>
<string name="not_in_roster">该联系人不在您的列表中,需要添加吗 ?</string>
<string name="not_in_roster">该联系人不在您的通讯录中,需要添加吗 ?</string>
<string name="add_contact">添加联系人</string>
<string name="send_failed">传递失败</string>
<string name="preparing_image">准备发送图片</string>
@ -89,7 +89,7 @@
<string name="choose_presence">选择设备</string>
<string name="send_unencrypted_message">发送未加密的信息</string>
<string name="send_message">发送信息</string>
<string name="send_message_to_x">发信息给 %s</string>
<string name="send_message_to_x">发信息给%s</string>
<string name="send_omemo_message">发送OMEMO加密信息</string>
<string name="send_omemo_x509_message">发送v\\OMEMO加密信息</string>
<string name="send_pgp_message">发送OpenPGP加密信息</string>
@ -97,15 +97,15 @@
<string name="send_unencrypted">不加密发送</string>
<string name="decryption_failed">解密失败,可能是私钥不正确。</string>
<string name="openkeychain_required">OpenKeychain</string>
<string name="openkeychain_required_long">畅聊使用了第三方app <b>OpenKeychain</b> 来加密、解密信息并管理您的密钥。\n\nOpenKeychain 遵循 GPLv3 并且可以在 F-Droid 和 Google Play 上获取。\n\n<small>(之后请重启畅聊)</small></string>
<string name="openkeychain_required_long">畅聊使用了第三方程序<b>OpenKeychain</b>来加密、解密信息并管理您的密钥。\n\nOpenKeychain遵循GPLv3并且可以在F-Droid 和Google Play上获取。\n\n<small>(之后请重启畅聊)</small></string>
<string name="restart">重启</string>
<string name="install">安装</string>
<string name="openkeychain_not_installed">请安装OpenKeychain以解密</string>
<string name="offering">提供…</string>
<string name="waiting">等待…</string>
<string name="no_pgp_key">未发现 OpenPGP 密钥</string>
<string name="no_pgp_key">无OpenPGP密钥</string>
<string name="contact_has_no_pgp_key">因您的联系人未公布公钥,畅聊未能成功加密您的信息。\n\n<small>请通知对方设置OpenPGP。</small></string>
<string name="no_pgp_keys">未找到OpenPGP密钥</string>
<string name="no_pgp_keys">OpenPGP密钥</string>
<string name="contacts_have_no_pgp_keys">因您的联系人未公布公钥,畅聊未能成功加密您的信息。\n\n<small>请通知对方设置OpenPGP.</small></string>
<string name="pref_general">常规</string>
<string name="pref_accept_files">接收文件</string>
@ -143,7 +143,7 @@
<string name="error_not_an_image_file">您选择的文件不是图像</string>
<string name="error_compressing_image">无法转换图片</string>
<string name="error_file_not_found">未找到文件</string>
<string name="error_io_exception">常规I/O 错误。可能是存储空间不足?</string>
<string name="error_io_exception">常规I/O错误。可能是存储空间不足</string>
<string name="error_security_exception_during_image_copy">您用来选择图片的程序没有给予读取权限。\n\n &lt;/small&gt;尝试其他文件管理器选择图片&lt;/small&gt;</string>
<string name="account_status_unknown">未知</string>
<string name="account_status_disabled">暂时不可用</string>
@ -198,7 +198,7 @@
<string name="server_info_push">XEP-0357推送</string>
<string name="server_info_available">有效</string>
<string name="server_info_unavailable">无效</string>
<string name="missing_public_keys">缺少公钥通知</string>
<string name="missing_public_keys">缺少公钥</string>
<string name="last_seen_now">刚来过</string>
<string name="last_seen_min">一分钟前来过</string>
<string name="last_seen_mins">%d分钟来过</string>
@ -245,9 +245,9 @@
<string name="leave">离开</string>
<string name="contact_added_you">联系人已添加你到通讯录</string>
<string name="add_back">反向添加</string>
<string name="contact_has_read_up_to_this_point">%s 读到这里了</string>
<string name="contacts_have_read_up_to_this_point">%s 读到这里了</string>
<string name="contacts_and_n_more_have_read_up_to_this_point">%1$s 和另外%2$d人读到这里了</string>
<string name="contact_has_read_up_to_this_point">%s读到这里了</string>
<string name="contacts_have_read_up_to_this_point">%s读到这里了</string>
<string name="contacts_and_n_more_have_read_up_to_this_point">%1$s和另外%2$d人读到这里了</string>
<string name="everyone_has_read_up_to_this_point">所有人都读到这里了</string>
<string name="publish">发布</string>
<string name="touch_to_choose_picture">点击头像以选择图片</string>
@ -258,7 +258,7 @@
<string name="or_long_press_for_default">(长按以恢复默认)</string>
<string name="error_publish_avatar_no_server_support">服务器不支持头像</string>
<string name="private_message">私聊</string>
<string name="private_message_to"> %s</string>
<string name="private_message_to">至%s</string>
<string name="send_private_message_to">与%s私聊</string>
<string name="connect">连接</string>
<string name="account_already_exists">该账号已存在</string>
@ -327,14 +327,14 @@
<string name="notification_restored_backup_title">备份已恢复</string>
<string name="notification_restored_backup_subtitle">别忘了启用帐号。</string>
<string name="choose_file">选择文件</string>
<string name="receiving_x_file">正在接受%1$s (已完成%2$d%%)</string>
<string name="download_x_file">下载 %s</string>
<string name="delete_x_file">删除 %s</string>
<string name="receiving_x_file">正在下载%1$s(已完成%2$d%%)</string>
<string name="download_x_file">下载%s</string>
<string name="delete_x_file">删除%s</string>
<string name="file">文件</string>
<string name="open_x_file"> 打开 %s</string>
<string name="open_x_file"> 打开%s</string>
<string name="sending_file">正在发送(已完成%1$d%%</string>
<string name="preparing_file">准备传输文件</string>
<string name="x_file_offered_for_download">可以下载 %s</string>
<string name="x_file_offered_for_download">可以下载%s</string>
<string name="cancel_transmission">取消传输</string>
<string name="file_transmission_failed">文件传输失败</string>
<string name="file_transmission_cancelled">文件传输已取消</string>
@ -351,7 +351,7 @@
<string name="copy_omemo_clipboard_description">复制OMEMO指纹到剪贴板</string>
<string name="regenerate_omemo_key">重新生成OMEMO密钥</string>
<string name="clear_other_devices">清除设备</string>
<string name="clear_other_devices_desc">清除所有其他设备的 OMEMO 通告?下次设备连接时将重新通告,但可能收不到你发送的消息。</string>
<string name="clear_other_devices_desc">清除所有其他设备的OMEMO通告下次设备连接时将重新通告但可能收不到你发送的消息。</string>
<string name="error_no_keys_to_trust_server_error">此联系人没有可用的密钥。\n从服务器获取密钥失败。也许你的联系人所在服务器发生问题。</string>
<string name="error_no_keys_to_trust_presence">没有可以用于这个账户的密钥。\n请确保你有相互的在线状态的订阅。</string>
<string name="error_trustkeys_title">出错了</string>
@ -380,12 +380,12 @@
<string name="remove_owner_privileges">吊销所有者权限</string>
<string name="remove_from_room">从群聊中移除</string>
<string name="remove_from_channel">从频道中移除</string>
<string name="could_not_change_affiliation">不能修改 %s 的从属关系</string>
<string name="could_not_change_affiliation">不能修改%s的从属关系</string>
<string name="ban_from_conference">从群聊中封禁</string>
<string name="ban_from_channel">从频道中封禁</string>
<string name="removing_from_public_conference">%s将被从公共频道中移除。只有将此用户封禁才能将他永远移除。</string>
<string name="ban_now">立刻封禁</string>
<string name="could_not_change_role">不能修改 %s 的角色</string>
<string name="could_not_change_role">不能修改%s的角色</string>
<string name="conference_options">私密群聊设置</string>
<string name="channel_options">公开频道设置</string>
<string name="members_only">私密,只有成员可以加入</string>
@ -471,9 +471,9 @@
<string name="action_add_account_with_certificate">用证书登录</string>
<string name="unable_to_parse_certificate">无法解析证书</string>
<string name="mam_prefs">存档设置</string>
<string name="server_side_mam_prefs">服务端存档设置</string>
<string name="server_side_mam_prefs">服务端聊天历史存档设置</string>
<string name="fetching_mam_prefs">正在获取存档设置。请稍候……</string>
<string name="unable_to_fetch_mam_prefs">无法获取存档</string>
<string name="unable_to_fetch_mam_prefs">无法获取存档</string>
<string name="captcha_required">需要验证码</string>
<string name="captcha_hint">输入上图文字</string>
<string name="certificate_chain_is_not_trusted">证书链不受信任</string>
@ -490,7 +490,7 @@
<string name="hostname_or_onion">服务器或者.onion地址</string>
<string name="not_a_valid_port">该端口号无效</string>
<string name="not_valid_hostname">该主机名无效</string>
<string name="connected_accounts">%2$d个账户中的%1$d个已连接</string>
<string name="connected_accounts">已连接%2$d个中的%1$d个账户</string>
<plurals name="x_messages">
<item quantity="other">%d条消息</item>
</plurals>
@ -512,7 +512,7 @@
<string name="pref_picture_compression_summary">提示:使用“选择文件”发送原图。这将忽略此设置。</string>
<string name="always">总是</string>
<string name="large_images_only">仅大图片</string>
<string name="battery_optimizations_enabled">节电模式已启用</string>
<string name="battery_optimizations_enabled">已启用节电模式</string>
<string name="battery_optimizations_enabled_explained">你的设备正在为畅聊进行电池优化,这可能导致通知的延迟甚至消息的丢失。\n建议禁用电池优化。</string>
<string name="battery_optimizations_enabled_dialog">你的设备正在为畅聊进行电池优化,这可能导致通知的延迟甚至消息的丢失。\n你将会被提示禁用该功能。</string>
<string name="disable">禁用</string>
@ -670,7 +670,7 @@
<string name="mtm_accept_cert">接受未知的证书?</string>
<string name="mtm_trust_anchor">服务器证书未由已知证书机构签发。</string>
<string name="mtm_accept_servername">接受不匹配的服务器名称?</string>
<string name="mtm_hostname_mismatch">由于 “%s”服务器无法验证。证书仅对此有效</string>
<string name="mtm_hostname_mismatch">由于“%s”服务器无法验证。证书仅对此有效</string>
<string name="mtm_connect_anyway">您仍希望连接吗?</string>
<string name="mtm_cert_details">证书详情:</string>
<string name="once">仅一次</string>
@ -684,7 +684,7 @@
<string name="error_trustkey_device_list">无法获取设备列表</string>
<string name="error_trustkey_bundle">无法获取密钥</string>
<string name="error_trustkey_hint_mutual">提示:某些情况下,可以将对方加入联系人列表,以解决此问题。</string>
<string name="disable_encryption_message">确认要禁用此会话的 OMEMO 加密吗?\n这会允许您的服务器管理员阅读你们的消息但这可能是和使用过时客户端的人会话的唯一方式。</string>
<string name="disable_encryption_message">确认要禁用此会话的OMEMO加密吗\n这会允许您的服务器管理员阅读你们的消息但这可能是和使用过时客户端的人会话的唯一方式。</string>
<string name="disable_now">立即禁用</string>
<string name="draft">草稿:</string>
<string name="pref_omemo_setting">OMEMO加密</string>
@ -699,7 +699,7 @@
<string name="small"></string>
<string name="medium"></string>
<string name="large"></string>
<string name="not_encrypted_for_this_device">该设备的消息未加密。</string>
<string name="not_encrypted_for_this_device">消息未对本设备加密。</string>
<string name="omemo_decryption_failed">解密OMEMO消息失败</string>
<string name="undo">撤销</string>
<string name="location_disabled">位置分享已停用</string>
@ -712,7 +712,7 @@
<string name="title_activity_show_location">显示位置</string>
<string name="share">分享</string>
<string name="unable_to_start_recording">无法开始录制</string>
<string name="please_wait">请等待…</string>
<string name="please_wait">请等待…</string>
<string name="no_microphone_permission">允许畅聊使用麦克风</string>
<string name="search_messages">搜索消息</string>
<string name="gif">GIF动图</string>
@ -783,7 +783,7 @@
<string name="abort_registration_procedure">确定放弃注册?</string>
<string name="yes"></string>
<string name="no"></string>
<string name="verifying">正在验证.....</string>
<string name="verifying">正在验证...</string>
<string name="requesting_sms">请求短信...</string>
<string name="incorrect_pin">验证码错误。</string>
<string name="pin_expired">验证码已失效</string>
@ -875,7 +875,7 @@
<string name="group_chats_and_channels"><![CDATA[群聊与频道]]> </string>
<string name="jabber_network">jabber.network</string>
<string name="local_server">本地服务器</string>
<string name="pref_channel_discovery_summary">大多数用户应该选择“ jabber.network”以从整个XMPP生态系统中获得更好的建议。</string>
<string name="pref_channel_discovery_summary">大多数用户应该选择“jabber.network”以从整个XMPP生态系统中获得更好的建议。</string>
<string name="pref_channel_discovery">频道发现方法</string>
<string name="backup">备份</string>
<string name="category_about">关于</string>