Merge tag '2.9.9' into develop

This commit is contained in:
Geno 2021-03-28 10:36:21 +02:00
commit 35e6c476dd
76 changed files with 2993 additions and 2793 deletions

View File

@ -11,7 +11,7 @@ android:
- '.+' - '.+'
before_script: before_script:
- mkdir libs - mkdir libs
- wget -O libs/libwebrtc-m87.aar https://gultsch.de/files/libwebrtc-m87.aar - wget -O libs/libwebrtc-m89.aar https://gultsch.de/files/libwebrtc-m89.aar
script: script:
- ./gradlew assembleQuicksyFreeCompatDebug - ./gradlew assembleQuicksyFreeCompatDebug
- ./gradlew assembleQuicksyFreeSystemDebug - ./gradlew assembleQuicksyFreeSystemDebug

View File

@ -1,5 +1,9 @@
# Changelog # Changelog
### Version 2.9.9
* Various bug fixes around Tor support
### Version 2.9.8 ### Version 2.9.8
* Verify A/V calls with preexisting OMEMO sessions * Verify A/V calls with preexisting OMEMO sessions

View File

@ -139,7 +139,7 @@ Note: This is kind of a weird quirk in OpenFire. Most other servers would just t
Maybe you attempted to use the Jabber ID `test@b.tld` because `a.tld` doesnt point to the correct host. In that case you might have to enable the extended connection settings in the expert settings of Conversations and set a host name. Maybe you attempted to use the Jabber ID `test@b.tld` because `a.tld` doesnt point to the correct host. In that case you might have to enable the extended connection settings in the expert settings of Conversations and set a host name.
### I get 'Stream opening error'. What does that mean? #### I get 'Stream opening error'. What does that mean?
In most cases this error is caused by ejabberd advertising support for TLSv1.3 but not properly supporting it. This can happen if the OpenSSL version on the server already supports TLSv1.3 but the fast\_tls wrapper library used by ejabberd not (properly) support it. Upgrading fast\_tls and ejabberd or - theoretically - downgrading OpenSSL should fix the issue. A work around is to explicitly disable TLSv1.3 support in the ejabberd configuration. More information can be found on [this issue on the ejabberd issue tracker](https://github.com/processone/ejabberd/issues/2614). In most cases this error is caused by ejabberd advertising support for TLSv1.3 but not properly supporting it. This can happen if the OpenSSL version on the server already supports TLSv1.3 but the fast\_tls wrapper library used by ejabberd not (properly) support it. Upgrading fast\_tls and ejabberd or - theoretically - downgrading OpenSSL should fix the issue. A work around is to explicitly disable TLSv1.3 support in the ejabberd configuration. More information can be found on [this issue on the ejabberd issue tracker](https://github.com/processone/ejabberd/issues/2614).

View File

@ -8,7 +8,7 @@ buildscript {
jcenter() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:4.1.2' classpath 'com.android.tools.build:gradle:4.1.3'
} }
} }
@ -73,9 +73,11 @@ dependencies {
implementation "com.leinardi.android:speed-dial:2.0.1" implementation "com.leinardi.android:speed-dial:2.0.1"
implementation "com.squareup.retrofit2:retrofit:2.9.0" implementation "com.squareup.retrofit2:retrofit:2.9.0"
implementation "com.squareup.retrofit2:converter-gson:2.9.0" implementation "com.squareup.retrofit2:converter-gson:2.9.0"
implementation "com.squareup.okhttp3:okhttp:4.9.1"
implementation 'com.google.guava:guava:30.1-android' implementation 'com.google.guava:guava:30.1-android'
quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.12.18' quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.12.18'
// implementation fileTree(include: ['libwebrtc-m87.aar'], dir: 'libs') // implementation fileTree(include: ['libwebrtc-m89.aar'], dir: 'libs')
implementation 'org.webrtc:google-webrtc:1.0.32006' implementation 'org.webrtc:google-webrtc:1.0.32006'
} }
@ -91,8 +93,8 @@ android {
defaultConfig { defaultConfig {
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 29 targetSdkVersion 29
versionCode 42006 versionCode 42010
versionName "2.9.8" versionName "2.9.9"
archivesBaseName += "-$versionName" archivesBaseName += "-$versionName"
applicationId "eu.sum7.conversations" applicationId "eu.sum7.conversations"
resValue "string", "applicationId", applicationId resValue "string", "applicationId", applicationId
@ -101,6 +103,10 @@ android {
} }
configurations {
compile.exclude group: 'org.jetbrains' , module:'annotations'
}
dataBinding { dataBinding {
enabled true enabled true
} }

View File

@ -1,25 +0,0 @@
Conversations is a messenger for the next decade. Based on already established
internet standards that have been around for over ten years Conversations isnt
trying to replace current commercial messengers. It will simply outlive them.
Commercial, closed source products are coming and going. 15 years ago we had ICQ
which was replaced by Skype. MySpace was replaced by Facebook. WhatsApp and
Hangouts will disappear soon. Internet standards however stick around. People
are still using IRC and e-mail even though these protocols have been around for
decades. Utilizing proven standards doesnt mean one can not evolve. GMail has
revolutionized the way we look at e-mail. Firefox and Chrome have changed the
way we use the Web. Conversations will change the way we look at instant
messaging. Being less obtrusive than a telephone call instant messaging has
always played an important role in modern society. Conversations will show that
instant messaging can be fast, reliable and private. Conversations will not
force its security and privacy aspects upon the user. For those willing to use
encryption Conversations will make it as uncomplicated as possible. However
Conversations is aware that end-to-end encryption by the very principle isnt
trivial. Instead of trying the impossible and making encryption easier than
comparing a fingerprint Conversations will try to educate the willing user and
explain the necessary steps and the reasons behind them. Those unwilling to
learn about encryption will still be protected by the design principals of
Conversations. Conversations will simply not share or generate certain
information for example by encouraging the use of federated servers.
Conversations will always utilize the best available standards for encryption
and media encoding instead of reinventing the wheel. However it isnt afraid to
break with behavior patterns that have been proven ineffective.

View File

@ -1,32 +0,0 @@
* XEP-0027: Current Jabber OpenPGP Usage
* XEP-0030: Service Discovery
* XEP-0045: Multi-User Chat
* XEP-0048: Bookmarks
* XEP-0084: User Avatar
* XEP-0085: Chat State Notifications
* XEP-0092: Software Version
* XEP-0115: Entity Capabilities
* XEP-0163: Personal Eventing Protocol (avatars and nicks)
* XEP-0166: Jingle (only used for file transfer)
* XEP-0172: User Nickname
* XEP-0184: Message Delivery Receipts (reply only)
* XEP-0191: Blocking command
* XEP-0198: Stream Management
* XEP-0199: XMPP Ping
* XEP-0234: Jingle File Transfer
* XEP-0237: Roster Versioning
* XEP-0245: The /me Command
* XEP-0249: Direct MUC Invitations (receiving only)
* XEP-0260: Jingle SOCKS5 Bytestreams Transport Method
* XEP-0261: Jingle In-Band Bytestreams Transport Method
* XEP-0280: Message Carbons
* XEP-0308: Last Message Correction
* XEP-0313: Message Archive Management
* XEP-0319: Last User Interaction in Presence
* XEP-0333: Chat Markers
* XEP-0352: Client State Indication
* XEP-0357: Push Notifications
* XEP-0363: HTTP File Upload
* XEP-0368: SRV records for XMPP over TLS
* XEP-0377: Spam Reporting
* XEP-0384: OMEMO Encryption

View File

@ -1,97 +0,0 @@
Observations on implementing XMPP
=================================
After spending the last two and a half month basically writing my own XMPP
library from scratch I decided to share some of the observations I made in the
process. In part this article can be seen as a response to a blog post made by
Dr. Ing. Georg Lukas. The blog post introduces a couple of XEP (XMPP Extensions)
which make the life on mobile devices a lot easier but states that they are
currently very few implementations of those XEPs. So I went ahead and
implemented all of them in my Android XMPP client.
### General observations
The first thing I noticed is that XMPP is actually okish designed. If you were
to design a new chat protocol today you probably wouldnt choose XML again
however the protocol basically consists of only three different packages which
are quickly hidden under some sort of abstraction layer within your library.
Getting from zero to sending messages to other users actually was very simple
and straight forward. But then came the XEPs.
### Multi-User Chat
The first one was XEP-0045 Multi-User Chat. This is the one XEP of the XEPs Im
going to mention in my article which is actually wildly adopted. Most clients
and servers I know of support MUC. However the level of completeness varies.
MUC actually introduces access and permission roles which are far more complex
than what some of us are used to from IRC but a lot of clients just dont
implement them. Im not implementing them myself (at least for now) because I
somewhat doubt that someone would actually use them (however this might be some
sort of chicken or egg problem). I did find some strange bugs though which might
be interesting for other library developers. In theory a MUC server
implementation can allow a single user (same jid) to join a conference room
multiple times with the same nick from different clients. This means if someone
wants to participate in a conference from two different devices (mobile and
desktop for example) one wouldnt have to name oneself `userDesktop` and
`userMobile` but just `user`. Both ejabberd and prosody support this but with
strange side effects. Prosody for example doesnt allow a user to change its
name once two clients are “merged” by having the same nick.
### Carbons and Stream Management
Two of the other XEPs Lukas mentions — Carbons (XEP-0280) and Stream Management
(XEP-0198) — were actually fairly easy to implement. The only challenges were to
find a server to support them (I ended up running my own Prosody server) and a
desktop client to test them with. For carbons there is a patched Mcabber version
and Gajim. After implementing stream management I had very good results on my
mobile device. I had sessions running for up to 24 hours with a walking outside,
loosing mobile coverage for a few minutes and so on. The only limitation was
that I had to keep on developing and reinstalling my app.
### Off the record
And then came OTR... This is were I spend the most time debugging stuff and
trying to get things right and compatible with other clients. This is the part
were I want to help other developers not to make the same mistakes and maybe
come to some sort of consent among XMPP developers to ultimately increase the
interoperability. OTR has some down sides which make it difficult or at times
even dangerous to implement within XMPP. First of all it is a synchronous
protocol which is tunneled through a different protocol (XMPP). Synchronous
means — among other things — auto replies. (An OTR session begins with “hi Im
speaking otr give me your key” “ok cool here is my key”) And auto replies — we
know that since the first time an out of office auto responder went postal — are
dangerous. Things really start to get messy when you use one of the best
features of XMPP — multiple clients. The way XMPP works is that clients are
encouraged to send their messages to the raw jid and let the server decide what
full jid the messages are routed to. If in doubt even all of them. So what
happens when Alice sends a start-otr-message to Bobs raw jid? Bob receives the
message on his notebook as well as his cell phone. Both of them answer. Alice
gets two different replies. Shit explodes. Even if Alice sends the message to
bob/notebook chances are that Bob has carbon messages enabled and still receives
the messages on both devices. Now assuming that Bobs client is clever enough not
to auto reply to carbonated messages Bob/cellphone will still end up with a lot
of garbage messages. (Essentially the entire conversation between Alice and
Bob/notebook but unreadable of course) Therefor it should be good practice to
tag OTR messages as both private and no-copy (private is part of the carbons
XEP, no-copy is a general hint). I found that prosody for some reasons doesnt
honor the private tag on outgoing messages. While this is easily fixed I presume
that having both the private and the no-copy tag will make it more compatible
with servers or clients I dont know about yet.
#### Rules to follow when implementing OTR
To summarize my observations on implementing OTR in XMPP let me make the
following three statements.
1. While it is good practice for unencrypted messages to be send to the raw jid
and have the receiving server or user decide how they should be routed OTR
messages must be send to a specific resource. To make this work the user should
be given the option to select the presence (which can be assisted with some
educated guessing by the client based on previous messages). Furthermore a
client should encourage a user to choose meaningful presences instead of the
clients name or even random ones. Something like `/mobile`, `/notebook`,
`/desktop` is a greater assist to any one who wants to start an otr session then
`/Gajim`, `/mcabber` or `/pidgin`.
2. Messages should be tagged private and no-copy to avoid unnecessary traffic or
otr error loops with faulty clients. This tagging should be done even if your
own client doesnt support carbons.
3. When dealing with “legacy clients” — meaning clients which dont follow my
advise — a client should be extra careful not to create message loops. This
means to not respond with otr errors if a client is not 100% sure it is the only
client which received the message

View File

@ -0,0 +1 @@
• Various bug fixes around Tor support

View File

@ -3,9 +3,11 @@ package eu.siacs.conversations;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.net.Uri; import android.net.Uri;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import eu.siacs.conversations.crypto.XmppDomainVerifier;
import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.chatstate.ChatState; import eu.siacs.conversations.xmpp.chatstate.ChatState;
@ -105,6 +107,7 @@ public final class Config {
public static final boolean USE_BOOKMARKS2 = false; public static final boolean USE_BOOKMARKS2 = false;
public static final boolean PROCESS_EXTMAP_ALLOW_MIXED = false;
public static final boolean DISABLE_PROXY_LOOKUP = false; //useful to debug ibb public static final boolean DISABLE_PROXY_LOOKUP = false; //useful to debug ibb
public static final boolean USE_DIRECT_JINGLE_CANDIDATES = true; public static final boolean USE_DIRECT_JINGLE_CANDIDATES = true;
public static final boolean DISABLE_HTTP_UPLOAD = false; public static final boolean DISABLE_HTTP_UPLOAD = false;
@ -174,7 +177,14 @@ public final class Config {
//if the contacts domain matches one of the following domains OMEMO wont be turned on automatically //if the contacts domain matches one of the following domains OMEMO wont be turned on automatically
//can be used for well known, widely used gateways //can be used for well known, widely used gateways
public static final List<String> CONTACT_DOMAINS = Collections.singletonList("cheogram.com"); private static final List<String> CONTACT_DOMAINS = Arrays.asList(
"cheogram.com",
"*.covid.monal.im"
);
public static boolean matchesContactDomain(final String domain) {
return XmppDomainVerifier.matchDomain(domain, CONTACT_DOMAINS);
}
} }
private Config() { private Config() {

View File

@ -14,7 +14,6 @@ import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.URL;
import java.util.ArrayDeque; import java.util.ArrayDeque;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@ -209,7 +208,7 @@ public class PgpDecryptionService {
message.setRelativeFilePath(path); message.setRelativeFilePath(path);
} }
} }
URL url = message.getFileParams().url; final String url = message.getFileParams().url;
mXmppConnectionService.getFileBackend().updateFileParams(message, url); mXmppConnectionService.getFileBackend().updateFileParams(message, url);
message.setEncryption(Message.ENCRYPTION_DECRYPTED); message.setEncryption(Message.ENCRYPTION_DECRYPTED);
mXmppConnectionService.updateMessage(message); mXmppConnectionService.updateMessage(message);

View File

@ -6,17 +6,14 @@ import android.util.Log;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import com.google.common.base.CharMatcher;
import com.google.common.base.Joiner; import com.google.common.base.Joiner;
import com.google.common.base.Splitter; import com.google.common.base.Splitter;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.io.BaseEncoding;
import org.openintents.openpgp.OpenPgpError; import org.openintents.openpgp.OpenPgpError;
import org.openintents.openpgp.OpenPgpSignatureResult; import org.openintents.openpgp.OpenPgpSignatureResult;
import org.openintents.openpgp.util.OpenPgpApi; import org.openintents.openpgp.util.OpenPgpApi;
import org.openintents.openpgp.util.OpenPgpApi.IOpenPgpCallback; import org.openintents.openpgp.util.OpenPgpApi.IOpenPgpCallback;
import org.openintents.openpgp.util.OpenPgpUtils;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
@ -75,7 +72,7 @@ public class PgpEngine {
params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true);
String body; String body;
if (message.hasFileOnRemoteHost()) { if (message.hasFileOnRemoteHost()) {
body = message.getFileParams().url.toString(); body = message.getFileParams().url;
} else { } else {
body = message.getBody(); body = message.getBody();
} }

View File

@ -1,6 +1,5 @@
package eu.siacs.conversations.crypto; package eu.siacs.conversations.crypto;
import android.os.Build;
import android.util.Log; import android.util.Log;
import android.util.Pair; import android.util.Pair;
@ -72,8 +71,8 @@ public class XmppDomainVerifier implements DomainHostnameVerifier {
} }
} }
private static boolean matchDomain(String needle, List<String> haystack) { public static boolean matchDomain(final String needle, final List<String> haystack) {
for (String entry : haystack) { for (final String entry : haystack) {
if (entry.startsWith("*.")) { if (entry.startsWith("*.")) {
int offset = 0; int offset = 0;
while (offset < needle.length()) { while (offset < needle.length()) {
@ -81,16 +80,13 @@ public class XmppDomainVerifier implements DomainHostnameVerifier {
if (i < 0) { if (i < 0) {
break; break;
} }
Log.d(LOGTAG, "comparing " + needle.substring(i) + " and " + entry.substring(1));
if (needle.substring(i).equalsIgnoreCase(entry.substring(1))) { if (needle.substring(i).equalsIgnoreCase(entry.substring(1))) {
Log.d(LOGTAG, "domain " + needle + " matched " + entry);
return true; return true;
} }
offset = i + 1; offset = i + 1;
} }
} else { } else {
if (entry.equalsIgnoreCase(needle)) { if (entry.equalsIgnoreCase(needle)) {
Log.d(LOGTAG, "domain " + needle + " matched " + entry);
return true; return true;
} }
} }

View File

@ -1169,7 +1169,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId()); final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId());
final String content; final String content;
if (message.hasFileOnRemoteHost()) { if (message.hasFileOnRemoteHost()) {
content = message.getFileParams().url.toString(); content = message.getFileParams().url;
} else { } else {
content = message.getBody(); content = message.getBody();
} }

View File

@ -4,7 +4,6 @@ import android.content.ContentValues;
import android.database.Cursor; import android.database.Cursor;
import android.os.SystemClock; import android.os.SystemClock;
import android.util.Log; import android.util.Log;
import android.util.Pair;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
@ -147,7 +146,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
} }
public boolean httpUploadAvailable(long filesize) { public boolean httpUploadAvailable(long filesize) {
return xmppConnection != null && (xmppConnection.getFeatures().httpUpload(filesize) || xmppConnection.getFeatures().p1S3FileTransfer()); return xmppConnection != null && xmppConnection.getFeatures().httpUpload(filesize);
} }
public boolean httpUploadAvailable() { public boolean httpUploadAvailable() {

View File

@ -143,7 +143,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
} }
final String contact = conversation.getJid().getDomain().toEscapedString(); final String contact = conversation.getJid().getDomain().toEscapedString();
final String account = conversation.getAccount().getServer(); final String account = conversation.getAccount().getServer();
if (Config.OMEMO_EXCEPTIONS.CONTACT_DOMAINS.contains(contact) || Config.OMEMO_EXCEPTIONS.ACCOUNT_DOMAINS.contains(account)) { if (Config.OMEMO_EXCEPTIONS.matchesContactDomain(contact) || Config.OMEMO_EXCEPTIONS.ACCOUNT_DOMAINS.contains(account)) {
return false; return false;
} }
return conversation.isSingleOrPrivateAndNonAnonymous() || conversation.getBooleanAttribute(ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS, false); return conversation.isSingleOrPrivateAndNonAnonymous() || conversation.getBooleanAttribute(ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS, false);
@ -788,7 +788,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) { if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) {
String otherBody; String otherBody;
if (message.hasFileOnRemoteHost()) { if (message.hasFileOnRemoteHost()) {
otherBody = message.getFileParams().url.toString(); otherBody = message.getFileParams().url;
} else { } else {
otherBody = message.body; otherBody = message.body;
} }

File diff suppressed because it is too large Load Diff

View File

@ -408,20 +408,6 @@ public class IqGenerator extends AbstractGenerator {
return packet; return packet;
} }
public IqPacket requestP1S3Slot(Jid host, String md5) {
IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
packet.setTo(host);
packet.query(Namespace.P1_S3_FILE_TRANSFER).setAttribute("md5", md5);
return packet;
}
public IqPacket requestP1S3Url(Jid host, String fileId) {
IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
packet.setTo(host);
packet.query(Namespace.P1_S3_FILE_TRANSFER).setAttribute("fileid", fileId);
return packet;
}
private static String convertFilename(String name) { private static String convertFilename(String name) {
int pos = name.indexOf('.'); int pos = name.indexOf('.');
if (pos != -1) { if (pos != -1) {

View File

@ -1,6 +1,5 @@
package eu.siacs.conversations.generator; package eu.siacs.conversations.generator;
import java.net.URL;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
@ -14,7 +13,6 @@ import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Conversational; import eu.siacs.conversations.entities.Conversational;
import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.http.P1S3UrlStreamHandler;
import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xml.Namespace;
@ -103,18 +101,9 @@ public class MessageGenerator extends AbstractGenerator {
MessagePacket packet = preparePacket(message); MessagePacket packet = preparePacket(message);
String content; String content;
if (message.hasFileOnRemoteHost()) { if (message.hasFileOnRemoteHost()) {
Message.FileParams fileParams = message.getFileParams(); final Message.FileParams fileParams = message.getFileParams();
final URL url = fileParams.url; content = fileParams.url;
if (P1S3UrlStreamHandler.PROTOCOL_NAME.equals(url.getProtocol())) { packet.addChild("x", Namespace.OOB).addChild("url").setContent(content);
Element x = packet.addChild("x", Namespace.P1_S3_FILE_TRANSFER);
final String file = url.getFile();
x.setAttribute("name", file.charAt(0) == '/' ? file.substring(1) : file);
x.setAttribute("fileid", url.getHost());
return packet;
} else {
content = url.toString();
packet.addChild("x", Namespace.OOB).addChild("url").setContent(content);
}
} else { } else {
content = message.getBody(); content = message.getBody();
} }
@ -126,16 +115,9 @@ public class MessageGenerator extends AbstractGenerator {
MessagePacket packet = preparePacket(message); MessagePacket packet = preparePacket(message);
if (message.hasFileOnRemoteHost()) { if (message.hasFileOnRemoteHost()) {
Message.FileParams fileParams = message.getFileParams(); Message.FileParams fileParams = message.getFileParams();
final URL url = fileParams.url; final String url = fileParams.url;
if (P1S3UrlStreamHandler.PROTOCOL_NAME.equals(url.getProtocol())) { packet.setBody(url);
Element x = packet.addChild("x", Namespace.P1_S3_FILE_TRANSFER); packet.addChild("x", Namespace.OOB).addChild("url").setContent(url);
final String file = url.getFile();
x.setAttribute("name", file.charAt(0) == '/' ? file.substring(1) : file);
x.setAttribute("fileid", url.getHost());
} else {
packet.setBody(url.toString());
packet.addChild("x", Namespace.OOB).addChild("url").setContent(url.toString());
}
} else { } else {
if (Config.supportUnencrypted()) { if (Config.supportUnencrypted()) {
packet.setBody(PGP_FALLBACK_MESSAGE); packet.setBody(PGP_FALLBACK_MESSAGE);
@ -225,7 +207,7 @@ public class MessageGenerator extends AbstractGenerator {
return packet; return packet;
} }
public MessagePacket received(Account account, final Jid from, final String id, ArrayList<String> namespaces, int type) { public MessagePacket received(Account account, final Jid from, final String id, ArrayList<String> namespaces, int type) {
final MessagePacket receivedPacket = new MessagePacket(); final MessagePacket receivedPacket = new MessagePacket();
receivedPacket.setType(type); receivedPacket.setType(type);
receivedPacket.setTo(from); receivedPacket.setTo(from);

View File

@ -0,0 +1,41 @@
package eu.siacs.conversations.http;
import java.util.regex.Pattern;
import okhttp3.HttpUrl;
public final class AesGcmURL {
/**
* This matches a 48 or 44 byte IV + KEY hex combo, like used in http/aesgcm upload anchors
*/
public static final Pattern IV_KEY = Pattern.compile("([A-Fa-f0-9]{2}){48}|([A-Fa-f0-9]{2}){44}");
public static final String PROTOCOL_NAME = "aesgcm";
private AesGcmURL() {
}
public static String toAesGcmUrl(HttpUrl url) {
if (url.isHttps()) {
return PROTOCOL_NAME + url.toString().substring(5);
} else {
return url.toString();
}
}
public static HttpUrl of(final String url) {
final int end = url.indexOf("://");
if (end < 0) {
throw new IllegalArgumentException("Scheme not found");
}
final String protocol = url.substring(0, end);
if (PROTOCOL_NAME.equals(protocol)) {
return HttpUrl.get("https" + url.substring(PROTOCOL_NAME.length()));
} else {
return HttpUrl.get(url);
}
}
}

View File

@ -1,23 +0,0 @@
package eu.siacs.conversations.http;
import java.io.IOException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.regex.Pattern;
public class AesGcmURLStreamHandler extends URLStreamHandler {
/**
* This matches a 48 or 44 byte IV + KEY hex combo, like used in http/aesgcm upload anchors
*/
public static final Pattern IV_KEY = Pattern.compile("([A-Fa-f0-9]{2}){48}|([A-Fa-f0-9]{2}){44}");
public static final String PROTOCOL_NAME = "aesgcm";
@Override
protected URLConnection openConnection(URL url) throws IOException {
return new URL("https"+url.toString().substring(url.getProtocol().length())).openConnection();
}
}

View File

@ -1,18 +0,0 @@
package eu.siacs.conversations.http;
import java.net.URLStreamHandler;
import java.net.URLStreamHandlerFactory;
public class CustomURLStreamHandlerFactory implements URLStreamHandlerFactory {
@Override
public URLStreamHandler createURLStreamHandler(String protocol) {
if (AesGcmURLStreamHandler.PROTOCOL_NAME.equals(protocol)) {
return new AesGcmURLStreamHandler();
} else if (P1S3UrlStreamHandler.PROTOCOL_NAME.equals(protocol)) {
return new P1S3UrlStreamHandler();
} else {
return null;
}
}
}

View File

@ -1,23 +1,25 @@
package eu.siacs.conversations.http; package eu.siacs.conversations.http;
import android.os.Build;
import android.util.Log; import android.util.Log;
import org.apache.http.conn.ssl.StrictHostnameVerifier; import org.apache.http.conn.ssl.StrictHostnameVerifier;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.Proxy; import java.net.Proxy;
import java.net.URL; import java.net.UnknownHostException;
import java.security.KeyManagementException; import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.X509TrustManager; import javax.net.ssl.X509TrustManager;
@ -27,6 +29,10 @@ import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.services.AbstractConnectionManager; import eu.siacs.conversations.services.AbstractConnectionManager;
import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.TLSSocketFactory; import eu.siacs.conversations.utils.TLSSocketFactory;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.ResponseBody;
public class HttpConnectionManager extends AbstractConnectionManager { public class HttpConnectionManager extends AbstractConnectionManager {
@ -39,8 +45,18 @@ public class HttpConnectionManager extends AbstractConnectionManager {
super(service); super(service);
} }
public static Proxy getProxy() throws IOException { public static Proxy getProxy() {
return new Proxy(Proxy.Type.SOCKS, new InetSocketAddress(InetAddress.getByAddress(new byte[]{127, 0, 0, 1}), 9050)); final InetAddress localhost;
try {
localhost = InetAddress.getByAddress(new byte[]{127, 0, 0, 1});
} catch (final UnknownHostException e) {
throw new IllegalStateException(e);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return new Proxy(Proxy.Type.SOCKS, new InetSocketAddress(localhost, 9050));
} else {
return new Proxy(Proxy.Type.HTTP, new InetSocketAddress(localhost, 8118));
}
} }
public void createNewDownloadConnection(Message message) { public void createNewDownloadConnection(Message message) {
@ -75,15 +91,6 @@ public class HttpConnectionManager extends AbstractConnectionManager {
} }
} }
public boolean checkConnection(Message message) {
final Account account = message.getConversation().getAccount();
final URL url = message.getFileParams().url;
if (url.getProtocol().equalsIgnoreCase(P1S3UrlStreamHandler.PROTOCOL_NAME) && account.getStatus() != Account.State.ONLINE) {
return false;
}
return mXmppConnectionService.hasInternetConnection();
}
void finishConnection(HttpDownloadConnection connection) { void finishConnection(HttpDownloadConnection connection) {
synchronized (this.downloadConnections) { synchronized (this.downloadConnections) {
this.downloadConnections.remove(connection); this.downloadConnections.remove(connection);
@ -96,7 +103,21 @@ public class HttpConnectionManager extends AbstractConnectionManager {
} }
} }
void setupTrustManager(final HttpsURLConnection connection, final boolean interactive) { OkHttpClient buildHttpClient(final HttpUrl url, final Account account, boolean interactive) {
final String slotHostname = url.host();
final boolean onionSlot = slotHostname.endsWith(".onion");
final OkHttpClient.Builder builder = new OkHttpClient.Builder();
//builder.addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.HEADERS));
builder.writeTimeout(30, TimeUnit.SECONDS);
builder.readTimeout(30, TimeUnit.SECONDS);
setupTrustManager(builder, interactive);
if (mXmppConnectionService.useTorToConnect() || account.isOnion() || onionSlot) {
builder.proxy(HttpConnectionManager.getProxy()).build();
}
return builder.build();
}
private void setupTrustManager(final OkHttpClient.Builder builder, final boolean interactive) {
final X509TrustManager trustManager; final X509TrustManager trustManager;
final HostnameVerifier hostnameVerifier = mXmppConnectionService.getMemorizingTrustManager().wrapHostnameVerifier(new StrictHostnameVerifier(), interactive); final HostnameVerifier hostnameVerifier = mXmppConnectionService.getMemorizingTrustManager().wrapHostnameVerifier(new StrictHostnameVerifier(), interactive);
if (interactive) { if (interactive) {
@ -106,9 +127,27 @@ public class HttpConnectionManager extends AbstractConnectionManager {
} }
try { try {
final SSLSocketFactory sf = new TLSSocketFactory(new X509TrustManager[]{trustManager}, mXmppConnectionService.getRNG()); final SSLSocketFactory sf = new TLSSocketFactory(new X509TrustManager[]{trustManager}, mXmppConnectionService.getRNG());
connection.setSSLSocketFactory(sf); builder.sslSocketFactory(sf, trustManager);
connection.setHostnameVerifier(hostnameVerifier); builder.hostnameVerifier(hostnameVerifier);
} catch (final KeyManagementException | NoSuchAlgorithmException ignored) { } catch (final KeyManagementException | NoSuchAlgorithmException ignored) {
} }
} }
public static InputStream open(final String url, final boolean tor) throws IOException {
return open(HttpUrl.get(url), tor);
}
public static InputStream open(final HttpUrl httpUrl, final boolean tor) throws IOException {
final OkHttpClient.Builder builder = new OkHttpClient.Builder();
if (tor) {
builder.proxy(HttpConnectionManager.getProxy()).build();
}
final OkHttpClient client = builder.build();
final Request request = new Request.Builder().get().url(httpUrl).build();
final ResponseBody body = client.newCall(request).execute().body();
if (body == null) {
throw new IOException("No response body found");
}
return body.byteStream();
}
} }

View File

@ -1,29 +1,23 @@
package eu.siacs.conversations.http; package eu.siacs.conversations.http;
import android.os.PowerManager;
import android.util.Log; import android.util.Log;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.io.ByteStreams; import com.google.common.io.ByteStreams;
import com.google.common.primitives.Longs;
import java.io.BufferedInputStream;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.HttpURLConnection; import java.util.Locale;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.concurrent.CancellationException;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.SSLHandshakeException;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.R; import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.entities.Transferable;
@ -33,30 +27,30 @@ import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.FileWriterException; import eu.siacs.conversations.utils.FileWriterException;
import eu.siacs.conversations.utils.MimeUtils; import eu.siacs.conversations.utils.MimeUtils;
import eu.siacs.conversations.utils.WakeLockHelper; import okhttp3.Call;
import eu.siacs.conversations.xmpp.stanzas.IqPacket; import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import static eu.siacs.conversations.http.HttpConnectionManager.EXECUTOR; import static eu.siacs.conversations.http.HttpConnectionManager.EXECUTOR;
public class HttpDownloadConnection implements Transferable { public class HttpDownloadConnection implements Transferable {
private final Message message; private final Message message;
private final boolean mUseTor;
private final HttpConnectionManager mHttpConnectionManager; private final HttpConnectionManager mHttpConnectionManager;
private final XmppConnectionService mXmppConnectionService; private final XmppConnectionService mXmppConnectionService;
private URL mUrl; private HttpUrl mUrl;
private DownloadableFile file; private DownloadableFile file;
private int mStatus = Transferable.STATUS_UNKNOWN; private int mStatus = Transferable.STATUS_UNKNOWN;
private boolean acceptedAutomatically = false; private boolean acceptedAutomatically = false;
private int mProgress = 0; private int mProgress = 0;
private boolean canceled = false; private Call mostRecentCall;
private Method method = Method.HTTP_UPLOAD;
HttpDownloadConnection(Message message, HttpConnectionManager manager) { HttpDownloadConnection(Message message, HttpConnectionManager manager) {
this.message = message; this.message = message;
this.mHttpConnectionManager = manager; this.mHttpConnectionManager = manager;
this.mXmppConnectionService = manager.getXmppConnectionService(); this.mXmppConnectionService = manager.getXmppConnectionService();
this.mUseTor = mXmppConnectionService.useTorToConnect();
} }
@Override @Override
@ -88,13 +82,13 @@ public class HttpDownloadConnection implements Transferable {
try { try {
final Message.FileParams fileParams = message.getFileParams(); final Message.FileParams fileParams = message.getFileParams();
if (message.hasFileOnRemoteHost()) { if (message.hasFileOnRemoteHost()) {
mUrl = CryptoHelper.toHttpsUrl(fileParams.url); mUrl = AesGcmURL.of(fileParams.url);
} else if (message.isOOb() && fileParams.url != null && fileParams.size > 0) { } else if (message.isOOb() && fileParams.url != null && fileParams.size > 0) {
mUrl = fileParams.url; mUrl = AesGcmURL.of(fileParams.url);
} else { } else {
mUrl = CryptoHelper.toHttpsUrl(new URL(message.getBody().split("\n")[0])); mUrl = AesGcmURL.of(message.getBody().split("\n")[0]);
} }
final AbstractConnectionManager.Extension extension = AbstractConnectionManager.Extension.of(mUrl.getPath()); final AbstractConnectionManager.Extension extension = AbstractConnectionManager.Extension.of(mUrl.encodedPath());
if (VALID_CRYPTO_EXTENSIONS.contains(extension.main)) { if (VALID_CRYPTO_EXTENSIONS.contains(extension.main)) {
this.message.setEncryption(Message.ENCRYPTION_PGP); this.message.setEncryption(Message.ENCRYPTION_PGP);
} else if (message.getEncryption() != Message.ENCRYPTION_OTR } else if (message.getEncryption() != Message.ENCRYPTION_OTR
@ -111,22 +105,22 @@ public class HttpDownloadConnection implements Transferable {
if (this.message.getEncryption() == Message.ENCRYPTION_AXOLOTL && this.file.getKey() == null) { if (this.message.getEncryption() == Message.ENCRYPTION_AXOLOTL && this.file.getKey() == null) {
this.message.setEncryption(Message.ENCRYPTION_NONE); this.message.setEncryption(Message.ENCRYPTION_NONE);
} }
method = mUrl.getProtocol().equalsIgnoreCase(P1S3UrlStreamHandler.PROTOCOL_NAME) ? Method.P1_S3 : Method.HTTP_UPLOAD; //TODO add auth tag size to knownFileSize
long knownFileSize = message.getFileParams().size; final long knownFileSize = message.getFileParams().size;
if (knownFileSize > 0 && interactive && method != Method.P1_S3) { if (knownFileSize > 0 && interactive) {
this.file.setExpectedSize(knownFileSize); this.file.setExpectedSize(knownFileSize);
download(true); download(true);
} else { } else {
checkFileSize(interactive); checkFileSize(interactive);
} }
} catch (MalformedURLException e) { } catch (final IllegalArgumentException e) {
this.cancel(); this.cancel();
} }
} }
private void setupFile() { private void setupFile() {
final String reference = mUrl.getRef(); final String reference = mUrl.fragment();
if (reference != null && AesGcmURLStreamHandler.IV_KEY.matcher(reference).matches()) { if (reference != null && AesGcmURL.IV_KEY.matcher(reference).matches()) {
this.file = new DownloadableFile(mXmppConnectionService.getCacheDir().getAbsolutePath() + "/" + message.getUuid()); this.file = new DownloadableFile(mXmppConnectionService.getCacheDir().getAbsolutePath() + "/" + message.getUuid());
this.file.setKeyAndIv(CryptoHelper.hexToBytes(reference)); this.file.setKeyAndIv(CryptoHelper.hexToBytes(reference));
Log.d(Config.LOGTAG, "create temporary OMEMO encrypted file: " + this.file.getAbsolutePath() + "(" + message.getMimeType() + ")"); Log.d(Config.LOGTAG, "create temporary OMEMO encrypted file: " + this.file.getAbsolutePath() + "(" + message.getMimeType() + ")");
@ -145,7 +139,10 @@ public class HttpDownloadConnection implements Transferable {
@Override @Override
public void cancel() { public void cancel() {
this.canceled = true; final Call call = this.mostRecentCall;
if (call != null && !call.isCanceled()) {
call.cancel();
}
mHttpConnectionManager.finishConnection(this); mHttpConnectionManager.finishConnection(this);
message.setTransferable(null); message.setTransferable(null);
if (message.isFileOrImage()) { if (message.isFileOrImage()) {
@ -209,14 +206,19 @@ public class HttpDownloadConnection implements Transferable {
mHttpConnectionManager.updateConversationUi(true); mHttpConnectionManager.updateConversationUi(true);
} }
private void showToastForException(Exception e) { private void showToastForException(final Exception e) {
final Call call = mostRecentCall;
final boolean cancelled = call != null && call.isCanceled();
if (e == null || cancelled) {
return;
}
if (e instanceof java.net.UnknownHostException) { if (e instanceof java.net.UnknownHostException) {
mXmppConnectionService.showErrorToastInUi(R.string.download_failed_server_not_found); mXmppConnectionService.showErrorToastInUi(R.string.download_failed_server_not_found);
} else if (e instanceof java.net.ConnectException) { } else if (e instanceof java.net.ConnectException) {
mXmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_connect); mXmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_connect);
} else if (e instanceof FileWriterException) { } else if (e instanceof FileWriterException) {
mXmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_write_file); mXmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_write_file);
} else if (!(e instanceof CancellationException)) { } else {
mXmppConnectionService.showErrorToastInUi(R.string.download_failed_file_not_found); mXmppConnectionService.showErrorToastInUi(R.string.download_failed_file_not_found);
} }
} }
@ -260,41 +262,13 @@ public class HttpDownloadConnection implements Transferable {
@Override @Override
public void run() { public void run() {
if (mUrl.getProtocol().equalsIgnoreCase(P1S3UrlStreamHandler.PROTOCOL_NAME)) { check();
retrieveUrl();
} else {
check();
}
} }
private void retrieveUrl() { private void retrieveFailed(@Nullable final Exception e) {
changeStatus(STATUS_CHECKING);
final Account account = message.getConversation().getAccount();
IqPacket request = mXmppConnectionService.getIqGenerator().requestP1S3Url(account.getDomain(), mUrl.getHost());
mXmppConnectionService.sendIqPacket(message.getConversation().getAccount(), request, (a, packet) -> {
if (packet.getType() == IqPacket.TYPE.RESULT) {
String download = packet.query().getAttribute("download");
if (download != null) {
try {
mUrl = new URL(download);
check();
return;
} catch (MalformedURLException e) {
//fallthrough
}
}
}
Log.d(Config.LOGTAG, "unable to retrieve actual download url");
retrieveFailed(null);
});
}
private void retrieveFailed(@Nullable Exception e) {
changeStatus(STATUS_OFFER_CHECK_FILESIZE); changeStatus(STATUS_OFFER_CHECK_FILESIZE);
if (interactive) { if (interactive) {
if (e != null) { showToastForException(e);
showToastForException(e);
}
} else { } else {
HttpDownloadConnection.this.acceptedAutomatically = false; HttpDownloadConnection.this.acceptedAutomatically = false;
HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message); HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message);
@ -306,7 +280,7 @@ public class HttpDownloadConnection implements Transferable {
long size; long size;
try { try {
size = retrieveFileSize(); size = retrieveFileSize();
} catch (Exception e) { } catch (final Exception e) {
Log.d(Config.LOGTAG, "io exception in http file size checker: " + e.getMessage()); Log.d(Config.LOGTAG, "io exception in http file size checker: " + e.getMessage());
retrieveFailed(e); retrieveFailed(e);
return; return;
@ -330,46 +304,23 @@ public class HttpDownloadConnection implements Transferable {
} }
private long retrieveFileSize() throws IOException { private long retrieveFileSize() throws IOException {
Log.d(Config.LOGTAG, "retrieve file size. interactive:" + interactive);
changeStatus(STATUS_CHECKING);
final OkHttpClient client = mHttpConnectionManager.buildHttpClient(
mUrl,
message.getConversation().getAccount(),
interactive
);
final Request request = new Request.Builder()
.url(URL.stripFragment(mUrl))
.head()
.build();
mostRecentCall = client.newCall(request);
try { try {
Log.d(Config.LOGTAG, "retrieve file size. interactive:" + interactive); final Response response = mostRecentCall.execute();
changeStatus(STATUS_CHECKING); final String contentLength = response.header("Content-Length");
HttpURLConnection connection; final String contentType = response.header("Content-Type");
final String hostname = mUrl.getHost(); final AbstractConnectionManager.Extension extension = AbstractConnectionManager.Extension.of(mUrl.encodedPath());
final boolean onion = hostname != null && hostname.endsWith(".onion");
if (mUseTor || message.getConversation().getAccount().isOnion() || onion) {
connection = (HttpURLConnection) mUrl.openConnection(HttpConnectionManager.getProxy());
} else {
connection = (HttpURLConnection) mUrl.openConnection();
}
if (method == Method.P1_S3) {
connection.setRequestMethod("GET");
connection.addRequestProperty("Range", "bytes=0-0");
} else {
connection.setRequestMethod("HEAD");
}
connection.setUseCaches(false);
Log.d(Config.LOGTAG, "url: " + connection.getURL().toString());
connection.setRequestProperty("User-Agent", mXmppConnectionService.getIqGenerator().getUserAgent());
if (connection instanceof HttpsURLConnection) {
mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive);
}
connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000);
connection.connect();
String contentLength;
if (method == Method.P1_S3) {
String contentRange = connection.getHeaderField("Content-Range");
String[] contentRangeParts = contentRange == null ? new String[0] : contentRange.split("/");
if (contentRangeParts.length != 2) {
contentLength = null;
} else {
contentLength = contentRangeParts[1];
}
} else {
contentLength = connection.getHeaderField("Content-Length");
}
final String contentType = connection.getContentType();
final AbstractConnectionManager.Extension extension = AbstractConnectionManager.Extension.of(mUrl.getPath());
if (Strings.isNullOrEmpty(extension.getExtension()) && contentType != null) { if (Strings.isNullOrEmpty(extension.getExtension()) && contentType != null) {
final String fileExtension = MimeUtils.guessExtensionFromMimeType(contentType); final String fileExtension = MimeUtils.guessExtensionFromMimeType(contentType);
if (fileExtension != null) { if (fileExtension != null) {
@ -378,8 +329,7 @@ public class HttpDownloadConnection implements Transferable {
setupFile(); setupFile();
} }
} }
connection.disconnect(); if (Strings.isNullOrEmpty(contentLength)) {
if (contentLength == null) {
throw new IOException("no content-length found in HEAD response"); throw new IOException("no content-length found in HEAD response");
} }
return Long.parseLong(contentLength, 10); return Long.parseLong(contentLength, 10);
@ -397,8 +347,6 @@ public class HttpDownloadConnection implements Transferable {
private final boolean interactive; private final boolean interactive;
private OutputStream os;
public FileDownloader(boolean interactive) { public FileDownloader(boolean interactive) {
this.interactive = interactive; this.interactive = interactive;
} }
@ -411,9 +359,10 @@ public class HttpDownloadConnection implements Transferable {
decryptIfNeeded(); decryptIfNeeded();
updateImageBounds(); updateImageBounds();
finish(); finish();
} catch (SSLHandshakeException e) { } catch (final SSLHandshakeException e) {
changeStatus(STATUS_OFFER); changeStatus(STATUS_OFFER);
} catch (Exception e) { } catch (final Exception e) {
Log.d(Config.LOGTAG, message.getConversation().getAccount().getJid().asBareJid() + ": unable to download file", e);
if (interactive) { if (interactive) {
showToastForException(e); showToastForException(e);
} else { } else {
@ -425,104 +374,77 @@ public class HttpDownloadConnection implements Transferable {
} }
private void download() throws Exception { private void download() throws Exception {
InputStream is = null; final OkHttpClient client = mHttpConnectionManager.buildHttpClient(
HttpURLConnection connection = null; mUrl,
final PowerManager.WakeLock wakeLock = mHttpConnectionManager.createWakeLock(Thread.currentThread()); message.getConversation().getAccount(),
try { interactive
wakeLock.acquire(); );
if (mUseTor || message.getConversation().getAccount().isOnion()) {
connection = (HttpURLConnection) mUrl.openConnection(HttpConnectionManager.getProxy());
} else {
connection = (HttpURLConnection) mUrl.openConnection();
}
if (connection instanceof HttpsURLConnection) {
mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive);
}
connection.setUseCaches(false);
connection.setRequestProperty("User-Agent", mXmppConnectionService.getIqGenerator().getUserAgent());
final long expected = file.getExpectedSize();
final boolean tryResume = file.exists() && file.getSize() > 0 && file.getSize() < expected;
long resumeSize = 0;
if (tryResume) { final Request.Builder requestBuilder = new Request.Builder().url(URL.stripFragment(mUrl));
resumeSize = file.getSize();
Log.d(Config.LOGTAG, "http download trying resume after " + resumeSize + " of " + expected); final long expected = file.getExpectedSize();
connection.setRequestProperty("Range", "bytes=" + resumeSize + "-"); final boolean tryResume = file.exists() && file.getSize() > 0 && file.getSize() < expected;
} final long resumeSize;
connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000); if (tryResume) {
connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000); resumeSize = file.getSize();
connection.connect(); Log.d(Config.LOGTAG, "http download trying resume after " + resumeSize + " of " + expected);
is = new BufferedInputStream(connection.getInputStream()); requestBuilder.addHeader("Range", String.format(Locale.ENGLISH, "bytes=%d-", resumeSize));
final String contentRange = connection.getHeaderField("Content-Range"); } else {
boolean serverResumed = tryResume && contentRange != null && contentRange.startsWith("bytes " + resumeSize + "-"); resumeSize = 0;
}
final Request request = requestBuilder.build();
mostRecentCall = client.newCall(request);
final Response response = mostRecentCall.execute();
final int code = response.code();
if (code >= 200 && code <= 299) {
final String contentRange = response.header("Content-Range");
final boolean serverResumed = tryResume && contentRange != null && contentRange.startsWith("bytes " + resumeSize + "-");
final InputStream inputStream = response.body().byteStream();
final OutputStream outputStream;
long transmitted = 0; long transmitted = 0;
if (tryResume && serverResumed) { if (tryResume && serverResumed) {
Log.d(Config.LOGTAG, "server resumed"); Log.d(Config.LOGTAG, "server resumed");
transmitted = file.getSize(); transmitted = file.getSize();
updateProgress(Math.round(((double) transmitted / expected) * 100)); updateProgress(Math.round(((double) transmitted / expected) * 100));
os = AbstractConnectionManager.createOutputStream(file, true, false); outputStream = AbstractConnectionManager.createOutputStream(file, true, false);
if (os == null) {
throw new FileWriterException();
}
} else { } else {
long reportedContentLengthOnGet; final String contentLength = response.header("Content-Length");
try { final long size = Strings.isNullOrEmpty(contentLength) ? 0 : Longs.tryParse(contentLength);
reportedContentLengthOnGet = Long.parseLong(connection.getHeaderField("Content-Length")); if (expected != size) {
} catch (NumberFormatException | NullPointerException e) { Log.d(Config.LOGTAG, "content-length reported on GET (" + size + ") did not match Content-Length reported on HEAD (" + expected + ")");
reportedContentLengthOnGet = 0;
}
if (expected != reportedContentLengthOnGet) {
Log.d(Config.LOGTAG, "content-length reported on GET (" + reportedContentLengthOnGet + ") did not match Content-Length reported on HEAD (" + expected + ")");
} }
file.getParentFile().mkdirs(); file.getParentFile().mkdirs();
if (!file.exists() && !file.createNewFile()) { if (!file.exists() && !file.createNewFile()) {
throw new FileWriterException(); throw new FileWriterException();
} }
os = AbstractConnectionManager.createOutputStream(file, false, false); outputStream = AbstractConnectionManager.createOutputStream(file, false, false);
} }
int count; int count;
byte[] buffer = new byte[4096]; final byte[] buffer = new byte[4096];
while ((count = is.read(buffer)) != -1) { while ((count = inputStream.read(buffer)) != -1) {
transmitted += count; transmitted += count;
try { try {
os.write(buffer, 0, count); outputStream.write(buffer, 0, count);
} catch (IOException e) { } catch (IOException e) {
throw new FileWriterException(); throw new FileWriterException();
} }
updateProgress(Math.round(((double) transmitted / expected) * 100)); updateProgress(Math.round(((double) transmitted / expected) * 100));
if (canceled) {
throw new CancellationException();
}
} }
try { outputStream.flush();
os.flush(); } else {
} catch (IOException e) { throw new IOException(String.format(Locale.ENGLISH, "HTTP Status code was %d", code));
throw new FileWriterException();
}
} catch (CancellationException | IOException e) {
Log.d(Config.LOGTAG, message.getConversation().getAccount().getJid().asBareJid() + ": http download failed", e);
throw e;
} finally {
FileBackend.close(os);
FileBackend.close(is);
if (connection != null) {
connection.disconnect();
}
WakeLockHelper.release(wakeLock);
} }
} }
private void updateImageBounds() { private void updateImageBounds() {
final boolean privateMessage = message.isPrivateMessage(); final boolean privateMessage = message.isPrivateMessage();
message.setType(privateMessage ? Message.TYPE_PRIVATE_FILE : Message.TYPE_FILE); message.setType(privateMessage ? Message.TYPE_PRIVATE_FILE : Message.TYPE_FILE);
final URL url; final String url;
final String ref = mUrl.getRef(); final String ref = mUrl.fragment();
if (method == Method.P1_S3) { if (ref != null && AesGcmURL.IV_KEY.matcher(ref).matches()) {
url = message.getFileParams().url; url = AesGcmURL.toAesGcmUrl(mUrl);
} else if (ref != null && AesGcmURLStreamHandler.IV_KEY.matcher(ref).matches()) {
url = CryptoHelper.toAesGcmUrl(mUrl);
} else { } else {
url = mUrl; url = mUrl.toString();
} }
mXmppConnectionService.getFileBackend().updateFileParams(message, url); mXmppConnectionService.getFileBackend().updateFileParams(message, url);
mXmppConnectionService.updateMessage(message); mXmppConnectionService.updateMessage(message);

View File

@ -1,250 +1,208 @@
package eu.siacs.conversations.http; package eu.siacs.conversations.http;
import android.os.PowerManager;
import android.util.Log; import android.util.Log;
import java.io.FileInputStream; import com.google.common.util.concurrent.FutureCallback;
import java.io.InputStream; import com.google.common.util.concurrent.Futures;
import java.io.OutputStream; import com.google.common.util.concurrent.ListenableFuture;
import java.net.HttpURLConnection; import com.google.common.util.concurrent.MoreExecutors;
import java.net.URL;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Scanner;
import javax.net.ssl.HttpsURLConnection; import org.checkerframework.checker.nullness.compatqual.NullableDecl;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Future;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.AbstractConnectionManager; import eu.siacs.conversations.services.AbstractConnectionManager;
import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.Checksum;
import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.WakeLockHelper; import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import static eu.siacs.conversations.http.HttpConnectionManager.EXECUTOR; public class HttpUploadConnection implements Transferable, AbstractConnectionManager.ProgressListener {
public class HttpUploadConnection implements Transferable { static final List<String> WHITE_LISTED_HEADERS = Arrays.asList(
"Authorization",
"Cookie",
"Expires"
);
static final List<String> WHITE_LISTED_HEADERS = Arrays.asList( private final HttpConnectionManager mHttpConnectionManager;
"Authorization", private final XmppConnectionService mXmppConnectionService;
"Cookie", private final Method method;
"Expires" private boolean delayed = false;
); private DownloadableFile file;
private final Message message;
private String mime;
private SlotRequester.Slot slot;
private byte[] key = null;
private final HttpConnectionManager mHttpConnectionManager; private long transmitted = 0;
private final XmppConnectionService mXmppConnectionService; private Call mostRecentCall;
private final SlotRequester mSlotRequester; private ListenableFuture<SlotRequester.Slot> slotFuture;
private final Method method;
private final boolean mUseTor;
private boolean cancelled = false;
private boolean delayed = false;
private DownloadableFile file;
private final Message message;
private String mime;
private SlotRequester.Slot slot;
private byte[] key = null;
private long transmitted = 0; public HttpUploadConnection(Message message, Method method, HttpConnectionManager httpConnectionManager) {
this.message = message;
this.method = method;
this.mHttpConnectionManager = httpConnectionManager;
this.mXmppConnectionService = httpConnectionManager.getXmppConnectionService();
}
public HttpUploadConnection(Message message, Method method, HttpConnectionManager httpConnectionManager) { @Override
this.message = message; public boolean start() {
this.method = method; return false;
this.mHttpConnectionManager = httpConnectionManager; }
this.mXmppConnectionService = httpConnectionManager.getXmppConnectionService();
this.mSlotRequester = new SlotRequester(this.mXmppConnectionService);
this.mUseTor = mXmppConnectionService.useTorToConnect();
}
@Override @Override
public boolean start() { public int getStatus() {
return false; return STATUS_UPLOADING;
} }
@Override @Override
public int getStatus() { public long getFileSize() {
return STATUS_UPLOADING; return file == null ? 0 : file.getExpectedSize();
} }
@Override @Override
public long getFileSize() { public int getProgress() {
return file == null ? 0 : file.getExpectedSize(); if (file == null) {
} return 0;
}
return (int) ((((double) transmitted) / file.getExpectedSize()) * 100);
}
@Override @Override
public int getProgress() { public void cancel() {
if (file == null) { final ListenableFuture<SlotRequester.Slot> slotFuture = this.slotFuture;
return 0; if (slotFuture != null && !slotFuture.isDone()) {
} slotFuture.cancel(true);
return (int) ((((double) transmitted) / file.getExpectedSize()) * 100); }
} final Call call = this.mostRecentCall;
if (call != null && !call.isCanceled()) {
call.cancel();
}
}
@Override private void fail(String errorMessage) {
public void cancel() { finish();
this.cancelled = true; final Call call = this.mostRecentCall;
} final Future<SlotRequester.Slot> slotFuture = this.slotFuture;
final boolean cancelled = (call != null && call.isCanceled()) || (slotFuture != null && slotFuture.isCancelled());
mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED, cancelled ? Message.ERROR_MESSAGE_CANCELLED : errorMessage);
}
private void fail(String errorMessage) { private void markAsCancelled() {
finish(); finish();
mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED, cancelled ? Message.ERROR_MESSAGE_CANCELLED : errorMessage); mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED, Message.ERROR_MESSAGE_CANCELLED);
} }
private void finish() { private void finish() {
mHttpConnectionManager.finishUploadConnection(this); mHttpConnectionManager.finishUploadConnection(this);
message.setTransferable(null); message.setTransferable(null);
} }
public void init(boolean delay) { public void init(boolean delay) {
final Account account = message.getConversation().getAccount(); final Account account = message.getConversation().getAccount();
this.file = mXmppConnectionService.getFileBackend().getFile(message, false); this.file = mXmppConnectionService.getFileBackend().getFile(message, false);
if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
this.mime = "application/pgp-encrypted"; this.mime = "application/pgp-encrypted";
} else { } else {
this.mime = this.file.getMimeType(); this.mime = this.file.getMimeType();
} }
final long originalFileSize = file.getSize(); final long originalFileSize = file.getSize();
this.delayed = delay; this.delayed = delay;
if (Config.ENCRYPT_ON_HTTP_UPLOADED if (Config.ENCRYPT_ON_HTTP_UPLOADED
|| message.getEncryption() == Message.ENCRYPTION_AXOLOTL || message.getEncryption() == Message.ENCRYPTION_AXOLOTL
|| message.getEncryption() == Message.ENCRYPTION_OTR) { || message.getEncryption() == Message.ENCRYPTION_OTR) {
this.key = new byte[44]; this.key = new byte[44];
mXmppConnectionService.getRNG().nextBytes(this.key); mXmppConnectionService.getRNG().nextBytes(this.key);
this.file.setKeyAndIv(this.key); this.file.setKeyAndIv(this.key);
} }
this.file.setExpectedSize(originalFileSize + (file.getKey() != null ? 16 : 0));
message.resetFileParams();
this.slotFuture = new SlotRequester(mXmppConnectionService).request(method, account, file, mime);
Futures.addCallback(this.slotFuture, new FutureCallback<SlotRequester.Slot>() {
@Override
public void onSuccess(@NullableDecl SlotRequester.Slot result) {
HttpUploadConnection.this.slot = result;
HttpUploadConnection.this.upload();
}
final String md5; @Override
public void onFailure(@NotNull final Throwable throwable) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to request slot", throwable);
fail(throwable.getMessage());
}
}, MoreExecutors.directExecutor());
message.setTransferable(this);
mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND);
}
if (method == Method.P1_S3) { private void upload() {
try { final OkHttpClient client = mHttpConnectionManager.buildHttpClient(
md5 = Checksum.md5(AbstractConnectionManager.upgrade(file, new FileInputStream(file))); slot.put,
} catch (Exception e) { message.getConversation().getAccount(),
Log.d(Config.LOGTAG, account.getJid().asBareJid()+": unable to calculate md5()", e); true
fail(e.getMessage()); );
return; final RequestBody requestBody = AbstractConnectionManager.requestBody(file, this);
} final Request request = new Request.Builder()
} else { .url(slot.put)
md5 = null; .put(requestBody)
} .headers(slot.headers)
.build();
Log.d(Config.LOGTAG, "uploading file to " + slot.put);
this.mostRecentCall = client.newCall(request);
this.mostRecentCall.enqueue(new Callback() {
@Override
public void onFailure(@NotNull Call call, IOException e) {
Log.d(Config.LOGTAG, "http upload failed", e);
fail(e.getMessage());
}
this.file.setExpectedSize(originalFileSize + (file.getKey() != null ? 16 : 0)); @Override
message.resetFileParams(); public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
this.mSlotRequester.request(method, account, file, mime, md5, new SlotRequester.OnSlotRequested() { final int code = response.code();
@Override if (code == 200 || code == 201) {
public void success(SlotRequester.Slot slot) { Log.d(Config.LOGTAG, "finished uploading file");
if (!cancelled) { final String get;
HttpUploadConnection.this.slot = slot; if (key != null) {
EXECUTOR.execute(HttpUploadConnection.this::upload); get = AesGcmURL.toAesGcmUrl(slot.get.newBuilder().fragment(CryptoHelper.bytesToHex(key)).build());
} } else {
} get = slot.get.toString();
}
mXmppConnectionService.getFileBackend().updateFileParams(message, get);
mXmppConnectionService.getFileBackend().updateMediaScanner(file);
finish();
if (!message.isPrivateMessage()) {
message.setCounterpart(message.getConversation().getJid().asBareJid());
}
mXmppConnectionService.resendMessage(message, delayed);
} else {
Log.d(Config.LOGTAG, "http upload failed because response code was " + code);
fail("http upload failed because response code was " + code);
}
}
});
}
@Override public Message getMessage() {
public void failure(String message) { return message;
fail(message); }
}
});
message.setTransferable(this);
mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND);
}
private void upload() { @Override
OutputStream os = null; public void onProgress(final long progress) {
InputStream fileInputStream = null; this.transmitted = progress;
HttpURLConnection connection = null; mHttpConnectionManager.updateConversationUi(false);
final PowerManager.WakeLock wakeLock = mHttpConnectionManager.createWakeLock(Thread.currentThread()); }
try {
fileInputStream = new FileInputStream(file);
final String slotHostname = slot.getPutUrl().getHost();
final boolean onionSlot = slotHostname != null && slotHostname.endsWith(".onion");
final int expectedFileSize = (int) file.getExpectedSize();
final int readTimeout = (expectedFileSize / 2048) + Config.SOCKET_TIMEOUT; //assuming a minimum transfer speed of 16kbit/s
wakeLock.acquire(readTimeout);
Log.d(Config.LOGTAG, "uploading to " + slot.getPutUrl().toString()+ " w/ read timeout of "+readTimeout+"s");
if (mUseTor || message.getConversation().getAccount().isOnion() || onionSlot) {
connection = (HttpURLConnection) slot.getPutUrl().openConnection(HttpConnectionManager.getProxy());
} else {
connection = (HttpURLConnection) slot.getPutUrl().openConnection();
}
if (connection instanceof HttpsURLConnection) {
mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, true);
}
connection.setUseCaches(false);
connection.setRequestMethod("PUT");
connection.setFixedLengthStreamingMode(expectedFileSize);
connection.setRequestProperty("User-Agent",mXmppConnectionService.getIqGenerator().getUserAgent());
if(slot.getHeaders() != null) {
for(HashMap.Entry<String,String> entry : slot.getHeaders().entrySet()) {
connection.setRequestProperty(entry.getKey(),entry.getValue());
}
}
connection.setDoOutput(true);
connection.setDoInput(true);
connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
connection.setReadTimeout(readTimeout * 1000);
connection.connect();
final InputStream innerInputStream = AbstractConnectionManager.upgrade(file, fileInputStream);
os = connection.getOutputStream();
transmitted = 0;
int count;
byte[] buffer = new byte[4096];
while (((count = innerInputStream.read(buffer)) != -1) && !cancelled) {
transmitted += count;
os.write(buffer, 0, count);
mHttpConnectionManager.updateConversationUi(false);
}
os.flush();
os.close();
int code = connection.getResponseCode();
InputStream is = connection.getErrorStream();
if (is != null) {
try (Scanner scanner = new Scanner(is)) {
scanner.useDelimiter("\\Z");
Log.d(Config.LOGTAG, "body: " + scanner.next());
}
}
if (code == 200 || code == 201) {
Log.d(Config.LOGTAG, "finished uploading file");
final URL get;
if (key != null) {
if (method == Method.P1_S3) {
get = new URL(slot.getGetUrl().toString()+"#"+CryptoHelper.bytesToHex(key));
} else {
get = CryptoHelper.toAesGcmUrl(new URL(slot.getGetUrl().toString() + "#" + CryptoHelper.bytesToHex(key)));
}
} else {
get = slot.getGetUrl();
}
mXmppConnectionService.getFileBackend().updateFileParams(message, get);
mXmppConnectionService.getFileBackend().updateMediaScanner(file);
finish();
if (!message.isPrivateMessage()) {
message.setCounterpart(message.getConversation().getJid().asBareJid());
}
mXmppConnectionService.resendMessage(message, delayed);
} else {
Log.d(Config.LOGTAG,"http upload failed because response code was "+code);
fail("http upload failed because response code was "+code);
}
} catch (Exception e) {
e.printStackTrace();
Log.d(Config.LOGTAG,"http upload failed "+e.getMessage());
fail(e.getMessage());
} finally {
FileBackend.close(fileInputStream);
FileBackend.close(os);
if (connection != null) {
connection.disconnect();
}
WakeLockHelper.release(wakeLock);
}
}
public Message getMessage() {
return message;
}
} }

View File

@ -33,7 +33,7 @@ import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.XmppConnection;
public enum Method { public enum Method {
P1_S3, HTTP_UPLOAD, HTTP_UPLOAD_LEGACY; HTTP_UPLOAD, HTTP_UPLOAD_LEGACY;
public static Method determine(Account account) { public static Method determine(Account account) {
XmppConnection.Features features = account.getXmppConnection() == null ? null : account.getXmppConnection().getFeatures(); XmppConnection.Features features = account.getXmppConnection() == null ? null : account.getXmppConnection().getFeatures();
@ -44,8 +44,6 @@ public enum Method {
return HTTP_UPLOAD_LEGACY; return HTTP_UPLOAD_LEGACY;
} else if (features.httpUpload(0)) { } else if (features.httpUpload(0)) {
return HTTP_UPLOAD; return HTTP_UPLOAD;
} else if (features.p1S3FileTransfer()) {
return P1_S3;
} else { } else {
return HTTP_UPLOAD; return HTTP_UPLOAD;
} }

View File

@ -1,62 +0,0 @@
/*
* Copyright (c) 2018, Daniel Gultsch All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation and/or
* other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package eu.siacs.conversations.http;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import eu.siacs.conversations.xml.Element;
public class P1S3UrlStreamHandler extends URLStreamHandler {
public static final String PROTOCOL_NAME = "p1s3";
@Override
protected URLConnection openConnection(URL url) {
throw new IllegalStateException("Unable to open connection with stub protocol");
}
public static URL of(String fileId, String filename) throws MalformedURLException {
if (fileId == null || filename == null) {
throw new MalformedURLException("Paramaters must not be null");
}
return new URL(PROTOCOL_NAME+"://" + fileId + "/" + filename);
}
public static URL of(Element x) {
try {
return of(x.getAttribute("fileid"),x.getAttribute("name"));
} catch (MalformedURLException e) {
return null;
}
}
}

View File

@ -29,162 +29,126 @@
package eu.siacs.conversations.http; package eu.siacs.conversations.http;
import android.util.Log; import com.google.common.collect.ImmutableMap;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import java.net.MalformedURLException; import java.util.Map;
import java.net.URL;
import java.util.HashMap;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.parser.IqParser; import eu.siacs.conversations.parser.IqParser;
import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.IqResponseException;
import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.stanzas.IqPacket; import eu.siacs.conversations.xmpp.stanzas.IqPacket;
import okhttp3.Headers;
import okhttp3.HttpUrl;
public class SlotRequester { public class SlotRequester {
private final XmppConnectionService service; private final XmppConnectionService service;
public SlotRequester(XmppConnectionService service) { public SlotRequester(XmppConnectionService service) {
this.service = service; this.service = service;
} }
public void request(Method method, Account account, DownloadableFile file, String mime, String md5, OnSlotRequested callback) { public ListenableFuture<Slot> request(Method method, Account account, DownloadableFile file, String mime) {
if (method == Method.HTTP_UPLOAD) { if (method == Method.HTTP_UPLOAD_LEGACY) {
Jid host = account.getXmppConnection().findDiscoItemByFeature(Namespace.HTTP_UPLOAD); final Jid host = account.getXmppConnection().findDiscoItemByFeature(Namespace.HTTP_UPLOAD_LEGACY);
requestHttpUpload(account, host, file, mime, callback); return requestHttpUploadLegacy(account, host, file, mime);
} else if (method == Method.HTTP_UPLOAD_LEGACY) { } else {
Jid host = account.getXmppConnection().findDiscoItemByFeature(Namespace.HTTP_UPLOAD_LEGACY); final Jid host = account.getXmppConnection().findDiscoItemByFeature(Namespace.HTTP_UPLOAD);
requestHttpUploadLegacy(account, host, file, mime, callback); return requestHttpUpload(account, host, file, mime);
} else { }
requestP1S3(account, account.getDomain(), file.getName(), md5, callback); }
}
}
private void requestHttpUploadLegacy(Account account, Jid host, DownloadableFile file, String mime, OnSlotRequested callback) { private ListenableFuture<Slot> requestHttpUploadLegacy(Account account, Jid host, DownloadableFile file, String mime) {
IqPacket request = service.getIqGenerator().requestHttpUploadLegacySlot(host, file, mime); final SettableFuture<Slot> future = SettableFuture.create();
service.sendIqPacket(account, request, (a, packet) -> { final IqPacket request = service.getIqGenerator().requestHttpUploadLegacySlot(host, file, mime);
if (packet.getType() == IqPacket.TYPE.RESULT) { service.sendIqPacket(account, request, (a, packet) -> {
Element slotElement = packet.findChild("slot", Namespace.HTTP_UPLOAD_LEGACY); if (packet.getType() == IqPacket.TYPE.RESULT) {
if (slotElement != null) { final Element slotElement = packet.findChild("slot", Namespace.HTTP_UPLOAD_LEGACY);
try { if (slotElement != null) {
final String putUrl = slotElement.findChildContent("put"); try {
final String getUrl = slotElement.findChildContent("get"); final String putUrl = slotElement.findChildContent("put");
if (getUrl != null && putUrl != null) { final String getUrl = slotElement.findChildContent("get");
Slot slot = new Slot(new URL(putUrl)); if (getUrl != null && putUrl != null) {
slot.getUrl = new URL(getUrl); final Slot slot = new Slot(
slot.headers = new HashMap<>(); HttpUrl.get(putUrl),
slot.headers.put("Content-Type", mime == null ? "application/octet-stream" : mime); HttpUrl.get(getUrl),
callback.success(slot); Headers.of("Content-Type", mime == null ? "application/octet-stream" : mime)
return; );
} future.set(slot);
} catch (MalformedURLException e) { return;
//fall through }
} } catch (final IllegalArgumentException e) {
} future.setException(e);
} return;
Log.d(Config.LOGTAG, account.getJid().toString() + ": invalid response to slot request " + packet); }
callback.failure(IqParser.extractErrorMessage(packet)); }
}); }
future.setException(new IqResponseException(IqParser.extractErrorMessage(packet)));
});
return future;
}
} private ListenableFuture<Slot> requestHttpUpload(Account account, Jid host, DownloadableFile file, String mime) {
final SettableFuture<Slot> future = SettableFuture.create();
final IqPacket request = service.getIqGenerator().requestHttpUploadSlot(host, file, mime);
service.sendIqPacket(account, request, (a, packet) -> {
if (packet.getType() == IqPacket.TYPE.RESULT) {
final Element slotElement = packet.findChild("slot", Namespace.HTTP_UPLOAD);
if (slotElement != null) {
try {
final Element put = slotElement.findChild("put");
final Element get = slotElement.findChild("get");
final String putUrl = put == null ? null : put.getAttribute("url");
final String getUrl = get == null ? null : get.getAttribute("url");
if (getUrl != null && putUrl != null) {
final ImmutableMap.Builder<String, String> headers = new ImmutableMap.Builder<>();
for (final Element child : put.getChildren()) {
if ("header".equals(child.getName())) {
final String name = child.getAttribute("name");
final String value = child.getContent();
if (HttpUploadConnection.WHITE_LISTED_HEADERS.contains(name) && value != null && !value.trim().contains("\n")) {
headers.put(name, value.trim());
}
}
}
headers.put("Content-Type", mime == null ? "application/octet-stream" : mime);
final Slot slot = new Slot(HttpUrl.get(putUrl), HttpUrl.get(getUrl), headers.build());
future.set(slot);
return;
}
} catch (final IllegalArgumentException e) {
future.setException(e);
return;
}
}
}
future.setException(new IqResponseException(IqParser.extractErrorMessage(packet)));
});
return future;
}
private void requestHttpUpload(Account account, Jid host, DownloadableFile file, String mime, OnSlotRequested callback) { public static class Slot {
IqPacket request = service.getIqGenerator().requestHttpUploadSlot(host, file, mime); public final HttpUrl put;
service.sendIqPacket(account, request, (a, packet) -> { public final HttpUrl get;
if (packet.getType() == IqPacket.TYPE.RESULT) { public final Headers headers;
Element slotElement = packet.findChild("slot", Namespace.HTTP_UPLOAD);
if (slotElement != null) {
try {
final Element put = slotElement.findChild("put");
final Element get = slotElement.findChild("get");
final String putUrl = put == null ? null : put.getAttribute("url");
final String getUrl = get == null ? null : get.getAttribute("url");
if (getUrl != null && putUrl != null) {
Slot slot = new Slot(new URL(putUrl));
slot.getUrl = new URL(getUrl);
slot.headers = new HashMap<>();
for (Element child : put.getChildren()) {
if ("header".equals(child.getName())) {
final String name = child.getAttribute("name");
final String value = child.getContent();
if (HttpUploadConnection.WHITE_LISTED_HEADERS.contains(name) && value != null && !value.trim().contains("\n")) {
slot.headers.put(name, value.trim());
}
}
}
slot.headers.put("Content-Type", mime == null ? "application/octet-stream" : mime);
callback.success(slot);
return;
}
} catch (MalformedURLException e) {
//fall through
}
}
}
Log.d(Config.LOGTAG, account.getJid().toString() + ": invalid response to slot request " + packet);
callback.failure(IqParser.extractErrorMessage(packet));
});
} private Slot(HttpUrl put, HttpUrl get, Headers headers) {
this.put = put;
this.get = get;
this.headers = headers;
}
private void requestP1S3(final Account account, Jid host, String filename, String md5, OnSlotRequested callback) { private Slot(HttpUrl put, HttpUrl getUrl, Map<String, String> headers) {
IqPacket request = service.getIqGenerator().requestP1S3Slot(host, md5); this.put = put;
service.sendIqPacket(account, request, (a, packet) -> { this.get = getUrl;
if (packet.getType() == IqPacket.TYPE.RESULT) { this.headers = Headers.of(headers);
String putUrl = packet.query(Namespace.P1_S3_FILE_TRANSFER).getAttribute("upload"); }
String id = packet.query().getAttribute("fileid"); }
try {
if (putUrl != null && id != null) {
Slot slot = new Slot(new URL(putUrl));
slot.getUrl = P1S3UrlStreamHandler.of(id, filename);
slot.headers = new HashMap<>();
slot.headers.put("Content-MD5", md5);
slot.headers.put("Content-Type", " "); //required to force it to empty. otherwise library will set something
callback.success(slot);
return;
}
} catch (MalformedURLException e) {
//fall through;
}
}
callback.failure("unable to request slot");
});
Log.d(Config.LOGTAG, "requesting slot with p1. md5=" + md5);
}
public interface OnSlotRequested {
void success(Slot slot);
void failure(String message);
}
public static class Slot {
private final URL putUrl;
private URL getUrl;
private HashMap<String, String> headers;
private Slot(URL putUrl) {
this.putUrl = putUrl;
}
public URL getPutUrl() {
return putUrl;
}
public URL getGetUrl() {
return getUrl;
}
public HashMap<String, String> getHeaders() {
return headers;
}
}
} }

View File

@ -0,0 +1,32 @@
package eu.siacs.conversations.http;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.List;
import okhttp3.HttpUrl;
public class URL {
public static final List<String> WELL_KNOWN_SCHEMES = Arrays.asList("http", "https", AesGcmURL.PROTOCOL_NAME);
public static String tryParse(String url) {
final URI uri;
try {
uri = new URI(url);
} catch (URISyntaxException e) {
return null;
}
if (WELL_KNOWN_SCHEMES.contains(uri.getScheme())) {
return uri.toString();
} else {
return null;
}
}
public static HttpUrl stripFragment(final HttpUrl url) {
return url.newBuilder().fragment(null).build();
}
}

View File

@ -3,7 +3,6 @@ package eu.siacs.conversations.parser;
import android.util.Log; import android.util.Log;
import android.util.Pair; import android.util.Pair;
import java.net.URL;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -33,7 +32,6 @@ import eu.siacs.conversations.entities.ReadByMarker;
import eu.siacs.conversations.entities.ReceiptRequest; import eu.siacs.conversations.entities.ReceiptRequest;
import eu.siacs.conversations.entities.RtpSessionStatus; import eu.siacs.conversations.entities.RtpSessionStatus;
import eu.siacs.conversations.http.HttpConnectionManager; import eu.siacs.conversations.http.HttpConnectionManager;
import eu.siacs.conversations.http.P1S3UrlStreamHandler;
import eu.siacs.conversations.services.MessageArchiveService; import eu.siacs.conversations.services.MessageArchiveService;
import eu.siacs.conversations.services.QuickConversationsService; import eu.siacs.conversations.services.QuickConversationsService;
import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService;
@ -408,8 +406,6 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
final String pgpEncrypted = packet.findChildContent("x", "jabber:x:encrypted"); final String pgpEncrypted = packet.findChildContent("x", "jabber:x:encrypted");
final Element replaceElement = packet.findChild("replace", "urn:xmpp:message-correct:0"); final Element replaceElement = packet.findChild("replace", "urn:xmpp:message-correct:0");
final Element oob = packet.findChild("x", Namespace.OOB); final Element oob = packet.findChild("x", Namespace.OOB);
final Element xP1S3 = packet.findChild("x", Namespace.P1_S3_FILE_TRANSFER);
final URL xP1S3url = xP1S3 == null ? null : P1S3UrlStreamHandler.of(xP1S3);
final String oobUrl = oob != null ? oob.findChildContent("url") : null; final String oobUrl = oob != null ? oob.findChildContent("url") : null;
final String replacementId = replaceElement == null ? null : replaceElement.getAttribute("id"); final String replacementId = replaceElement == null ? null : replaceElement.getAttribute("id");
final Element axolotlEncrypted = packet.findChildEnsureSingle(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX); final Element axolotlEncrypted = packet.findChildEnsureSingle(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX);
@ -464,7 +460,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
} }
} }
if ((body != null || pgpEncrypted != null || (axolotlEncrypted != null && axolotlEncrypted.hasChild("payload")) || oobUrl != null || xP1S3 != null) && !isMucStatusMessage) { if ((body != null || pgpEncrypted != null || (axolotlEncrypted != null && axolotlEncrypted.hasChild("payload")) || oobUrl != null) && !isMucStatusMessage) {
final boolean conversationIsProbablyMuc = isTypeGroupChat || mucUserElement != null || account.getXmppConnection().getMucServersWithholdAccount().contains(counterpart.getDomain().toEscapedString()); 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 Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, counterpart.asBareJid(), conversationIsProbablyMuc, false, query, false);
final boolean conversationMultiMode = conversation.getMode() == Conversation.MODE_MULTI; final boolean conversationMultiMode = conversation.getMode() == Conversation.MODE_MULTI;
@ -504,13 +500,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
} }
} }
final Message message; final Message message;
if (xP1S3url != null) { if (pgpEncrypted != null && Config.supportOpenPgp()) {
message = new Message(conversation, xP1S3url.toString(), Message.ENCRYPTION_NONE, status);
message.setOob(true);
if (CryptoHelper.isPgpEncryptedUrl(xP1S3url.toString())) {
message.setEncryption(Message.ENCRYPTION_DECRYPTED);
}
} else if (pgpEncrypted != null && Config.supportOpenPgp()) {
message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status); message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status);
} else if (axolotlEncrypted != null && Config.supportOmemo()) { } else if (axolotlEncrypted != null && Config.supportOmemo()) {
Jid origin; Jid origin;

View File

@ -19,7 +19,6 @@ import eu.siacs.conversations.entities.Presence;
import eu.siacs.conversations.generator.IqGenerator; import eu.siacs.conversations.generator.IqGenerator;
import eu.siacs.conversations.generator.PresenceGenerator; import eu.siacs.conversations.generator.PresenceGenerator;
import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.XmppUri; import eu.siacs.conversations.utils.XmppUri;
import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xml.Namespace;

View File

@ -18,7 +18,6 @@ import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Environment; import android.os.Environment;
import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor;
import android.os.SystemClock;
import android.provider.MediaStore; import android.provider.MediaStore;
import android.provider.OpenableColumns; import android.provider.OpenableColumns;
import android.system.Os; import android.system.Os;
@ -30,6 +29,7 @@ import android.util.Log;
import android.util.LruCache; import android.util.LruCache;
import androidx.annotation.RequiresApi; import androidx.annotation.RequiresApi;
import androidx.annotation.StringRes;
import androidx.core.content.FileProvider; import androidx.core.content.FileProvider;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
@ -44,7 +44,6 @@ import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.ServerSocket; import java.net.ServerSocket;
import java.net.Socket; import java.net.Socket;
import java.net.URL;
import java.security.DigestOutputStream; import java.security.DigestOutputStream;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
@ -416,9 +415,9 @@ public class FileBackend {
} }
} }
public static void updateFileParams(Message message, URL url, long size) { public static void updateFileParams(Message message, String url, long size) {
final StringBuilder body = new StringBuilder(); final StringBuilder body = new StringBuilder();
body.append(url.toString()).append('|').append(size); body.append(url).append('|').append(size);
message.setBody(body.toString()); message.setBody(body.toString());
} }
@ -648,12 +647,13 @@ public class FileBackend {
} catch (IOException e) { } catch (IOException e) {
throw new FileWriterException(); throw new FileWriterException();
} }
} catch (FileNotFoundException e) { } catch (final FileNotFoundException e) {
throw new FileCopyException(R.string.error_file_not_found); throw new FileCopyException(R.string.error_file_not_found);
} catch (FileWriterException e) { } catch (final FileWriterException e) {
throw new FileCopyException(R.string.error_unable_to_create_temporary_file); throw new FileCopyException(R.string.error_unable_to_create_temporary_file);
} catch (IOException e) { } catch (final SecurityException e) {
e.printStackTrace(); throw new FileCopyException(R.string.error_security_exception);
} catch (final IOException e) {
throw new FileCopyException(R.string.error_io_exception); throw new FileCopyException(R.string.error_io_exception);
} finally { } finally {
close(os); close(os);
@ -1305,7 +1305,7 @@ public class FileBackend {
updateFileParams(message, null); updateFileParams(message, null);
} }
public void updateFileParams(Message message, URL url) { public void updateFileParams(Message message, String url) {
DownloadableFile file = getFile(message); DownloadableFile file = getFile(message);
final String mime = file.getMimeType(); final String mime = file.getMimeType();
final boolean privateMessage = message.isPrivateMessage(); final boolean privateMessage = message.isPrivateMessage();
@ -1315,7 +1315,7 @@ public class FileBackend {
final boolean pdf = "application/pdf".equals(mime); final boolean pdf = "application/pdf".equals(mime);
final StringBuilder body = new StringBuilder(); final StringBuilder body = new StringBuilder();
if (url != null) { if (url != null) {
body.append(url.toString()); body.append(url);
} }
body.append('|').append(file.getSize()); body.append('|').append(file.getSize());
if (image || video || (pdf && Compatibility.runsTwentyOne())) { if (image || video || (pdf && Compatibility.runsTwentyOne())) {
@ -1464,11 +1464,11 @@ public class FileBackend {
public static class FileCopyException extends Exception { public static class FileCopyException extends Exception {
private final int resId; private final int resId;
private FileCopyException(int resId) { private FileCopyException(@StringRes int resId) {
this.resId = resId; this.resId = resId;
} }
public int getResId() { public @StringRes int getResId() {
return resId; return resId;
} }
} }

View File

@ -13,22 +13,25 @@ import org.bouncycastle.crypto.modes.GCMBlockCipher;
import org.bouncycastle.crypto.params.AEADParameters; import org.bouncycastle.crypto.params.AEADParameters;
import org.bouncycastle.crypto.params.KeyParameter; import org.bouncycastle.crypto.params.KeyParameter;
import java.io.FileInputStream;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import javax.crypto.NoSuchPaddingException; import javax.annotation.Nullable;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.R; import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.utils.Compatibility; import eu.siacs.conversations.utils.Compatibility;
import okhttp3.MediaType;
import okhttp3.RequestBody;
import okio.BufferedSink;
import okio.Okio;
import okio.Source;
import static eu.siacs.conversations.entities.Transferable.VALID_CRYPTO_EXTENSIONS; import static eu.siacs.conversations.entities.Transferable.VALID_CRYPTO_EXTENSIONS;
@ -42,7 +45,7 @@ public class AbstractConnectionManager {
this.mXmppConnectionService = service; this.mXmppConnectionService = service;
} }
public static InputStream upgrade(DownloadableFile file, InputStream is) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, InvalidKeyException, NoSuchPaddingException, NoSuchProviderException { public static InputStream upgrade(DownloadableFile file, InputStream is) {
if (file.getKey() != null && file.getIv() != null) { if (file.getKey() != null && file.getIv() != null) {
AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine()); AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine());
cipher.init(true, new AEADParameters(new KeyParameter(file.getKey()), 128, file.getIv())); cipher.init(true, new AEADParameters(new KeyParameter(file.getKey()), 128, file.getIv()));
@ -52,6 +55,43 @@ public class AbstractConnectionManager {
} }
} }
//For progress tracking see:
//https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/Progress.java
public static RequestBody requestBody(final DownloadableFile file, final ProgressListener progressListener) {
return new RequestBody() {
@Override
public long contentLength() {
return file.getSize() + (file.getKey() != null ? 16 : 0);
}
@Nullable
@Override
public MediaType contentType() {
return MediaType.parse(file.getMimeType());
}
@Override
public void writeTo(final BufferedSink sink) throws IOException {
long transmitted = 0;
try (final Source source = Okio.source(upgrade(file, new FileInputStream(file)))) {
long read;
while ((read = source.read(sink.buffer(), 8196)) != -1) {
transmitted += read;
sink.flush();
progressListener.onProgress(transmitted);
}
}
}
};
}
public interface ProgressListener {
void onProgress(long progress);
}
public static OutputStream createOutputStream(DownloadableFile file, boolean append, boolean decrypt) { public static OutputStream createOutputStream(DownloadableFile file, boolean append, boolean decrypt) {
FileOutputStream os; FileOutputStream os;
try { try {
@ -121,6 +161,7 @@ public class AbstractConnectionManager {
} }
public static Extension of(String path) { public static Extension of(String path) {
//TODO accept List<String> pathSegments
final int pos = path.lastIndexOf('/'); final int pos = path.lastIndexOf('/');
final String filename = path.substring(pos + 1).toLowerCase(); final String filename = path.substring(pos + 1).toLowerCase();
final String[] parts = filename.split("\\."); final String[] parts = filename.split("\\.");

View File

@ -3,13 +3,10 @@ package eu.siacs.conversations.services;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.util.Log; import android.util.Log;
import androidx.annotation.RequiresApi;
import net.ypresto.androidtranscoder.MediaTranscoder; import net.ypresto.androidtranscoder.MediaTranscoder;
import net.ypresto.androidtranscoder.format.MediaFormatStrategy; import net.ypresto.androidtranscoder.format.MediaFormatStrategy;

View File

@ -51,13 +51,8 @@ public class ChannelDiscoveryService {
void initializeMuclumbusService() { void initializeMuclumbusService() {
final OkHttpClient.Builder builder = new OkHttpClient.Builder(); final OkHttpClient.Builder builder = new OkHttpClient.Builder();
if (service.useTorToConnect()) { if (service.useTorToConnect()) {
try { builder.proxy(HttpConnectionManager.getProxy());
builder.proxy(HttpConnectionManager.getProxy());
} catch (IOException e) {
throw new RuntimeException("Unable to use Tor proxy", e);
}
} }
Retrofit retrofit = new Retrofit.Builder() Retrofit retrofit = new Retrofit.Builder()
.client(builder.build()) .client(builder.build())
@ -73,7 +68,7 @@ public class ChannelDiscoveryService {
} }
void discover(@NonNull final String query, Method method, OnChannelSearchResultsFound onChannelSearchResultsFound) { void discover(@NonNull final String query, Method method, OnChannelSearchResultsFound onChannelSearchResultsFound) {
List<Room> result = cache.getIfPresent(key(method, query)); final List<Room> result = cache.getIfPresent(key(method, query));
if (result != null) { if (result != null) {
onChannelSearchResultsFound.onChannelSearchResultsFound(result); onChannelSearchResultsFound.onChannelSearchResultsFound(result);
return; return;

View File

@ -31,6 +31,7 @@ import android.app.NotificationManager;
import android.app.Service; import android.app.Service;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri; import android.net.Uri;
import android.os.Handler; import android.os.Handler;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
@ -40,6 +41,9 @@ import android.util.SparseArray;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import com.google.common.base.Charsets;
import com.google.common.io.CharStreams;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
@ -52,7 +56,6 @@ import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.net.URL;
import java.security.KeyStore; import java.security.KeyStore;
import java.security.KeyStoreException; import java.security.KeyStoreException;
import java.security.MessageDigest; import java.security.MessageDigest;
@ -74,7 +77,6 @@ import java.util.logging.Logger;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.TrustManagerFactory;
@ -83,6 +85,7 @@ import javax.net.ssl.X509TrustManager;
import eu.siacs.conversations.R; import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.DomainHostnameVerifier; import eu.siacs.conversations.crypto.DomainHostnameVerifier;
import eu.siacs.conversations.entities.MTMDecision; import eu.siacs.conversations.entities.MTMDecision;
import eu.siacs.conversations.http.HttpConnectionManager;
import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.ui.MemorizingActivity; import eu.siacs.conversations.ui.MemorizingActivity;
@ -486,15 +489,18 @@ public class MemorizingTrustManager {
defaultTrustManager.checkServerTrusted(chain, authType); defaultTrustManager.checkServerTrusted(chain, authType);
else else
defaultTrustManager.checkClientTrusted(chain, authType); defaultTrustManager.checkClientTrusted(chain, authType);
} catch (CertificateException e) { } catch (final CertificateException e) {
boolean trustSystemCAs = !PreferenceManager.getDefaultSharedPreferences(master).getBoolean("dont_trust_system_cas", false); final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(master);
if (domain != null && isServer && trustSystemCAs && !isIp(domain)) { final boolean trustSystemCAs = !preferences.getBoolean("dont_trust_system_cas", false);
if (domain != null && isServer && trustSystemCAs && !isIp(domain) && !domain.endsWith(".onion")) {
final String hash = getBase64Hash(chain[0], "SHA-256"); final String hash = getBase64Hash(chain[0], "SHA-256");
final List<String> fingerprints = getPoshFingerprints(domain); final List<String> fingerprints = getPoshFingerprints(domain);
if (hash != null && fingerprints.size() > 0) { if (hash != null && fingerprints.size() > 0) {
if (fingerprints.contains(hash)) { if (fingerprints.contains(hash)) {
Log.d("mtm", "trusted cert fingerprint of " + domain + " via posh"); Log.d("mtm", "trusted cert fingerprint of " + domain + " via posh");
return; return;
} else {
Log.d("mtm", "fingerprint " + hash + " not found in " + fingerprints);
} }
if (getPoshCacheFile(domain).delete()) { if (getPoshCacheFile(domain).delete()) {
Log.d("mtm", "deleted posh file for " + domain + " after not being able to verify"); Log.d("mtm", "deleted posh file for " + domain + " after not being able to verify");
@ -511,7 +517,7 @@ public class MemorizingTrustManager {
} }
private List<String> getPoshFingerprints(String domain) { private List<String> getPoshFingerprints(String domain) {
List<String> cached = getPoshFingerprintsFromCache(domain); final List<String> cached = getPoshFingerprintsFromCache(domain);
if (cached == null) { if (cached == null) {
return getPoshFingerprintsFromServer(domain); return getPoshFingerprintsFromServer(domain);
} else { } else {
@ -525,19 +531,13 @@ public class MemorizingTrustManager {
private List<String> getPoshFingerprintsFromServer(String domain, String url, int maxTtl, boolean followUrl) { private List<String> getPoshFingerprintsFromServer(String domain, String url, int maxTtl, boolean followUrl) {
Log.d("mtm", "downloading json for " + domain + " from " + url); Log.d("mtm", "downloading json for " + domain + " from " + url);
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(master);
final boolean useTor = QuickConversationsService.isConversations() && preferences.getBoolean("use_tor", master.getResources().getBoolean(R.bool.use_tor));
try { try {
List<String> results = new ArrayList<>(); final List<String> results = new ArrayList<>();
HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection(); final InputStream inputStream = HttpConnectionManager.open(url, useTor);
connection.setConnectTimeout(5000); final String body = CharStreams.toString(new InputStreamReader(inputStream, Charsets.UTF_8));
connection.setReadTimeout(5000); final JSONObject jsonObject = new JSONObject(body);
BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String inputLine;
StringBuilder builder = new StringBuilder();
while ((inputLine = in.readLine()) != null) {
builder.append(inputLine);
}
JSONObject jsonObject = new JSONObject(builder.toString());
in.close();
int expires = jsonObject.getInt("expires"); int expires = jsonObject.getInt("expires");
if (expires <= 0) { if (expires <= 0) {
return new ArrayList<>(); return new ArrayList<>();
@ -554,17 +554,15 @@ public class MemorizingTrustManager {
if (followUrl && redirect != null && redirect.toLowerCase().startsWith("https")) { if (followUrl && redirect != null && redirect.toLowerCase().startsWith("https")) {
return getPoshFingerprintsFromServer(domain, redirect, expires, false); return getPoshFingerprintsFromServer(domain, redirect, expires, false);
} }
JSONArray fingerprints = jsonObject.getJSONArray("fingerprints"); final JSONArray fingerprints = jsonObject.getJSONArray("fingerprints");
for (int i = 0; i < fingerprints.length(); i++) { for (int i = 0; i < fingerprints.length(); i++) {
JSONObject fingerprint = fingerprints.getJSONObject(i); final JSONObject fingerprint = fingerprints.getJSONObject(i);
String sha256 = fingerprint.getString("sha-256"); final String sha256 = fingerprint.getString("sha-256");
if (sha256 != null) { results.add(sha256);
results.add(sha256);
}
} }
writeFingerprintsToCache(domain, results, 1000L * expires + System.currentTimeMillis()); writeFingerprintsToCache(domain, results, 1000L * expires + System.currentTimeMillis());
return results; return results;
} catch (Exception e) { } catch (final Exception e) {
Log.d("mtm", "error fetching posh " + e.getMessage()); Log.d("mtm", "error fetching posh " + e.getMessage());
return new ArrayList<>(); return new ArrayList<>();
} }
@ -575,7 +573,7 @@ public class MemorizingTrustManager {
} }
private void writeFingerprintsToCache(String domain, List<String> results, long expires) { private void writeFingerprintsToCache(String domain, List<String> results, long expires) {
File file = getPoshCacheFile(domain); final File file = getPoshCacheFile(domain);
file.getParentFile().mkdirs(); file.getParentFile().mkdirs();
try { try {
file.createNewFile(); file.createNewFile();
@ -592,20 +590,11 @@ public class MemorizingTrustManager {
} }
private List<String> getPoshFingerprintsFromCache(String domain) { private List<String> getPoshFingerprintsFromCache(String domain) {
File file = getPoshCacheFile(domain); final File file = getPoshCacheFile(domain);
try { try {
InputStream is = new FileInputStream(file); final InputStream inputStream = new FileInputStream(file);
BufferedReader buf = new BufferedReader(new InputStreamReader(is)); final String json = CharStreams.toString(new InputStreamReader(inputStream, Charsets.UTF_8));
final JSONObject jsonObject = new JSONObject(json);
String line = buf.readLine();
StringBuilder sb = new StringBuilder();
while (line != null) {
sb.append(line).append("\n");
line = buf.readLine();
}
JSONObject jsonObject = new JSONObject(sb.toString());
is.close();
long expires = jsonObject.getLong("expires"); long expires = jsonObject.getLong("expires");
long expiresIn = expires - System.currentTimeMillis(); long expiresIn = expires - System.currentTimeMillis();
if (expiresIn < 0) { if (expiresIn < 0) {
@ -614,15 +603,13 @@ public class MemorizingTrustManager {
} else { } else {
Log.d("mtm", "posh fingerprints expire in " + (expiresIn / 1000) + "s"); Log.d("mtm", "posh fingerprints expire in " + (expiresIn / 1000) + "s");
} }
List<String> result = new ArrayList<>(); final List<String> result = new ArrayList<>();
JSONArray jsonArray = jsonObject.getJSONArray("fingerprints"); final JSONArray jsonArray = jsonObject.getJSONArray("fingerprints");
for (int i = 0; i < jsonArray.length(); ++i) { for (int i = 0; i < jsonArray.length(); ++i) {
result.add(jsonArray.getString(i)); result.add(jsonArray.getString(i));
} }
return result; return result;
} catch (FileNotFoundException e) { } catch (final IOException e) {
return null;
} catch (IOException e) {
return null; return null;
} catch (JSONException e) { } catch (JSONException e) {
file.delete(); file.delete();

View File

@ -55,7 +55,6 @@ import org.openintents.openpgp.util.OpenPgpApi;
import org.openintents.openpgp.util.OpenPgpServiceConnection; import org.openintents.openpgp.util.OpenPgpServiceConnection;
import java.io.File; import java.io.File;
import java.net.URL;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.security.Security; import java.security.Security;
import java.security.cert.CertificateException; import java.security.cert.CertificateException;
@ -104,7 +103,6 @@ import eu.siacs.conversations.generator.AbstractGenerator;
import eu.siacs.conversations.generator.IqGenerator; import eu.siacs.conversations.generator.IqGenerator;
import eu.siacs.conversations.generator.MessageGenerator; import eu.siacs.conversations.generator.MessageGenerator;
import eu.siacs.conversations.generator.PresenceGenerator; import eu.siacs.conversations.generator.PresenceGenerator;
import eu.siacs.conversations.http.CustomURLStreamHandlerFactory;
import eu.siacs.conversations.http.HttpConnectionManager; import eu.siacs.conversations.http.HttpConnectionManager;
import eu.siacs.conversations.parser.AbstractParser; import eu.siacs.conversations.parser.AbstractParser;
import eu.siacs.conversations.parser.IqParser; import eu.siacs.conversations.parser.IqParser;
@ -183,10 +181,6 @@ public class XmppConnectionService extends Service {
private static final String SETTING_LAST_ACTIVITY_TS = "last_activity_timestamp"; private static final String SETTING_LAST_ACTIVITY_TS = "last_activity_timestamp";
static {
URL.setURLStreamHandlerFactory(new CustomURLStreamHandlerFactory());
}
public final CountDownLatch restoredFromDatabaseLatch = new CountDownLatch(1); public final CountDownLatch restoredFromDatabaseLatch = new CountDownLatch(1);
private final SerialSingleThreadExecutor mFileAddingExecutor = new SerialSingleThreadExecutor("FileAdding"); private final SerialSingleThreadExecutor mFileAddingExecutor = new SerialSingleThreadExecutor("FileAdding");
private final SerialSingleThreadExecutor mVideoCompressionExecutor = new SerialSingleThreadExecutor("VideoCompression"); private final SerialSingleThreadExecutor mVideoCompressionExecutor = new SerialSingleThreadExecutor("VideoCompression");
@ -663,6 +657,7 @@ public class XmppConnectionService extends Service {
if (Config.RESET_ATTEMPT_COUNT_ON_NETWORK_CHANGE) { if (Config.RESET_ATTEMPT_COUNT_ON_NETWORK_CHANGE) {
resetAllAttemptCounts(true, false); resetAllAttemptCounts(true, false);
} }
Resolver.clearCache();
} }
break; break;
case Intent.ACTION_SHUTDOWN: case Intent.ACTION_SHUTDOWN:
@ -999,7 +994,10 @@ public class XmppConnectionService extends Service {
public boolean isScreenLocked() { public boolean isScreenLocked() {
final KeyguardManager keyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE); final KeyguardManager keyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
return keyguardManager != null && keyguardManager.inKeyguardRestrictedInputMode(); final PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
final boolean locked = keyguardManager != null && keyguardManager.isKeyguardLocked();
final boolean interactive = powerManager != null && powerManager.isInteractive();
return locked || !interactive;
} }
private boolean isPhoneSilenced() { private boolean isPhoneSilenced() {
@ -1794,7 +1792,7 @@ public class XmppConnectionService extends Service {
IqPacket request = mIqGenerator.deleteItem(Namespace.BOOKMARKS2, bookmark.getJid().asBareJid().toEscapedString()); IqPacket request = mIqGenerator.deleteItem(Namespace.BOOKMARKS2, bookmark.getJid().asBareJid().toEscapedString());
sendIqPacket(account, request, (a, response) -> { sendIqPacket(account, request, (a, response) -> {
if (response.getType() == IqPacket.TYPE.ERROR) { if (response.getType() == IqPacket.TYPE.ERROR) {
Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": unable to delete bookmark " + response.getError()); Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": unable to delete bookmark " + response.getErrorCondition());
} }
}); });
} else if (connection.getFeatures().bookmarksConversion()) { } else if (connection.getFeatures().bookmarksConversion()) {
@ -2864,13 +2862,12 @@ public class XmppConnectionService extends Service {
} }
@Override @Override
public void onFetchFailed(final Conversation conversation, Element error) { public void onFetchFailed(final Conversation conversation, final String errorCondition) {
if (conversation.getStatus() == Conversation.STATUS_ARCHIVED) { if (conversation.getStatus() == Conversation.STATUS_ARCHIVED) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": conversation (" + conversation.getJid() + ") got archived before IQ result"); Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": conversation (" + conversation.getJid() + ") got archived before IQ result");
return; return;
} }
if (error != null && "remote-server-not-found".equals(error.getName())) { if ("remote-server-not-found".equals(errorCondition)) {
synchronized (account.inProgressConferenceJoins) { synchronized (account.inProgressConferenceJoins) {
account.inProgressConferenceJoins.remove(conversation); account.inProgressConferenceJoins.remove(conversation);
} }
@ -3236,7 +3233,7 @@ public class XmppConnectionService extends Service {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received timeout waiting for conference configuration fetch"); Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received timeout waiting for conference configuration fetch");
} else { } else {
if (callback != null) { if (callback != null) {
callback.onFetchFailed(conversation, packet.getError()); callback.onFetchFailed(conversation, packet.getErrorCondition());
} }
} }
} }
@ -3531,7 +3528,7 @@ public class XmppConnectionService extends Service {
if (publicationResponse.getType() == IqPacket.TYPE.RESULT) { if (publicationResponse.getType() == IqPacket.TYPE.RESULT) {
callback.onAvatarPublicationSucceeded(); callback.onAvatarPublicationSucceeded();
} else { } else {
Log.d(Config.LOGTAG, "failed to publish vcard " + publicationResponse.getError()); Log.d(Config.LOGTAG, "failed to publish vcard " + publicationResponse.getErrorCondition());
callback.onAvatarPublicationFailed(R.string.error_publish_avatar_server_reject); callback.onAvatarPublicationFailed(R.string.error_publish_avatar_server_reject);
} }
}); });
@ -3964,7 +3961,9 @@ public class XmppConnectionService extends Service {
if (message.getServerMsgId() == null) { if (message.getServerMsgId() == null) {
message.setServerMsgId(serverMessageId); message.setServerMsgId(serverMessageId);
} }
if (message.getEncryption() == Message.ENCRYPTION_NONE && isBodyModified(message, body)) { if (message.getEncryption() == Message.ENCRYPTION_NONE
&& message.isTypeText()
&& isBodyModified(message, body)) {
message.setBody(body.content); message.setBody(body.content);
if (body.count > 1) { if (body.count > 1) {
message.setBodyLanguage(body.language); message.setBodyLanguage(body.language);
@ -4346,7 +4345,7 @@ public class XmppConnectionService extends Service {
} }
private void sendPresence(final Account account, final boolean includeIdleTimestamp) { private void sendPresence(final Account account, final boolean includeIdleTimestamp) {
Presence.Status status; final Presence.Status status;
if (manuallyChangePresence()) { if (manuallyChangePresence()) {
status = account.getPresenceStatus(); status = account.getPresenceStatus();
} else { } else {
@ -4814,7 +4813,7 @@ public class XmppConnectionService extends Service {
public interface OnConferenceConfigurationFetched { public interface OnConferenceConfigurationFetched {
void onConferenceConfigurationFetched(Conversation conversation); void onConferenceConfigurationFetched(Conversation conversation);
void onFetchFailed(Conversation conversation, Element error); void onFetchFailed(Conversation conversation, String errorCondition);
} }
public interface OnConferenceJoined { public interface OnConferenceJoined {

View File

@ -986,7 +986,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
menuCall.setVisible(false); menuCall.setVisible(false);
menuOngoingCall.setVisible(false); menuOngoingCall.setVisible(false);
} else { } else {
final XmppConnectionService service = activity.xmppConnectionService; final XmppConnectionService service = activity == null ? null : activity.xmppConnectionService;
final Optional<OngoingRtpSession> ongoingRtpSession = service == null ? Optional.absent() : service.getJingleConnectionManager().getOngoingRtpConnection(conversation.getContact()); final Optional<OngoingRtpSession> ongoingRtpSession = service == null ? Optional.absent() : service.getJingleConnectionManager().getOngoingRtpConnection(conversation.getContact());
if (ongoingRtpSession.isPresent()) { if (ongoingRtpSession.isPresent()) {
menuOngoingCall.setVisible(true); menuOngoingCall.setVisible(true);
@ -994,8 +994,9 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
} else { } else {
menuOngoingCall.setVisible(false); menuOngoingCall.setVisible(false);
final RtpCapability.Capability rtpCapability = RtpCapability.check(conversation.getContact()); final RtpCapability.Capability rtpCapability = RtpCapability.check(conversation.getContact());
final boolean cameraAvailable = activity != null && activity.isCameraFeatureAvailable();
menuCall.setVisible(rtpCapability != RtpCapability.Capability.NONE); menuCall.setVisible(rtpCapability != RtpCapability.Capability.NONE);
menuVideoCall.setVisible(rtpCapability == RtpCapability.Capability.VIDEO); menuVideoCall.setVisible(rtpCapability == RtpCapability.Capability.VIDEO && cameraAvailable);
} }
menuContactDetails.setVisible(!this.conversation.withSelf()); menuContactDetails.setVisible(!this.conversation.withSelf());
menuMucDetails.setVisible(false); menuMucDetails.setVisible(false);
@ -1605,7 +1606,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
} }
private void createNewConnection(final Message message) { private void createNewConnection(final Message message) {
if (!activity.xmppConnectionService.getHttpConnectionManager().checkConnection(message)) { if (!activity.xmppConnectionService.hasInternetConnection()) {
Toast.makeText(getActivity(), R.string.not_connected_try_again, Toast.LENGTH_SHORT).show(); Toast.makeText(getActivity(), R.string.not_connected_try_again, Toast.LENGTH_SHORT).show();
return; return;
} }
@ -2991,6 +2992,11 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
final Menu menu = popupMenu.getMenu(); final Menu menu = popupMenu.getMenu();
menu.findItem(R.id.action_manage_accounts).setVisible(QuickConversationsService.isConversations()); menu.findItem(R.id.action_manage_accounts).setVisible(QuickConversationsService.isConversations());
popupMenu.setOnMenuItemClickListener(item -> { popupMenu.setOnMenuItemClickListener(item -> {
final XmppActivity activity = this.activity;
if (activity == null) {
Log.e(Config.LOGTAG,"Unable to perform action. no context provided");
return true;
}
switch (item.getItemId()) { switch (item.getItemId()) {
case R.id.action_show_qr_code: case R.id.action_show_qr_code:
activity.showQrCode(conversation.getAccount().getShareableUri()); activity.showQrCode(conversation.getAccount().getShareableUri());

View File

@ -38,7 +38,6 @@ import com.google.common.base.CharMatcher;
import org.openintents.openpgp.util.OpenPgpUtils; import org.openintents.openpgp.util.OpenPgpUtils;
import java.net.URL;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@ -78,6 +77,7 @@ import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.XmppConnection.Features; import eu.siacs.conversations.xmpp.XmppConnection.Features;
import eu.siacs.conversations.xmpp.forms.Data; import eu.siacs.conversations.xmpp.forms.Data;
import eu.siacs.conversations.xmpp.pep.Avatar; import eu.siacs.conversations.xmpp.pep.Avatar;
import okhttp3.HttpUrl;
public class EditAccountActivity extends OmemoActivity implements OnAccountUpdate, OnUpdateBlocklist, public class EditAccountActivity extends OmemoActivity implements OnAccountUpdate, OnUpdateBlocklist,
OnKeyStatusUpdated, OnCaptchaRequested, KeyChainAliasCallback, XmppConnectionService.OnShowErrorToast, XmppConnectionService.OnMamPreferencesFetched { OnKeyStatusUpdated, OnCaptchaRequested, KeyChainAliasCallback, XmppConnectionService.OnShowErrorToast, XmppConnectionService.OnMamPreferencesFetched {
@ -188,7 +188,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
final boolean openRegistrationUrl = registerNewAccount && !accountInfoEdited && mAccount != null && mAccount.getStatus() == Account.State.REGISTRATION_WEB; final boolean openRegistrationUrl = registerNewAccount && !accountInfoEdited && mAccount != null && mAccount.getStatus() == Account.State.REGISTRATION_WEB;
final boolean openPaymentUrl = mAccount != null && mAccount.getStatus() == Account.State.PAYMENT_REQUIRED; final boolean openPaymentUrl = mAccount != null && mAccount.getStatus() == Account.State.PAYMENT_REQUIRED;
final boolean redirectionWorthyStatus = openPaymentUrl || openRegistrationUrl; final boolean redirectionWorthyStatus = openPaymentUrl || openRegistrationUrl;
URL url = connection != null && redirectionWorthyStatus ? connection.getRedirectionUrl() : null; final HttpUrl url = connection != null && redirectionWorthyStatus ? connection.getRedirectionUrl() : null;
if (url != null && !wasDisabled) { if (url != null && !wasDisabled) {
try { try {
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url.toString()))); startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url.toString())));
@ -531,7 +531,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
} }
} else { } else {
XmppConnection connection = mAccount == null ? null : mAccount.getXmppConnection(); XmppConnection connection = mAccount == null ? null : mAccount.getXmppConnection();
URL url = connection != null && mAccount.getStatus() == Account.State.PAYMENT_REQUIRED ? connection.getRedirectionUrl() : null; HttpUrl url = connection != null && mAccount.getStatus() == Account.State.PAYMENT_REQUIRED ? connection.getRedirectionUrl() : null;
if (url != null) { if (url != null) {
this.binding.saveButton.setText(R.string.open_website); this.binding.saveButton.setText(R.string.open_website);
} else if (inNeedOfSaslAccept()) { } else if (inNeedOfSaslAccept()) {
@ -542,7 +542,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
} }
} else { } else {
XmppConnection connection = mAccount == null ? null : mAccount.getXmppConnection(); XmppConnection connection = mAccount == null ? null : mAccount.getXmppConnection();
URL url = connection != null && mAccount.getStatus() == Account.State.REGISTRATION_WEB ? connection.getRedirectionUrl() : null; HttpUrl url = connection != null && mAccount.getStatus() == Account.State.REGISTRATION_WEB ? connection.getRedirectionUrl() : null;
if (url != null && this.binding.accountRegisterNew.isChecked() && !accountInfoEdited) { if (url != null && this.binding.accountRegisterNew.isChecked() && !accountInfoEdited) {
this.binding.saveButton.setText(R.string.open_website); this.binding.saveButton.setText(R.string.open_website);
} else { } else {
@ -736,7 +736,8 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
} }
@Override @Override
public void onNewIntent(Intent intent) { public void onNewIntent(final Intent intent) {
super.onNewIntent(intent);
if (intent != null && intent.getData() != null) { if (intent != null && intent.getData() != null) {
final XmppUri uri = new XmppUri(intent.getData()); final XmppUri uri = new XmppUri(intent.getData());
if (xmppConnectionServiceBound) { if (xmppConnectionServiceBound) {
@ -1071,9 +1072,6 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
} else { } else {
this.binding.serverInfoHttpUpload.setText(R.string.server_info_available); this.binding.serverInfoHttpUpload.setText(R.string.server_info_available);
} }
} else if (features.p1S3FileTransfer()) {
this.binding.serverInfoHttpUploadDescription.setText(R.string.p1_s3_filetransfer);
this.binding.serverInfoHttpUpload.setText(R.string.server_info_available);
} else { } else {
this.binding.serverInfoHttpUpload.setText(R.string.server_info_unavailable); this.binding.serverInfoHttpUpload.setText(R.string.server_info_unavailable);
} }

View File

@ -31,8 +31,6 @@ import org.osmdroid.views.MapView;
import org.osmdroid.views.overlay.CopyrightOverlay; import org.osmdroid.views.overlay.CopyrightOverlay;
import org.osmdroid.views.overlay.Overlay; import org.osmdroid.views.overlay.Overlay;
import java.io.IOException;
import eu.siacs.conversations.BuildConfig; import eu.siacs.conversations.BuildConfig;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.R; import eu.siacs.conversations.R;
@ -98,11 +96,7 @@ public abstract class LocationActivity extends ActionBarActivity implements Loca
config.load(ctx, getPreferences()); config.load(ctx, getPreferences());
config.setUserAgentValue(BuildConfig.APPLICATION_ID + "/" + BuildConfig.VERSION_CODE); config.setUserAgentValue(BuildConfig.APPLICATION_ID + "/" + BuildConfig.VERSION_CODE);
if (QuickConversationsService.isConversations() && getBooleanPreference("use_tor", R.bool.use_tor)) { if (QuickConversationsService.isConversations() && getBooleanPreference("use_tor", R.bool.use_tor)) {
try { config.setHttpProxy(HttpConnectionManager.getProxy());
config.setHttpProxy(HttpConnectionManager.getProxy());
} catch (IOException e) {
throw new RuntimeException("Unable to configure proxy");
}
} }
} }

View File

@ -874,7 +874,12 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
} }
private void enableVideo(View view) { private void enableVideo(View view) {
requireRtpConnection().setVideoEnabled(true); try {
requireRtpConnection().setVideoEnabled(true);
} catch (final IllegalStateException e) {
Toast.makeText(this, R.string.unable_to_enable_video, Toast.LENGTH_SHORT).show();
return;
}
updateInCallButtonConfigurationVideo(true, requireRtpConnection().isCameraSwitchable()); updateInCallButtonConfigurationVideo(true, requireRtpConnection().isCameraSwitchable());
} }

View File

@ -411,6 +411,10 @@ public class SettingsActivity extends XmppActivity implements
private void createBackup() { private void createBackup() {
ContextCompat.startForegroundService(this, new Intent(this, ExportBackupService.class)); ContextCompat.startForegroundService(this, new Intent(this, ExportBackupService.class));
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setMessage(R.string.backup_started_message);
builder.setPositiveButton(R.string.ok, null);
builder.create().show();
} }
private void displayToast(final String msg) { private void displayToast(final String msg) {

View File

@ -408,11 +408,7 @@ public abstract class XmppActivity extends ActionBarActivity {
metrics = getResources().getDisplayMetrics(); metrics = getResources().getDisplayMetrics();
ExceptionHelper.init(getApplicationContext()); ExceptionHelper.init(getApplicationContext());
new EmojiService(this).init(); new EmojiService(this).init();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { this.isCameraFeatureAvailable = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
this.isCameraFeatureAvailable = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
} else {
this.isCameraFeatureAvailable = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA);
}
this.mTheme = findTheme(); this.mTheme = findTheme();
setTheme(this.mTheme); setTheme(this.mTheme);
} }

View File

@ -31,7 +31,7 @@ import androidx.core.content.ContextCompat;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import java.net.URL; import java.net.URI;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.regex.Matcher; import java.util.regex.Matcher;
@ -48,7 +48,6 @@ import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.Message.FileParams; import eu.siacs.conversations.entities.Message.FileParams;
import eu.siacs.conversations.entities.RtpSessionStatus; import eu.siacs.conversations.entities.RtpSessionStatus;
import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.http.P1S3UrlStreamHandler;
import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.MessageArchiveService; import eu.siacs.conversations.services.MessageArchiveService;
import eu.siacs.conversations.services.NotificationService; import eu.siacs.conversations.services.NotificationService;
@ -798,21 +797,13 @@ public class MessageAdapter extends ArrayAdapter<Message> {
displayEmojiMessage(viewHolder, message.getBody().trim(), darkBackground); displayEmojiMessage(viewHolder, message.getBody().trim(), darkBackground);
} else if (message.treatAsDownloadable()) { } else if (message.treatAsDownloadable()) {
try { try {
URL url = new URL(message.getBody()); final URI uri = new URI(message.getBody());
if (P1S3UrlStreamHandler.PROTOCOL_NAME.equalsIgnoreCase(url.getProtocol())) {
displayDownloadableMessage(viewHolder,
message,
activity.getString(R.string.check_x_filesize,
UIHelper.getFileDescriptionString(activity, message)),
darkBackground);
} else {
displayDownloadableMessage(viewHolder, displayDownloadableMessage(viewHolder,
message, message,
activity.getString(R.string.check_x_filesize_on_host, activity.getString(R.string.check_x_filesize_on_host,
UIHelper.getFileDescriptionString(activity, message), UIHelper.getFileDescriptionString(activity, message),
url.getHost()), uri.getHost()),
darkBackground); darkBackground);
}
} catch (Exception e) { } catch (Exception e) {
displayDownloadableMessage(viewHolder, displayDownloadableMessage(viewHolder,
message, message,
@ -890,10 +881,6 @@ public class MessageAdapter extends ArrayAdapter<Message> {
this.highlightedTerm = terms == null ? null : StylingHelper.filterHighlightedWords(terms); this.highlightedTerm = terms == null ? null : StylingHelper.filterHighlightedWords(terms);
} }
public interface OnQuoteListener {
void onQuote(String text);
}
public interface OnContactPictureClicked { public interface OnContactPictureClicked {
void onContactPictureClicked(Message message); void onContactPictureClicked(Message message);
} }

View File

@ -94,10 +94,10 @@ public class ShareUtil {
url = message.getBody(); url = message.getBody();
} else if (message.hasFileOnRemoteHost()) { } else if (message.hasFileOnRemoteHost()) {
resId = R.string.file_url; resId = R.string.file_url;
url = message.getFileParams().url.toString(); url = message.getFileParams().url;
} else { } else {
final Message.FileParams fileParams = message.getFileParams(); final Message.FileParams fileParams = message.getFileParams();
url = (fileParams != null && fileParams.url != null) ? fileParams.url.toString() : message.getBody().trim(); url = (fileParams != null && fileParams.url != null) ? fileParams.url : message.getBody().trim();
resId = R.string.file_url; resId = R.string.file_url;
} }
if (activity.copyTextToClipboard(url, resId)) { if (activity.copyTextToClipboard(url, resId)) {

View File

@ -17,7 +17,7 @@ import eu.siacs.conversations.ui.XmppActivity;
public class AccountUtils { public class AccountUtils {
public static final Class MANAGE_ACCOUNT_ACTIVITY; public static final Class<?> MANAGE_ACCOUNT_ACTIVITY;
static { static {
MANAGE_ACCOUNT_ACTIVITY = getManageAccountActivityClass(); MANAGE_ACCOUNT_ACTIVITY = getManageAccountActivityClass();
@ -78,7 +78,7 @@ public class AccountUtils {
return pending; return pending;
} }
public static void launchManageAccounts(Activity activity) { public static void launchManageAccounts(final Activity activity) {
if (MANAGE_ACCOUNT_ACTIVITY != null) { if (MANAGE_ACCOUNT_ACTIVITY != null) {
activity.startActivity(new Intent(activity, MANAGE_ACCOUNT_ACTIVITY)); activity.startActivity(new Intent(activity, MANAGE_ACCOUNT_ACTIVITY));
} else { } else {
@ -86,15 +86,15 @@ public class AccountUtils {
} }
} }
public static void launchManageAccount(XmppActivity xmppActivity) { public static void launchManageAccount(final XmppActivity xmppActivity) {
Account account = getFirst(xmppActivity.xmppConnectionService); final Account account = getFirst(xmppActivity.xmppConnectionService);
xmppActivity.switchToAccount(account); xmppActivity.switchToAccount(account);
} }
private static Class getManageAccountActivityClass() { private static Class<?> getManageAccountActivityClass() {
try { try {
return Class.forName("eu.siacs.conversations.ui.ManageAccountActivity"); return Class.forName("eu.siacs.conversations.ui.ManageAccountActivity");
} catch (ClassNotFoundException e) { } catch (final ClassNotFoundException e) {
return null; return null;
} }
} }

View File

@ -9,8 +9,6 @@ import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.asn1.x500.style.IETFUtils; import org.bouncycastle.asn1.x500.style.IETFUtils;
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
@ -31,7 +29,6 @@ import eu.siacs.conversations.Config;
import eu.siacs.conversations.R; import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.http.AesGcmURLStreamHandler;
import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.Jid;
public final class CryptoHelper { public final class CryptoHelper {
@ -278,28 +275,6 @@ public final class CryptoHelper {
} }
} }
public static URL toAesGcmUrl(URL url) {
if (!url.getProtocol().equalsIgnoreCase("https")) {
return url;
}
try {
return new URL(AesGcmURLStreamHandler.PROTOCOL_NAME + url.toString().substring(url.getProtocol().length()));
} catch (MalformedURLException e) {
return url;
}
}
public static URL toHttpsUrl(URL url) {
if (!url.getProtocol().equalsIgnoreCase(AesGcmURLStreamHandler.PROTOCOL_NAME)) {
return url;
}
try {
return new URL("https" + url.toString().substring(url.getProtocol().length()));
} catch (MalformedURLException e) {
return url;
}
}
public static boolean isPgpEncryptedUrl(String url) { public static boolean isPgpEncryptedUrl(String url) {
if (url == null) { if (url == null) {
return false; return false;

View File

@ -31,86 +31,90 @@ package eu.siacs.conversations.utils;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import java.net.MalformedURLException; import java.net.URI;
import java.net.URL; import java.net.URISyntaxException;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import eu.siacs.conversations.entities.Conversational; import eu.siacs.conversations.entities.Conversational;
import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.http.AesGcmURLStreamHandler; import eu.siacs.conversations.http.AesGcmURL;
import eu.siacs.conversations.http.P1S3UrlStreamHandler; import eu.siacs.conversations.http.URL;
public class MessageUtils { public class MessageUtils {
private static final Pattern LTR_RTL = Pattern.compile("(\\u200E[^\\u200F]*\\u200F){3,}"); private static final Pattern LTR_RTL = Pattern.compile("(\\u200E[^\\u200F]*\\u200F){3,}");
private static final String EMPTY_STRING = ""; private static final String EMPTY_STRING = "";
public static String prepareQuote(Message message) { public static String prepareQuote(Message message) {
final StringBuilder builder = new StringBuilder(); final StringBuilder builder = new StringBuilder();
final String body; final String body;
if (message.hasMeCommand()) { if (message.hasMeCommand()) {
final String nick; final String nick;
if (message.getStatus() == Message.STATUS_RECEIVED) { if (message.getStatus() == Message.STATUS_RECEIVED) {
if (message.getConversation().getMode() == Conversational.MODE_MULTI) { if (message.getConversation().getMode() == Conversational.MODE_MULTI) {
nick = Strings.nullToEmpty(message.getCounterpart().getResource()); nick = Strings.nullToEmpty(message.getCounterpart().getResource());
} else { } else {
nick = message.getContact().getPublicDisplayName(); nick = message.getContact().getPublicDisplayName();
} }
} else { } else {
nick = UIHelper.getMessageDisplayName(message); nick = UIHelper.getMessageDisplayName(message);
} }
body = nick + " " + message.getBody().substring(Message.ME_COMMAND.length()); body = nick + " " + message.getBody().substring(Message.ME_COMMAND.length());
} else { } else {
body = message.getMergedBody().toString(); body = message.getMergedBody().toString();
} }
for (String line : body.split("\n")) { for (String line : body.split("\n")) {
if (line.length() <= 0) { if (line.length() <= 0) {
continue; continue;
} }
final char c = line.charAt(0); final char c = line.charAt(0);
if (c == '>' && UIHelper.isPositionFollowedByQuoteableCharacter(line, 0) if (c == '>' && UIHelper.isPositionFollowedByQuoteableCharacter(line, 0)
|| (c == '\u00bb' && !UIHelper.isPositionFollowedByQuote(line, 0))) { || (c == '\u00bb' && !UIHelper.isPositionFollowedByQuote(line, 0))) {
continue; continue;
} }
if (builder.length() != 0) { if (builder.length() != 0) {
builder.append('\n'); builder.append('\n');
} }
builder.append(line.trim()); builder.append(line.trim());
} }
return builder.toString(); return builder.toString();
} }
public static boolean treatAsDownloadable(final String body, final boolean oob) { public static boolean treatAsDownloadable(final String body, final boolean oob) {
try { final String[] lines = body.split("\n");
final String[] lines = body.split("\n"); if (lines.length == 0) {
if (lines.length == 0) { return false;
return false; }
} for (final String line : lines) {
for (String line : lines) { if (line.contains("\\s+")) {
if (line.contains("\\s+")) { return false;
return false; }
} }
} final URI uri;
final URL url = new URL(lines[0]); try {
final String ref = url.getRef(); uri = new URI(lines[0]);
final String protocol = url.getProtocol(); } catch (final URISyntaxException e) {
final boolean encrypted = ref != null && AesGcmURLStreamHandler.IV_KEY.matcher(ref).matches(); return false;
final boolean followedByDataUri = lines.length == 2 && lines[1].startsWith("data:"); }
final boolean validAesGcm = AesGcmURLStreamHandler.PROTOCOL_NAME.equalsIgnoreCase(protocol) && encrypted && (lines.length == 1 || followedByDataUri); if (!URL.WELL_KNOWN_SCHEMES.contains(uri.getScheme())) {
final boolean validProtocol = "http".equalsIgnoreCase(protocol) || "https".equalsIgnoreCase(protocol) || P1S3UrlStreamHandler.PROTOCOL_NAME.equalsIgnoreCase(protocol); return false;
final boolean validOob = validProtocol && (oob || encrypted) && lines.length == 1; }
return validAesGcm || validOob; final String ref = uri.getFragment();
} catch (MalformedURLException e) { final String protocol = uri.getScheme();
return false; final boolean encrypted = ref != null && AesGcmURL.IV_KEY.matcher(ref).matches();
} final boolean followedByDataUri = lines.length == 2 && lines[1].startsWith("data:");
} final boolean validAesGcm = AesGcmURL.PROTOCOL_NAME.equalsIgnoreCase(protocol) && encrypted && (lines.length == 1 || followedByDataUri);
final boolean validProtocol = "http".equalsIgnoreCase(protocol) || "https".equalsIgnoreCase(protocol);
final boolean validOob = validProtocol && (oob || encrypted) && lines.length == 1;
return validAesGcm || validOob;
}
public static String filterLtrRtl(String body) { public static String filterLtrRtl(String body) {
return LTR_RTL.matcher(body).replaceFirst(EMPTY_STRING); return LTR_RTL.matcher(body).replaceFirst(EMPTY_STRING);
} }
public static boolean unInitiatedButKnownSize(Message message) { public static boolean unInitiatedButKnownSize(Message message) {
return message.getType() == Message.TYPE_TEXT && message.getTransferable() == null && message.isOOb() && message.getFileParams().size > 0 && message.getFileParams().url != null; return message.getType() == Message.TYPE_TEXT && message.getTransferable() == null && message.isOOb() && message.getFileParams().size > 0 && message.getFileParams().url != null;
} }
} }

View File

@ -25,7 +25,6 @@ import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URL;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Properties; import java.util.Properties;
@ -580,11 +579,6 @@ public final class MimeUtils {
return null; return null;
} }
public static String extractRelevantExtension(URL url) {
String path = url.getPath();
return extractRelevantExtension(path, true);
}
public static String extractRelevantExtension(final String path) { public static String extractRelevantExtension(final String path) {
return extractRelevantExtension(path, false); return extractRelevantExtension(path, false);
} }

View File

@ -22,10 +22,12 @@ import java.util.concurrent.TimeUnit;
import java.util.List; import java.util.List;
import de.measite.minidns.AbstractDNSClient; import de.measite.minidns.AbstractDNSClient;
import de.measite.minidns.DNSCache;
import de.measite.minidns.DNSClient; import de.measite.minidns.DNSClient;
import de.measite.minidns.DNSName; import de.measite.minidns.DNSName;
import de.measite.minidns.Question; import de.measite.minidns.Question;
import de.measite.minidns.Record; import de.measite.minidns.Record;
import de.measite.minidns.cache.LRUCache;
import de.measite.minidns.dnssec.DNSSECResultNotAuthenticException; import de.measite.minidns.dnssec.DNSSECResultNotAuthenticException;
import de.measite.minidns.dnsserverlookup.AndroidUsingExec; import de.measite.minidns.dnsserverlookup.AndroidUsingExec;
import de.measite.minidns.hla.DnssecResolverApi; import de.measite.minidns.hla.DnssecResolverApi;
@ -75,9 +77,7 @@ public class Resolver {
final Field useHardcodedDnsServers = DNSClient.class.getDeclaredField("useHardcodedDnsServers"); final Field useHardcodedDnsServers = DNSClient.class.getDeclaredField("useHardcodedDnsServers");
useHardcodedDnsServers.setAccessible(true); useHardcodedDnsServers.setAccessible(true);
useHardcodedDnsServers.setBoolean(dnsClient, false); useHardcodedDnsServers.setBoolean(dnsClient, false);
} catch (NoSuchFieldException e) { } catch (NoSuchFieldException | IllegalAccessException e) {
Log.e(Config.LOGTAG, "Unable to disable hardcoded DNS servers", e);
} catch (IllegalAccessException e) {
Log.e(Config.LOGTAG, "Unable to disable hardcoded DNS servers", e); Log.e(Config.LOGTAG, "Unable to disable hardcoded DNS servers", e);
} }
} }
@ -91,10 +91,6 @@ public class Resolver {
return happyEyeball(resolveNoSrvRecords(DNSName.from(hostname), port, true)); return happyEyeball(resolveNoSrvRecords(DNSName.from(hostname), port, true));
} }
public static boolean useDirectTls(final int port) {
return port == 443 || port == 5223;
}
public static boolean invalidHostname(final String hostname) { public static boolean invalidHostname(final String hostname) {
try { try {
DNSName.from(hostname); DNSName.from(hostname);
@ -104,15 +100,30 @@ public class Resolver {
} }
} }
public static void clearCache() {
final AbstractDNSClient client = ResolverApi.INSTANCE.getClient();
final DNSCache dnsCache = client.getCache();
if (dnsCache instanceof LRUCache) {
Log.d(Config.LOGTAG,"clearing DNS cache");
((LRUCache) dnsCache).clear();
}
}
public static boolean useDirectTls(final int port) {
return port == 443 || port == 5223;
}
public static Result resolve(String domain) { public static Result resolve(String domain) {
final Result ipResult = fromIpAddress(domain, DEFAULT_PORT_XMPP); final Result ipResult = fromIpAddress(domain, DEFAULT_PORT_XMPP);
if (ipResult != null) { if (ipResult != null) {
ipResult.connect(); ipResult.connect();
return ipResult; return ipResult;
} }
final List<Result> results = new ArrayList<>(); final List<Result> results = new ArrayList<>();
final List<Result> fallbackResults = new ArrayList<>(); final List<Result> fallbackResults = new ArrayList<>();
Thread[] threads = new Thread[3]; final Thread[] threads = new Thread[3];
threads[0] = new Thread(() -> { threads[0] = new Thread(() -> {
try { try {
final List<Result> list = resolveSrv(domain, true); final List<Result> list = resolveSrv(domain, true);
@ -139,7 +150,7 @@ public class Resolver {
fallbackResults.addAll(list); fallbackResults.addAll(list);
} }
}); });
for (Thread thread : threads) { for (final Thread thread : threads) {
thread.start(); thread.start();
} }
try { try {

View File

@ -32,7 +32,7 @@ public class XmppUri {
private Map<String, String> parameters = Collections.emptyMap(); private Map<String, String> parameters = Collections.emptyMap();
private boolean safeSource = true; private boolean safeSource = true;
public XmppUri(String uri) { public XmppUri(final String uri) {
try { try {
parse(Uri.parse(uri)); parse(Uri.parse(uri));
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {

View File

@ -23,7 +23,6 @@ public final class Namespace {
public static final String NICK = "http://jabber.org/protocol/nick"; public static final String NICK = "http://jabber.org/protocol/nick";
public static final String FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL = "http://jabber.org/protocol/offline"; public static final String FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL = "http://jabber.org/protocol/offline";
public static final String BIND = "urn:ietf:params:xml:ns:xmpp-bind"; public static final String BIND = "urn:ietf:params:xml:ns:xmpp-bind";
public static final String P1_S3_FILE_TRANSFER = "p1:s3filetransfer";
public static final String BOOKMARKS_CONVERSION = "urn:xmpp:bookmarks-conversion:0"; public static final String BOOKMARKS_CONVERSION = "urn:xmpp:bookmarks-conversion:0";
public static final String BOOKMARKS = "storage:bookmarks"; public static final String BOOKMARKS = "storage:bookmarks";
public static final String SYNCHRONIZATION = "im.quicksy.synchronization:0"; public static final String SYNCHRONIZATION = "im.quicksy.synchronization:0";

View File

@ -0,0 +1,8 @@
package eu.siacs.conversations.xmpp;
public class IqResponseException extends Exception {
public IqResponseException(final String message) {
super(message);
}
}

View File

@ -21,9 +21,7 @@ import java.net.ConnectException;
import java.net.IDN; import java.net.IDN;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.Socket; import java.net.Socket;
import java.net.URL;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.security.KeyManagementException; import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
@ -70,6 +68,7 @@ import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.ServiceDiscoveryResult; import eu.siacs.conversations.entities.ServiceDiscoveryResult;
import eu.siacs.conversations.generator.IqGenerator; import eu.siacs.conversations.generator.IqGenerator;
import eu.siacs.conversations.http.HttpConnectionManager;
import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.MemorizingTrustManager; import eu.siacs.conversations.services.MemorizingTrustManager;
import eu.siacs.conversations.services.MessageArchiveService; import eu.siacs.conversations.services.MessageArchiveService;
@ -88,7 +87,6 @@ import eu.siacs.conversations.xml.Tag;
import eu.siacs.conversations.xml.TagWriter; import eu.siacs.conversations.xml.TagWriter;
import eu.siacs.conversations.xml.XmlReader; import eu.siacs.conversations.xml.XmlReader;
import eu.siacs.conversations.xmpp.forms.Data; import eu.siacs.conversations.xmpp.forms.Data;
import eu.siacs.conversations.xmpp.forms.Field;
import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived; import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
import eu.siacs.conversations.xmpp.stanzas.AbstractAcknowledgeableStanza; import eu.siacs.conversations.xmpp.stanzas.AbstractAcknowledgeableStanza;
@ -102,6 +100,7 @@ import eu.siacs.conversations.xmpp.stanzas.streammgmt.AckPacket;
import eu.siacs.conversations.xmpp.stanzas.streammgmt.EnablePacket; import eu.siacs.conversations.xmpp.stanzas.streammgmt.EnablePacket;
import eu.siacs.conversations.xmpp.stanzas.streammgmt.RequestPacket; import eu.siacs.conversations.xmpp.stanzas.streammgmt.RequestPacket;
import eu.siacs.conversations.xmpp.stanzas.streammgmt.ResumePacket; import eu.siacs.conversations.xmpp.stanzas.streammgmt.ResumePacket;
import okhttp3.HttpUrl;
public class XmppConnection implements Runnable { public class XmppConnection implements Runnable {
@ -172,7 +171,7 @@ public class XmppConnection implements Runnable {
private OnBindListener bindListener = null; private OnBindListener bindListener = null;
private OnMessageAcknowledged acknowledgedListener = null; private OnMessageAcknowledged acknowledgedListener = null;
private SaslMechanism saslMechanism; private SaslMechanism saslMechanism;
private URL redirectionUrl = null; private HttpUrl redirectionUrl = null;
private String verifiedHostname = null; private String verifiedHostname = null;
private volatile Thread mThread; private volatile Thread mThread;
private CountDownLatch mStreamCountDownLatch; private CountDownLatch mStreamCountDownLatch;
@ -356,9 +355,7 @@ public class XmppConnection implements Runnable {
this.changeStatus(Account.State.MISSING_INTERNET_PERMISSION); this.changeStatus(Account.State.MISSING_INTERNET_PERMISSION);
} catch (final StateChangingException e) { } catch (final StateChangingException e) {
this.changeStatus(e.state); this.changeStatus(e.state);
} catch (final UnknownHostException | ConnectException e) { } catch (final UnknownHostException | ConnectException | SocksSocketFactory.HostNotFoundException e) {
this.changeStatus(Account.State.SERVER_NOT_FOUND);
} catch (final SocksSocketFactory.HostNotFoundException e) {
this.changeStatus(Account.State.SERVER_NOT_FOUND); this.changeStatus(Account.State.SERVER_NOT_FOUND);
} catch (final SocksSocketFactory.SocksProxyNotFoundException e) { } catch (final SocksSocketFactory.SocksProxyNotFoundException e) {
this.changeStatus(Account.State.TOR_NOT_AVAILABLE); this.changeStatus(Account.State.TOR_NOT_AVAILABLE);
@ -471,13 +468,14 @@ public class XmppConnection implements Runnable {
if (failure.hasChild("account-disabled") && text != null) { if (failure.hasChild("account-disabled") && text != null) {
Matcher matcher = Patterns.AUTOLINK_WEB_URL.matcher(text); Matcher matcher = Patterns.AUTOLINK_WEB_URL.matcher(text);
if (matcher.find()) { if (matcher.find()) {
final HttpUrl url;
try { try {
URL url = new URL(text.substring(matcher.start(), matcher.end())); url = HttpUrl.get(text.substring(matcher.start(), matcher.end()));
if (url.getProtocol().equals("https")) { if (url.isHttps()) {
this.redirectionUrl = url; this.redirectionUrl = url;
throw new StateChangingException(Account.State.PAYMENT_REQUIRED); throw new StateChangingException(Account.State.PAYMENT_REQUIRED);
} }
} catch (MalformedURLException e) { } catch (IllegalArgumentException e) {
throw new StateChangingException(Account.State.UNAUTHORIZED); throw new StateChangingException(Account.State.UNAUTHORIZED);
} }
} }
@ -903,7 +901,7 @@ public class XmppConnection implements Runnable {
if (response.getType() == IqPacket.TYPE.RESULT) { if (response.getType() == IqPacket.TYPE.RESULT) {
sendRegistryRequest(); sendRegistryRequest();
} else { } else {
final Element error = response.getError(); final String error = response.getErrorCondition();
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); throw new StateChangingError(Account.State.REGISTRATION_INVALID_TOKEN);
} }
@ -947,11 +945,19 @@ public class XmppConnection implements Runnable {
is = null; is = null;
} }
} else { } else {
final boolean useTor = mXmppConnectionService.useTorToConnect() || account.isOnion();
try { try {
Field field = data.getFieldByName("url"); final String url = data.getValue("url");
URL url = field != null && field.getValue() != null ? new URL(field.getValue()) : null; final String fallbackUrl = data.getValue("captcha-fallback-url");
is = url != null ? url.openStream() : null; if (url != null) {
} catch (IOException e) { is = HttpConnectionManager.open(url, useTor);
} else if (fallbackUrl != null) {
is = HttpConnectionManager.open(fallbackUrl, useTor);
} else {
is = null;
}
} catch (final IOException e) {
Log.d(Config.LOGTAG,account.getJid().asBareJid()+": unable to fetch captcha", e);
is = null; is = null;
} }
} }
@ -974,7 +980,7 @@ public class XmppConnection implements Runnable {
if (url != null) { if (url != null) {
setAccountCreationFailed(url); setAccountCreationFailed(url);
} else if (instructions != null) { } else if (instructions != null) {
Matcher matcher = Patterns.AUTOLINK_WEB_URL.matcher(instructions); final Matcher matcher = Patterns.AUTOLINK_WEB_URL.matcher(instructions);
if (matcher.find()) { if (matcher.find()) {
setAccountCreationFailed(instructions.substring(matcher.start(), matcher.end())); setAccountCreationFailed(instructions.substring(matcher.start(), matcher.end()));
} }
@ -984,21 +990,16 @@ public class XmppConnection implements Runnable {
}, true); }, true);
} }
private void setAccountCreationFailed(String url) { private void setAccountCreationFailed(final String url) {
if (url != null) { final HttpUrl httpUrl = url == null ? null : HttpUrl.parse(url);
try { if (httpUrl != null && httpUrl.isHttps()) {
this.redirectionUrl = new URL(url); this.redirectionUrl = httpUrl;
if (this.redirectionUrl.getProtocol().equals("https")) { throw new StateChangingError(Account.State.REGISTRATION_WEB);
throw new StateChangingError(Account.State.REGISTRATION_WEB);
}
} catch (MalformedURLException e) {
//fall through
}
} }
throw new StateChangingError(Account.State.REGISTRATION_FAILED); throw new StateChangingError(Account.State.REGISTRATION_FAILED);
} }
public URL getRedirectionUrl() { public HttpUrl getRedirectionUrl() {
return this.redirectionUrl; return this.redirectionUrl;
} }
@ -1894,10 +1895,6 @@ public class XmppConnection implements Runnable {
this.blockListRequested = value; this.blockListRequested = value;
} }
public boolean p1S3FileTransfer() {
return hasDiscoFeature(account.getDomain(), Namespace.P1_S3_FILE_TRANSFER);
}
public boolean httpUpload(long filesize) { public boolean httpUpload(long filesize) {
if (Config.DISABLE_HTTP_UPLOAD) { if (Config.DISABLE_HTTP_UPLOAD) {
return false; return false;

View File

@ -288,6 +288,9 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
} }
final String sdpMid = content.getKey(); final String sdpMid = content.getKey();
final int mLineIndex = indices.indexOf(sdpMid); final int mLineIndex = indices.indexOf(sdpMid);
if (mLineIndex < 0) {
Log.w(Config.LOGTAG, "mLineIndex not found for " + sdpMid + ". available indices " + indices);
}
final IceCandidate iceCandidate = new IceCandidate(sdpMid, mLineIndex, sdp); final IceCandidate iceCandidate = new IceCandidate(sdpMid, mLineIndex, sdp);
Log.d(Config.LOGTAG, "received candidate: " + iceCandidate); Log.d(Config.LOGTAG, "received candidate: " + iceCandidate);
this.webRTCWrapper.addIceCandidate(iceCandidate); this.webRTCWrapper.addIceCandidate(iceCandidate);
@ -305,7 +308,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
throw new SecurityException("Unable to verify DTLS Fingerprint with OMEMO", e); throw new SecurityException("Unable to verify DTLS Fingerprint with OMEMO", e);
} }
this.omemoVerification.setOrEnsureEqual(omemoVerifiedPayload); this.omemoVerification.setOrEnsureEqual(omemoVerifiedPayload);
Log.d(Config.LOGTAG,id.account.getJid().asBareJid()+": received verifiable DTLS fingerprint via "+this.omemoVerification); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received verifiable DTLS fingerprint via " + this.omemoVerification);
return omemoVerifiedPayload.getPayload(); return omemoVerifiedPayload.getPayload();
} else if (expectVerification) { } else if (expectVerification) {
throw new SecurityException("DTLS fingerprint was unexpectedly not verifiable"); throw new SecurityException("DTLS fingerprint was unexpectedly not verifiable");
@ -677,7 +680,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
this.omemoVerification.setDeviceId(remoteDeviceId); this.omemoVerification.setDeviceId(remoteDeviceId);
} else { } else {
if (remoteDeviceId != null) { if (remoteDeviceId != null) {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid()+": remote party signaled support for OMEMO verification but we have OMEMO disabled"); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": remote party signaled support for OMEMO verification but we have OMEMO disabled");
} }
this.omemoVerification.setDeviceId(null); this.omemoVerification.setDeviceId(null);
} }
@ -781,7 +784,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
try { try {
verifiedPayload = id.account.getAxolotlService().encrypt(rtpContentMap, id.with, omemoVerification.getDeviceId()); verifiedPayload = id.account.getAxolotlService().encrypt(rtpContentMap, id.with, omemoVerification.getDeviceId());
} catch (final CryptoFailedException e) { } catch (final CryptoFailedException e) {
Log.w(Config.LOGTAG,id.account.getJid().asBareJid()+": unable to use OMEMO DTLS verification on outgoing session initiate. falling back", e); Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to use OMEMO DTLS verification on outgoing session initiate. falling back", e);
return rtpContentMap; return rtpContentMap;
} }
this.omemoVerification.setSessionFingerprint(verifiedPayload.getFingerprint()); this.omemoVerification.setSessionFingerprint(verifiedPayload.getFingerprint());

View File

@ -40,7 +40,7 @@ public class RtpContentMap {
} }
public static RtpContentMap of(final JinglePacket jinglePacket) { public static RtpContentMap of(final JinglePacket jinglePacket) {
final Map<String, DescriptionTransport> contents = DescriptionTransport.of(jinglePacket.getJingleContents()); final Map<String, DescriptionTransport> contents = DescriptionTransport.of(jinglePacket.getJingleContents());
if (isOmemoVerified(contents)) { if (isOmemoVerified(contents)) {
return new OmemoVerifiedRtpContentMap(jinglePacket.getGroup(), contents); return new OmemoVerifiedRtpContentMap(jinglePacket.getGroup(), contents);
} else { } else {
@ -53,7 +53,7 @@ public class RtpContentMap {
if (values.size() == 0) { if (values.size() == 0) {
return false; return false;
} }
for(final DescriptionTransport descriptionTransport : values) { for (final DescriptionTransport descriptionTransport : values) {
if (descriptionTransport.transport instanceof OmemoVerifiedIceUdpTransportInfo) { if (descriptionTransport.transport instanceof OmemoVerifiedIceUdpTransportInfo) {
continue; continue;
} }
@ -174,7 +174,7 @@ public class RtpContentMap {
} }
public static DescriptionTransport of(final SessionDescription sessionDescription, final SessionDescription.Media media) { public static DescriptionTransport of(final SessionDescription sessionDescription, final SessionDescription.Media media) {
final RtpDescription rtpDescription = RtpDescription.of(media); final RtpDescription rtpDescription = RtpDescription.of(sessionDescription, media);
final IceUdpTransportInfo transportInfo = IceUdpTransportInfo.of(sessionDescription, media); final IceUdpTransportInfo transportInfo = IceUdpTransportInfo.of(sessionDescription, media);
return new DescriptionTransport(rtpDescription, transportInfo); return new DescriptionTransport(rtpDescription, transportInfo);
} }

View File

@ -198,10 +198,10 @@ public class SessionDescription {
checkNoWhitespace(type, "feedback negotiation type must not contain whitespace"); checkNoWhitespace(type, "feedback negotiation type must not contain whitespace");
mediaAttributes.put("rtcp-fb", "* " + type + (Strings.isNullOrEmpty(subtype) ? "" : " " + subtype)); mediaAttributes.put("rtcp-fb", "* " + type + (Strings.isNullOrEmpty(subtype) ? "" : " " + subtype));
} }
for (RtpDescription.FeedbackNegotiationTrrInt feedbackNegotiationTrrInt : description.feedbackNegotiationTrrInts()) { for (final RtpDescription.FeedbackNegotiationTrrInt feedbackNegotiationTrrInt : description.feedbackNegotiationTrrInts()) {
mediaAttributes.put("rtcp-fb", "* trr-int " + feedbackNegotiationTrrInt.getValue()); mediaAttributes.put("rtcp-fb", "* trr-int " + feedbackNegotiationTrrInt.getValue());
} }
for (RtpDescription.RtpHeaderExtension extension : description.getHeaderExtensions()) { for (final RtpDescription.RtpHeaderExtension extension : description.getHeaderExtensions()) {
final String id = extension.getId(); final String id = extension.getId();
final String uri = extension.getUri(); final String uri = extension.getUri();
if (Strings.isNullOrEmpty(id)) { if (Strings.isNullOrEmpty(id)) {
@ -214,7 +214,12 @@ public class SessionDescription {
checkNoWhitespace(uri, "feedback negotiation uri must not contain whitespace"); checkNoWhitespace(uri, "feedback negotiation uri must not contain whitespace");
mediaAttributes.put("extmap", id + " " + uri); mediaAttributes.put("extmap", id + " " + uri);
} }
for (RtpDescription.SourceGroup sourceGroup : description.getSourceGroups()) {
if (Config.PROCESS_EXTMAP_ALLOW_MIXED && description.hasChild("extmap-allow-mixed", Namespace.JINGLE_RTP_HEADER_EXTENSIONS)) {
mediaAttributes.put("extmap-allow-mixed", "");
}
for (final RtpDescription.SourceGroup sourceGroup : description.getSourceGroups()) {
final String semantics = sourceGroup.getSemantics(); final String semantics = sourceGroup.getSemantics();
final List<String> groups = sourceGroup.getSsrcs(); final List<String> groups = sourceGroup.getSsrcs();
if (Strings.isNullOrEmpty(semantics)) { if (Strings.isNullOrEmpty(semantics)) {
@ -226,8 +231,8 @@ public class SessionDescription {
} }
mediaAttributes.put("ssrc-group", String.format("%s %s", semantics, Joiner.on(' ').join(groups))); mediaAttributes.put("ssrc-group", String.format("%s %s", semantics, Joiner.on(' ').join(groups)));
} }
for (RtpDescription.Source source : description.getSources()) { for (final RtpDescription.Source source : description.getSources()) {
for (RtpDescription.Source.Parameter parameter : source.getParameters()) { for (final RtpDescription.Source.Parameter parameter : source.getParameters()) {
final String id = source.getSsrcId(); final String id = source.getSsrcId();
final String parameterName = parameter.getParameterName(); final String parameterName = parameter.getParameterName();
final String parameterValue = parameter.getParameterValue(); final String parameterValue = parameter.getParameterValue();

View File

@ -208,6 +208,14 @@ public class WebRTCWrapper {
return null; return null;
} }
private static boolean isFrontFacing(final CameraEnumerator cameraEnumerator, final String deviceName) {
try {
return cameraEnumerator.isFrontFacing(deviceName);
} catch (final NullPointerException e) {
return false;
}
}
public void setup(final XmppConnectionService service, final AppRTCAudioManager.SpeakerPhonePreference speakerPhonePreference) throws InitializationException { public void setup(final XmppConnectionService service, final AppRTCAudioManager.SpeakerPhonePreference speakerPhonePreference) throws InitializationException {
try { try {
PeerConnectionFactory.initialize( PeerConnectionFactory.initialize(
@ -247,7 +255,14 @@ public class WebRTCWrapper {
.createPeerConnectionFactory(); .createPeerConnectionFactory();
final MediaStream stream = peerConnectionFactory.createLocalMediaStream("my-media-stream"); final PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(iceServers);
rtcConfig.tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.DISABLED; //XEP-0176 doesn't support tcp
rtcConfig.continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY;
rtcConfig.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN;
final PeerConnection peerConnection = peerConnectionFactory.createPeerConnection(rtcConfig, peerConnectionObserver);
if (peerConnection == null) {
throw new InitializationException("Unable to create PeerConnection");
}
final Optional<CapturerChoice> optionalCapturerChoice = media.contains(Media.VIDEO) ? getVideoCapturer() : Optional.absent(); final Optional<CapturerChoice> optionalCapturerChoice = media.contains(Media.VIDEO) ? getVideoCapturer() : Optional.absent();
@ -262,7 +277,7 @@ public class WebRTCWrapper {
this.localVideoTrack = peerConnectionFactory.createVideoTrack("my-video-track", videoSource); this.localVideoTrack = peerConnectionFactory.createVideoTrack("my-video-track", videoSource);
stream.addTrack(this.localVideoTrack); peerConnection.addTrack(this.localVideoTrack);
} }
@ -270,18 +285,8 @@ public class WebRTCWrapper {
//set up audio track //set up audio track
final AudioSource audioSource = peerConnectionFactory.createAudioSource(new MediaConstraints()); final AudioSource audioSource = peerConnectionFactory.createAudioSource(new MediaConstraints());
this.localAudioTrack = peerConnectionFactory.createAudioTrack("my-audio-track", audioSource); this.localAudioTrack = peerConnectionFactory.createAudioTrack("my-audio-track", audioSource);
stream.addTrack(this.localAudioTrack); peerConnection.addTrack(this.localAudioTrack);
} }
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");
}
peerConnection.addStream(stream);
peerConnection.setAudioPlayout(true); peerConnection.setAudioPlayout(true);
peerConnection.setAudioRecording(true); peerConnection.setAudioRecording(true);
this.peerConnection = peerConnection; this.peerConnection = peerConnection;
@ -388,7 +393,7 @@ public class WebRTCWrapper {
boolean isVideoEnabled() { boolean isVideoEnabled() {
final VideoTrack videoTrack = this.localVideoTrack; final VideoTrack videoTrack = this.localVideoTrack;
if (videoTrack == null) { if (videoTrack == null) {
throw new IllegalStateException("Local video track does not exist"); return false;
} }
return videoTrack.enabled(); return videoTrack.enabled();
} }
@ -525,14 +530,6 @@ public class WebRTCWrapper {
} }
} }
private static boolean isFrontFacing(final CameraEnumerator cameraEnumerator, final String deviceName) {
try {
return cameraEnumerator.isFrontFacing(deviceName);
} catch (final NullPointerException e) {
return false;
}
}
public PeerConnection.PeerConnectionState getState() { public PeerConnection.PeerConnectionState getState() {
return requirePeerConnection().connectionState(); return requirePeerConnection().connectionState();
} }

View File

@ -1,11 +1,8 @@
package eu.siacs.conversations.xmpp.jingle.stanzas; package eu.siacs.conversations.xmpp.jingle.stanzas;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.primitives.Ints; import com.google.common.primitives.Ints;
import java.util.List;
import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xml.Namespace;

View File

@ -6,11 +6,14 @@ import com.google.common.base.Preconditions;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xml.Namespace;
@ -530,11 +533,15 @@ public class RtpDescription extends GenericDescription {
} }
} }
public static RtpDescription of(final SessionDescription.Media media) { public static RtpDescription of(final SessionDescription sessionDescription, final SessionDescription.Media media) {
final RtpDescription rtpDescription = new RtpDescription(media.media); final RtpDescription rtpDescription = new RtpDescription(media.media);
final Map<String, List<Parameter>> parameterMap = new HashMap<>(); final Map<String, List<Parameter>> parameterMap = new HashMap<>();
final ArrayListMultimap<String, Element> feedbackNegotiationMap = ArrayListMultimap.create(); final ArrayListMultimap<String, Element> feedbackNegotiationMap = ArrayListMultimap.create();
final ArrayListMultimap<String, Source.Parameter> sourceParameterMap = ArrayListMultimap.create(); final ArrayListMultimap<String, Source.Parameter> sourceParameterMap = ArrayListMultimap.create();
final Set<String> attributes = Sets.newHashSet(Iterables.concat(
sessionDescription.attributes.keySet(),
media.attributes.keySet()
));
for (final String rtcpFb : media.attributes.get("rtcp-fb")) { for (final String rtcpFb : media.attributes.get("rtcp-fb")) {
final String[] parts = rtcpFb.split(" "); final String[] parts = rtcpFb.split(" ");
if (parts.length >= 2) { if (parts.length >= 2) {
@ -581,6 +588,9 @@ public class RtpDescription extends GenericDescription {
rtpDescription.addChild(extension); rtpDescription.addChild(extension);
} }
} }
if (attributes.contains("extmap-allow-mixed")) {
rtpDescription.addChild("extmap-allow-mixed", Namespace.JINGLE_RTP_HEADER_EXTENSIONS);
}
for (final String ssrcGroup : media.attributes.get("ssrc-group")) { for (final String ssrcGroup : media.attributes.get("ssrc-group")) {
final String[] parts = ssrcGroup.split(" "); final String[] parts = ssrcGroup.split(" ");
if (parts.length >= 2) { if (parts.length >= 2) {

View File

@ -5,44 +5,38 @@ import eu.siacs.conversations.xmpp.InvalidJid;
abstract public class AbstractAcknowledgeableStanza extends AbstractStanza { abstract public class AbstractAcknowledgeableStanza extends AbstractStanza {
protected AbstractAcknowledgeableStanza(String name) { protected AbstractAcknowledgeableStanza(String name) {
super(name); super(name);
} }
public String getId() { public String getId() {
return this.getAttribute("id"); return this.getAttribute("id");
} }
public void setId(final String id) { public void setId(final String id) {
setAttribute("id", id); setAttribute("id", id);
} }
public Element getError() { private Element getErrorConditionElement() {
Element error = findChild("error"); final Element error = findChild("error");
if (error != null) { if (error == null) {
for(Element element : error.getChildren()) { return null;
if (!element.getName().equals("text")) { }
return element; for (final Element element : error.getChildren()) {
} if (!element.getName().equals("text")) {
} return element;
} }
return null; }
} return null;
}
public String getErrorCondition() { public String getErrorCondition() {
Element error = findChild("error"); final Element condition = getErrorConditionElement();
if (error != null) { return condition == null ? null : condition.getName();
for(Element element : error.getChildren()) { }
if (!element.getName().equals("text")) {
return element.getName();
}
}
}
return null;
}
public boolean valid() { public boolean valid() {
return InvalidJid.isValid(getFrom()) && InvalidJid.isValid(getTo()); return InvalidJid.isValid(getFrom()) && InvalidJid.isValid(getTo());
} }
} }

View File

@ -942,4 +942,4 @@ que l\'administrador del servidor llegeixi els missatges, però pot ser l\'únic
<string name="unable_to_parse_invite">No es pot processar la invitació</string> <string name="unable_to_parse_invite">No es pot processar la invitació</string>
<string name="server_does_not_support_easy_onboarding_invites">El servidor no admet la generació d\'invitacions</string> <string name="server_does_not_support_easy_onboarding_invites">El servidor no admet la generació d\'invitacions</string>
<string name="no_active_accounts_support_this">Cap compte actiu admet aquesta funció</string> <string name="no_active_accounts_support_this">Cap compte actiu admet aquesta funció</string>
</resources> </resources>

View File

@ -16,6 +16,8 @@
<string name="action_unblock_contact">Odblokovat kontakt</string> <string name="action_unblock_contact">Odblokovat kontakt</string>
<string name="action_block_domain">Zablokovat doménu</string> <string name="action_block_domain">Zablokovat doménu</string>
<string name="action_unblock_domain">Odblokovat doménu</string> <string name="action_unblock_domain">Odblokovat doménu</string>
<string name="action_block_participant">Blokovat účastníka</string>
<string name="action_unblock_participant">Odblokovat účatníka</string>
<string name="title_activity_manage_accounts">Nastavení účtů</string> <string name="title_activity_manage_accounts">Nastavení účtů</string>
<string name="title_activity_settings">Nastavení</string> <string name="title_activity_settings">Nastavení</string>
<string name="title_activity_sharewith">Sdílet s konverzací</string> <string name="title_activity_sharewith">Sdílet s konverzací</string>
@ -27,10 +29,24 @@
<string name="just_now">právě teď</string> <string name="just_now">právě teď</string>
<string name="minute_ago">před minutou</string> <string name="minute_ago">před minutou</string>
<string name="minutes_ago">před %d minutami</string> <string name="minutes_ago">před %d minutami</string>
<plurals name="x_unread_conversations">
<item quantity="one">%d nepřečtená konverzace</item>
<item quantity="few">%d nepřečtené konverzace</item>
<item quantity="many">%d nepřečtených konverzací</item>
<item quantity="other">%d nepřečtených konverzací</item>
</plurals>
<string name="sending">odesílám…</string> <string name="sending">odesílám…</string>
<string name="message_decrypting">Dešifrování zprávy. Chvíli strpení...</string> <string name="message_decrypting">Dešifrování zprávy. Chvíli strpení</string>
<string name="pgp_message">OpenPGP šifrovaná zpráva</string> <string name="pgp_message">OpenPGP šifrovaná zpráva</string>
<string name="nick_in_use">Přezdívka se již používá</string> <string name="nick_in_use">Přezdívka se již používá</string>
<string name="invalid_muc_nick">Neplatná přezdívka</string>
<string name="admin">Administrátor</string> <string name="admin">Administrátor</string>
<string name="owner">Vlastník</string> <string name="owner">Vlastník</string>
<string name="moderator">Moderátor</string> <string name="moderator">Moderátor</string>
@ -41,12 +57,12 @@
<string name="unblock_contact_text">Chcete odblokovat příjem zpráv od %s?</string> <string name="unblock_contact_text">Chcete odblokovat příjem zpráv od %s?</string>
<string name="block_domain_text">Zablokovat všechny kontakty z %s?</string> <string name="block_domain_text">Zablokovat všechny kontakty z %s?</string>
<string name="unblock_domain_text">Odblokovat všechny kontakty z %s?</string> <string name="unblock_domain_text">Odblokovat všechny kontakty z %s?</string>
<string name="contact_blocked">Kontakty zablokovány</string> <string name="contact_blocked">Kontakt zablokován</string>
<string name="blocked">Zablokovaný</string> <string name="blocked">Zablokovaný</string>
<string name="remove_bookmark_text">Přejete si odstranit %s ze záložek? Předešlé rozhovory pod záložkou nebudou odstraněny.</string> <string name="remove_bookmark_text">Přejete si odstranit %s ze záložek? Předešlé rozhovory pod záložkou nebudou odstraněny.</string>
<string name="register_account">Registrovat nový účet na serveru</string> <string name="register_account">Registrovat nový účet na serveru</string>
<string name="change_password_on_server">Změnit heslo na serveru</string> <string name="change_password_on_server">Změnit heslo na serveru</string>
<string name="share_with">Sdílet s...</string> <string name="share_with">Sdílet s</string>
<string name="start_conversation">Začít konverzaci</string> <string name="start_conversation">Začít konverzaci</string>
<string name="invite_contact">Pozvat kontakt</string> <string name="invite_contact">Pozvat kontakt</string>
<string name="invite">Pozvat</string> <string name="invite">Pozvat</string>
@ -61,21 +77,26 @@
<string name="unblock">Odblokovat</string> <string name="unblock">Odblokovat</string>
<string name="save">Uložit</string> <string name="save">Uložit</string>
<string name="ok">OK</string> <string name="ok">OK</string>
<string name="crash_report_title">%1$s přestal reagovat</string>
<string name="crash_report_message">Zasíláním detailů o důvodu selhání z Vašeho XMPP účtu pomůžete dalšímu vývoji %1$s.</string>
<string name="send_now">Odeslat teď</string> <string name="send_now">Odeslat teď</string>
<string name="send_never">Již se neptat</string> <string name="send_never">Již se neptat</string>
<string name="problem_connecting_to_account">Nelze se připojit k účtu</string> <string name="problem_connecting_to_account">Nelze se připojit k účtu</string>
<string name="problem_connecting_to_accounts">Nebylo možné se připojit k několika účtům</string>
<string name="touch_to_fix">Ťukněte pro nastavení účtů</string>
<string name="attach_file">Přiložit soubor</string> <string name="attach_file">Přiložit soubor</string>
<string name="not_in_roster">Přidat chybějící kontakt do seznamu kontaktů?</string> <string name="not_in_roster">Přidat chybějící kontakt do seznamu kontaktů?</string>
<string name="add_contact">Přidat kontakt</string> <string name="add_contact">Přidat kontakt</string>
<string name="send_failed">doručení selhalo</string> <string name="send_failed">doručení selhalo</string>
<string name="preparing_image">Připravuji odeslání obrázku</string> <string name="preparing_image">Připravuji odeslání obrázku</string>
<string name="preparing_images">Připravuji odeslání obrázků</string> <string name="preparing_images">Připravuji odeslání obrázků</string>
<string name="sharing_files_please_wait">Sdílení souborů. Chvíli strpení...</string> <string name="sharing_files_please_wait">Sdílení souborů. Chvíli strpení</string>
<string name="action_clear_history">Smazat historii</string> <string name="action_clear_history">Smazat historii</string>
<string name="clear_conversation_history">Smaže historii konverzací</string> <string name="clear_conversation_history">Smaže historii konverzací</string>
<string name="clear_histor_msg">Opravdu chcete smazat všechny zprávy v této konverzace?\n\n<b>Varování</b>Toto neovlivní zprávy uložené na jiných zařízeních či serverech.</string> <string name="clear_histor_msg">Opravdu chcete smazat všechny zprávy v této konverzace?\n\n<b>Varování</b>Toto neovlivní zprávy uložené na jiných zařízeních či serverech.</string>
<string name="delete_file_dialog">Smazat soubor</string> <string name="delete_file_dialog">Smazat soubor</string>
<string name="delete_file_dialog_msg">Opravdu chcete smazat tento soubor?\n\n<b>Varování</b>Toto neovlivní kopie uložené na jiných zařízeních či serverech.</string> <string name="delete_file_dialog_msg">Opravdu chcete smazat tento soubor?\n\n<b>Varování</b>Toto neovlivní kopie uložené na jiných zařízeních či serverech.</string>
<string name="also_end_conversation">Poté zavřít tuto konverzaci</string>
<string name="choose_presence">Vybrat přístroj</string> <string name="choose_presence">Vybrat přístroj</string>
<string name="send_unencrypted_message">Odeslat nešifrovanou zprávu</string> <string name="send_unencrypted_message">Odeslat nešifrovanou zprávu</string>
<string name="send_message">Odeslat zprávu</string> <string name="send_message">Odeslat zprávu</string>
@ -83,16 +104,20 @@
<string name="send_omemo_message">Poslat OMEMO šifrovanou zprávu</string> <string name="send_omemo_message">Poslat OMEMO šifrovanou zprávu</string>
<string name="send_omemo_x509_message">Odeslat v\\OMEMO šifrovanou zprávu</string> <string name="send_omemo_x509_message">Odeslat v\\OMEMO šifrovanou zprávu</string>
<string name="send_pgp_message">Poslat OpenPGP šifrovanou zprávu</string> <string name="send_pgp_message">Poslat OpenPGP šifrovanou zprávu</string>
<string name="your_nick_has_been_changed">Přezdívka změněna</string>
<string name="send_unencrypted">Poslat nešifrované</string> <string name="send_unencrypted">Poslat nešifrované</string>
<string name="decryption_failed">Zašifrování se nezdařilo. Možná nemáte správný privátní klíč.</string> <string name="decryption_failed">Zašifrování se nezdařilo. Možná nemáte správný privátní klíč.</string>
<string name="openkeychain_required">OpenKeychain</string> <string name="openkeychain_required">OpenKeychain</string>
<string name="openkeychain_required_long"><![CDATA[%1$s používá <b>OpenKeychain</b> k šifrování a dešifrování zpráv a ke správě Vašich veřejných klíčů.<br><br>OpenKeychain je vydána pod licencí GPLv3+ a dostupná na F-Droid nebo Google Play. <br><br><small>(Po instalaci, prosím, restartujte%1$s.)</small>]]></string>
<string name="restart">Restartovat</string> <string name="restart">Restartovat</string>
<string name="install">Instalovat</string> <string name="install">Instalovat</string>
<string name="openkeychain_not_installed">Nainstalujte prosím OpenKeychain</string> <string name="openkeychain_not_installed">Nainstalujte prosím OpenKeychain</string>
<string name="offering">nabízí…</string> <string name="offering">nabízí…</string>
<string name="waiting">čekám…</string> <string name="waiting">čekám…</string>
<string name="no_pgp_key">Nebyl nalezen žádný OpenPGP klíč</string> <string name="no_pgp_key">Nebyl nalezen žádný OpenPGP klíč</string>
<string name="contact_has_no_pgp_key">Není možné zašifrovat zprávy, protože kontakt neoznamuje svůj veřejný klíč.\n\n<small>Požádejte kontakt, aby si nastavil OpenPGP.</small></string>
<string name="no_pgp_keys">Nebyly nalezeny žádné OpenPGP klíče</string> <string name="no_pgp_keys">Nebyly nalezeny žádné OpenPGP klíče</string>
<string name="contacts_have_no_pgp_keys">Není možné zašifrovat zprávy, protože kontakty neoznamují svůj veřejný klíč.\n\n<small>Požádejte je, aby si nastavili OpenPGP.</small></string>
<string name="pref_general">Obecné</string> <string name="pref_general">Obecné</string>
<string name="pref_accept_files">Přijímat soubory</string> <string name="pref_accept_files">Přijímat soubory</string>
<string name="pref_accept_files_summary">Automaticky přijímat soubory menší než…</string> <string name="pref_accept_files_summary">Automaticky přijímat soubory menší než…</string>
@ -103,13 +128,19 @@
<string name="pref_led">LED upozornění</string> <string name="pref_led">LED upozornění</string>
<string name="pref_led_summary">Blikat při přijetí nové zprávy</string> <string name="pref_led_summary">Blikat při přijetí nové zprávy</string>
<string name="pref_ringtone">Vyzváněcí tón</string> <string name="pref_ringtone">Vyzváněcí tón</string>
<string name="pref_notification_sound">Zvuk upozornění</string>
<string name="pref_notification_sound_summary">Zvuk upozornění na nové zprávy</string>
<string name="pref_call_ringtone_summary">Vyzváněcí tón pro příchozí hovory</string>
<string name="pref_notification_grace_period">Časová lhůta</string> <string name="pref_notification_grace_period">Časová lhůta</string>
<string name="pref_notification_grace_period_summary">Časová lhůta po kterou bude Conversations v tichém režimu při zaznamenání aktivity na jiném přístroji</string> <string name="pref_notification_grace_period_summary">Časová lhůta po kterou bude Conversations v tichém režimu při zaznamenání aktivity na jiném přístroji</string>
<string name="pref_advanced_options">Rozšířené</string> <string name="pref_advanced_options">Rozšířené</string>
<string name="pref_never_send_crash">Neodesílat detaily o pádu aplikace</string> <string name="pref_never_send_crash">Neodesílat detaily o pádu aplikace</string>
<string name="pref_never_send_crash_summary">Zasíláním detailů o důvodu selhání pomůžete dalšímu vývoji</string>
<string name="pref_confirm_messages">Potvrzovat zprávy</string> <string name="pref_confirm_messages">Potvrzovat zprávy</string>
<string name="pref_confirm_messages_summary">Nechat kontaky vědět kdy jste dostali a přečetli jejich zprávy</string> <string name="pref_confirm_messages_summary">Nechat kontaky vědět kdy jste dostali a přečetli jejich zprávy</string>
<string name="pref_ui_options">UI</string> <string name="pref_ui_options">UI</string>
<string name="openpgp_error">Chyba OpenKeychain.</string>
<string name="bad_key_for_encryption">Chybný klíč pro šifrování.</string>
<string name="accept">Přijmout</string> <string name="accept">Přijmout</string>
<string name="error">Došlo k chybě</string> <string name="error">Došlo k chybě</string>
<string name="recording_error">Chyba</string> <string name="recording_error">Chyba</string>
@ -121,8 +152,10 @@
<string name="attach_take_picture">Vyfotit obrázek</string> <string name="attach_take_picture">Vyfotit obrázek</string>
<string name="preemptively_grant">Aktivně povolovat vyžádání změn stavu</string> <string name="preemptively_grant">Aktivně povolovat vyžádání změn stavu</string>
<string name="error_not_an_image_file">Vybraný soubor není obrázek</string> <string name="error_not_an_image_file">Vybraný soubor není obrázek</string>
<string name="error_compressing_image">Nebylo možné převést obrázek</string>
<string name="error_file_not_found">Soubor nenalezen</string> <string name="error_file_not_found">Soubor nenalezen</string>
<string name="error_io_exception">Obecná I/O chyba. Že by již nebylo volné místo?</string> <string name="error_io_exception">Obecná I/O chyba. Že by již nebylo volné místo?</string>
<string name="error_security_exception_during_image_copy">Aplikace, kterou jste použil(a) k výběru obrázku, neposkytla dostatečná oprávnění ke čtení souboru.\n\n<small>Použijte jiného správce souborů k výběru obrázku</small>.</string>
<string name="account_status_unknown">Neznámý</string> <string name="account_status_unknown">Neznámý</string>
<string name="account_status_disabled">Dočasně vypnuto</string> <string name="account_status_disabled">Dočasně vypnuto</string>
<string name="account_status_online">Online</string> <string name="account_status_online">Online</string>
@ -134,9 +167,13 @@
<string name="account_status_regis_fail">Registrace selhala</string> <string name="account_status_regis_fail">Registrace selhala</string>
<string name="account_status_regis_conflict">Uživatelské jméno se již používá</string> <string name="account_status_regis_conflict">Uživatelské jméno se již používá</string>
<string name="account_status_regis_success">Registrace dokončena</string> <string name="account_status_regis_success">Registrace dokončena</string>
<string name="account_status_regis_not_sup">Registrace není podporována serverem</string>
<string name="account_status_regis_invalid_token">Chybný registrační token</string>
<string name="account_status_tls_error">Vyjednávání TLS selhalo</string>
<string name="account_status_policy_violation">Porušení podmínek</string> <string name="account_status_policy_violation">Porušení podmínek</string>
<string name="account_status_incompatible_server">Nekompatibilní server</string> <string name="account_status_incompatible_server">Nekompatibilní server</string>
<string name="account_status_stream_error">Chyba přenosu</string> <string name="account_status_stream_error">Chyba přenosu</string>
<string name="account_status_stream_opening_error">Chyba při otevírání proudu</string>
<string name="encryption_choice_unencrypted">Nešifrováno</string> <string name="encryption_choice_unencrypted">Nešifrováno</string>
<string name="encryption_choice_otr">OTR</string> <string name="encryption_choice_otr">OTR</string>
<string name="encryption_choice_pgp">OpenPGP</string> <string name="encryption_choice_pgp">OpenPGP</string>
@ -145,13 +182,19 @@
<string name="mgmt_account_disable">Dočasně vypnout</string> <string name="mgmt_account_disable">Dočasně vypnout</string>
<string name="mgmt_account_publish_avatar">Zveřejnit avatar</string> <string name="mgmt_account_publish_avatar">Zveřejnit avatar</string>
<string name="mgmt_account_publish_pgp">Zveřejnit OpenPGP klíč</string> <string name="mgmt_account_publish_pgp">Zveřejnit OpenPGP klíč</string>
<string name="unpublish_pgp">Odstranit veřejný klíč OpenPGP</string>
<string name="unpublish_pgp_message">Skutečně chcete odstranit Váš současný veřejný OpenPGP klíč?\nVaše kontakty Vám nebudou moci nadále posílat zprávy šifrované pomocí OpenPGP. </string>
<string name="openpgp_has_been_published">OpenPGP veřejný klíč zveřejněn.</string> <string name="openpgp_has_been_published">OpenPGP veřejný klíč zveřejněn.</string>
<string name="mgmt_account_enable">Povolit účet</string> <string name="mgmt_account_enable">Povolit účet</string>
<string name="mgmt_account_are_you_sure">Jste si jisti?</string> <string name="mgmt_account_are_you_sure">Jste si jisti?</string>
<string name="mgmt_account_delete_confirm_text">Smazáním Vašeho účtu dojde k vymazání celé Vaší historie konverzací. </string>
<string name="attach_record_voice">Nahrát hlas</string> <string name="attach_record_voice">Nahrát hlas</string>
<string name="account_settings_jabber_id">Adresa XMPP</string> <string name="account_settings_jabber_id">Adresa XMPP</string>
<string name="block_jabber_id">Blokovat XMPP adresu</string>
<string name="account_settings_example_jabber_id">jmeno@server.cz</string> <string name="account_settings_example_jabber_id">jmeno@server.cz</string>
<string name="password">Heslo</string> <string name="password">Heslo</string>
<string name="invalid_jid">Toto není platná XMPP adresa</string>
<string name="error_out_of_memory">Nedostatek paměti. Obrázek je příliš velký</string>
<string name="add_phone_book_text">Chcete přidat %s do svého adresáře?</string> <string name="add_phone_book_text">Chcete přidat %s do svého adresáře?</string>
<string name="server_info_show_more">Údaje serveru</string> <string name="server_info_show_more">Údaje serveru</string>
<string name="server_info_mam">XEP-0313: MAM</string> <string name="server_info_mam">XEP-0313: MAM</string>
@ -178,9 +221,11 @@
<string name="openpgp_key_id">OpenPGP ID klíče</string> <string name="openpgp_key_id">OpenPGP ID klíče</string>
<string name="omemo_fingerprint">OMEMO otisk</string> <string name="omemo_fingerprint">OMEMO otisk</string>
<string name="omemo_fingerprint_x509">v\\OMEMO otisk</string> <string name="omemo_fingerprint_x509">v\\OMEMO otisk</string>
<string name="omemo_fingerprint_selected_message">OMEMO otisk (původce zprávy)</string>
<string name="omemo_fingerprint_x509_selected_message">v\\OMEMO otisk (původce zprávy)</string>
<string name="other_devices">Ostatní přístroje</string> <string name="other_devices">Ostatní přístroje</string>
<string name="trust_omemo_fingerprints">Věřit OMEMO otiskům</string> <string name="trust_omemo_fingerprints">Věřit OMEMO otiskům</string>
<string name="fetching_keys">Získávání klíčů...</string> <string name="fetching_keys">Získávání klíčů</string>
<string name="done">Hotovo</string> <string name="done">Hotovo</string>
<string name="decrypt">Dešifrovat</string> <string name="decrypt">Dešifrovat</string>
<string name="bookmarks">Záložky</string> <string name="bookmarks">Záložky</string>
@ -198,56 +243,99 @@
<string name="channel_bare_jid_example">kanál@konference.server.cz</string> <string name="channel_bare_jid_example">kanál@konference.server.cz</string>
<string name="save_as_bookmark">Uložit jako záložku</string> <string name="save_as_bookmark">Uložit jako záložku</string>
<string name="delete_bookmark">Smazat záložku</string> <string name="delete_bookmark">Smazat záložku</string>
<string name="destroy_room">Zrušit skupinový chat</string>
<string name="destroy_channel">Zrušit kanál</string> <string name="destroy_channel">Zrušit kanál</string>
<string name="destroy_room_dialog">Skutečně chcete zrušit skupinový chat?\n\n<b>Varování:</b> Skupinový chat bude zcela odstraněn ze serveru.</string>
<string name="destroy_channel_dialog">Skutečně chcete zrušit veřejný kanál?\n\n<b>Varování:</b> Kanál bude zcela odstraněn ze serveru.</string>
<string name="could_not_destroy_room">Nebylo možné zrušit skupinový chat</string>
<string name="could_not_destroy_channel">Nebylo možné zrušit kanál</string>
<string name="action_edit_subject">Upravit předmět skupinového chatu</string>
<string name="topic">Téma</string>
<string name="joining_conference">Připojuji se ke skupinovému chatu…</string>
<string name="leave">Odejít</string> <string name="leave">Odejít</string>
<string name="contact_added_you">Kontakt přidán do seznamu</string> <string name="contact_added_you">Kontakt přidán do seznamu</string>
<string name="add_back">Opět přidat</string> <string name="add_back">Opět přidat</string>
<string name="contact_has_read_up_to_this_point">%s dočetl(a) až sem</string> <string name="contact_has_read_up_to_this_point">%s dočetl(a) až sem</string>
<string name="contacts_have_read_up_to_this_point">%s dočetli až sem</string>
<string name="contacts_and_n_more_have_read_up_to_this_point">%1$s +%2$d ostatní(ch) dočetli až sem</string>
<string name="everyone_has_read_up_to_this_point">Všichni dočetli až sem</string>
<string name="publish">Zveřejnit</string> <string name="publish">Zveřejnit</string>
<string name="touch_to_choose_picture">Ťuknutím na avatar vyberete obrázek z galerie</string>
<string name="publishing">Zveřejňuji…</string> <string name="publishing">Zveřejňuji…</string>
<string name="error_publish_avatar_server_reject">Server odmítl toto zveřejnění</string> <string name="error_publish_avatar_server_reject">Server odmítl toto zveřejnění</string>
<string name="error_publish_avatar_converting">Nebylo možné převést Váš obrázek</string>
<string name="error_saving_avatar">Nepodařilo se uložit avatar na disk</string> <string name="error_saving_avatar">Nepodařilo se uložit avatar na disk</string>
<string name="or_long_press_for_default">(Stisknout dlouze pro obnovení výchozího stavu)</string> <string name="or_long_press_for_default">(Stisknout dlouze pro obnovení výchozího stavu)</string>
<string name="error_publish_avatar_no_server_support">Váš server nepodporuje zveřejňování avataru</string>
<string name="private_message">šeptem</string> <string name="private_message">šeptem</string>
<string name="private_message_to">pro %s</string> <string name="private_message_to">pro %s</string>
<string name="send_private_message_to">Zaslat soukromou zprávu pro %s</string> <string name="send_private_message_to">Zaslat soukromou zprávu pro %s</string>
<string name="connect">Připojit</string> <string name="connect">Připojit</string>
<string name="account_already_exists">Tento účet již existuje</string> <string name="account_already_exists">Tento účet již existuje</string>
<string name="next">Další</string> <string name="next">Další</string>
<string name="server_info_session_established">Sezení vytvořeno</string>
<string name="skip">Přeskočit</string> <string name="skip">Přeskočit</string>
<string name="disable_notifications">Vypnout upozornění</string> <string name="disable_notifications">Vypnout upozornění</string>
<string name="enable">Povolit</string> <string name="enable">Povolit</string>
<string name="conference_requires_password">Požadováno heslo ke skupinovému chatu</string>
<string name="enter_password">Vložit heslo</string> <string name="enter_password">Vložit heslo</string>
<string name="request_presence_updates">Nejdříve, prosím, od kontaktu vyžádejte zasílání informací o změně stavu.\n\n<small>To bude využito k identifikaci aplikace, kterou kontakt používá.</small></string>
<string name="request_now">Ihned vyžádat</string> <string name="request_now">Ihned vyžádat</string>
<string name="ignore">Ignorovat</string> <string name="ignore">Ignorovat</string>
<string name="without_mutual_presence_updates"><b>Varování:</b> Odeslání bez povolení vzájemného informování o změně stavu může způsobit nečekané potíže.\n\n<small>Jděte do \"Detaily kontaktu\" a ověřte nastavení aktualizace stavu.</small></string>
<string name="pref_security_settings">Zabezpečení</string> <string name="pref_security_settings">Zabezpečení</string>
<string name="pref_allow_message_correction">Povolit opravu zpráv</string> <string name="pref_allow_message_correction">Povolit opravu zpráv</string>
<string name="pref_allow_message_correction_summary">Povolí kontaktům zpětné upravování jejich zpráv</string> <string name="pref_allow_message_correction_summary">Povolí kontaktům zpětné upravování jejich zpráv</string>
<string name="pref_expert_options">Expertní nastavení</string> <string name="pref_expert_options">Expertní nastavení</string>
<string name="pref_expert_options_summary">S tímto zacházejte velmi opatrně</string> <string name="pref_expert_options_summary">S tímto zacházejte velmi opatrně</string>
<string name="title_activity_about_x">O %s</string>
<string name="title_pref_quiet_hours">Tichý režim</string> <string name="title_pref_quiet_hours">Tichý režim</string>
<string name="title_pref_quiet_hours_start_time">Odkdy</string> <string name="title_pref_quiet_hours_start_time">Odkdy</string>
<string name="title_pref_quiet_hours_end_time">Dokdy</string> <string name="title_pref_quiet_hours_end_time">Dokdy</string>
<string name="title_pref_enable_quiet_hours">Povolit tichý režim</string> <string name="title_pref_enable_quiet_hours">Povolit tichý režim</string>
<string name="pref_quiet_hours_summary">Upozornění budou během tichého režimu ztlumena</string> <string name="pref_quiet_hours_summary">Upozornění budou během tichého režimu ztlumena</string>
<string name="pref_expert_options_other">Další</string> <string name="pref_expert_options_other">Další</string>
<string name="pref_autojoin">Synchronizovat se záložkami</string>
<string name="pref_autojoin_summary">Automaticky se připojovat ke skupinovým chatům, pokud jsou nastaveny v záložkách</string>
<string name="toast_message_omemo_fingerprint">OMEMO otisk zkopírován do schránky</string>
<string name="conference_banned">Byl(a) jste blokován(a) v této skupině</string>
<string name="conference_members_only">Tento skupinový chat je pouze pro registrované členy</string>
<string name="conference_resource_constraint">Omezení zdrojů</string>
<string name="conference_kicked">Byl(a) jste vyloučen(a) z této skupiny</string>
<string name="conference_shutdown">Skupinový chat byl ukončen</string>
<string name="conference_unknown_error">Již nejste členem tohoto skupinového chatu</string>
<string name="using_account">za použití účtu %s</string> <string name="using_account">za použití účtu %s</string>
<string name="hosted_on">hostován na %s</string>
<string name="checking_x">Ověřuji %s na HTTP hostiteli</string> <string name="checking_x">Ověřuji %s na HTTP hostiteli</string>
<string name="not_connected_try_again">Bez připojení. Zkus znovu později</string> <string name="not_connected_try_again">Bez připojení. Zkus znovu později</string>
<string name="check_x_filesize">Ověřit %s velikost</string> <string name="check_x_filesize">Ověřit %s velikost</string>
<string name="check_x_filesize_on_host">Kontrola %1$s velikosti na %2$s</string> <string name="check_x_filesize_on_host">Kontrola %1$s velikosti na %2$s</string>
<string name="message_options">Možnosti zpráv</string> <string name="message_options">Možnosti zpráv</string>
<string name="quote">Citovat</string>
<string name="paste_as_quote">Vložit jako citaci</string>
<string name="copy_original_url">Kopírovat originální URL</string> <string name="copy_original_url">Kopírovat originální URL</string>
<string name="send_again">Poslat znovu</string> <string name="send_again">Poslat znovu</string>
<string name="file_url">URL souboru</string> <string name="file_url">URL souboru</string>
<string name="url_copied_to_clipboard">Adresa URL zkopírována do schránky</string>
<string name="jabber_id_copied_to_clipboard">Adresa XMPP zkopírována do schránky</string>
<string name="error_message_copied_to_clipboard">Chybové hlášení zkopírováno do schránky</string>
<string name="web_address">webová adresa</string>
<string name="scan_qr_code">Skenovat 2D kód</string> <string name="scan_qr_code">Skenovat 2D kód</string>
<string name="show_qr_code">Zobrazit 2D kód</string> <string name="show_qr_code">Zobrazit 2D kód</string>
<string name="show_block_list">Zobrazit seznam blokovaných</string> <string name="show_block_list">Zobrazit seznam blokovaných</string>
<string name="account_details">Detaily účtu</string> <string name="account_details">Detaily účtu</string>
<string name="confirm">Potvrdit</string> <string name="confirm">Potvrdit</string>
<string name="try_again">Zkusit znovu</string> <string name="try_again">Zkusit znovu</string>
<string name="pref_keep_foreground_service">Služba na popředí</string>
<string name="pref_keep_foreground_service_summary">Zamezit operačnímu systému v ukončení připojení</string> <string name="pref_keep_foreground_service_summary">Zamezit operačnímu systému v ukončení připojení</string>
<string name="pref_create_backup">Vytvořit zálohu</string> <string name="pref_create_backup">Vytvořit zálohu</string>
<string name="pref_create_backup_summary">Soubory zálohy budou uloženy do %s</string>
<string name="notification_create_backup_title">Vytvářím soubory zálohy</string>
<string name="notification_backup_created_title">Záloha byla vytvořena</string>
<string name="notification_backup_created_subtitle">Soubory zálohy byly uloženy do %s</string>
<string name="restoring_backup">Obnovuji zálohu</string>
<string name="notification_restored_backup_title">Záloha obnovena</string>
<string name="notification_restored_backup_subtitle">Nezapomeňte povolit účet</string>
<string name="choose_file">Vybrat soubor</string> <string name="choose_file">Vybrat soubor</string>
<string name="receiving_x_file">Přijímám %1$s (%2$d%% dokončeno)</string> <string name="receiving_x_file">Přijímám %1$s (%2$d%% dokončeno)</string>
<string name="download_x_file">Stáhnout %s</string> <string name="download_x_file">Stáhnout %s</string>
@ -255,22 +343,37 @@
<string name="file">soubor</string> <string name="file">soubor</string>
<string name="open_x_file">Otevřít %s</string> <string name="open_x_file">Otevřít %s</string>
<string name="sending_file">odesílám (%1$d%% přeneseno)</string> <string name="sending_file">odesílám (%1$d%% přeneseno)</string>
<string name="preparing_file">Připravuji sdílení souboru</string>
<string name="x_file_offered_for_download">%s nabídnuto ke stažení</string> <string name="x_file_offered_for_download">%s nabídnuto ke stažení</string>
<string name="cancel_transmission">Zrušit přenos</string> <string name="cancel_transmission">Zrušit přenos</string>
<string name="file_transmission_failed">nebylo možné sdílet soubor</string>
<string name="file_transmission_cancelled">přenos souboru byl zrušen</string>
<string name="file_deleted">Soubor byl smazán</string>
<string name="no_application_found_to_open_file">Nebyla nalezena aplikace umožňující otevření souboru</string>
<string name="no_application_found_to_open_link">Nebyla nalezena aplikace umožňující otevření odkazu</string>
<string name="no_application_found_to_view_contact">Nebyla nalezena aplikace umožňující zobrazení kontaktu</string>
<string name="pref_show_dynamic_tags">Dynamické tagy</string>
<string name="pref_show_dynamic_tags_summary">Zobrazit tagy pro čtení pod kontakty</string> <string name="pref_show_dynamic_tags_summary">Zobrazit tagy pro čtení pod kontakty</string>
<string name="enable_notifications">Povolit upozornění</string> <string name="enable_notifications">Povolit upozornění</string>
<string name="no_conference_server_found">Žádný server pro skupinový chat nebyl nalezen</string>
<string name="conference_creation_failed">Nebylo možné vytvořit skupinový chat</string>
<string name="account_image_description">Avatar účtu</string> <string name="account_image_description">Avatar účtu</string>
<string name="copy_omemo_clipboard_description">Zkopírovat OMEMO otisk do schránky</string> <string name="copy_omemo_clipboard_description">Zkopírovat OMEMO otisk do schránky</string>
<string name="regenerate_omemo_key">Znovu vytvořit OMEMO klíč</string> <string name="regenerate_omemo_key">Znovu vytvořit OMEMO klíč</string>
<string name="clear_other_devices">Smazat přístroje</string> <string name="clear_other_devices">Smazat přístroje</string>
<string name="clear_other_devices_desc">Opravdu chcete vymazat ostatní přístroje z OMEMO upozornění? Až se příště tyto přístroje připojí, znovu se ohlásí, ale pravděpodobně neobdrží zprávy odeslané v mezičase mezi přihlášeními.</string>
<string name="error_no_keys_to_trust_server_error">Pro tento kontakt nejsou dostupné žádné použitelné klíče.\nNebylo možné získat nové klíče ze serveru. Možná je něco v nepořádku se serverem kontaktu?</string>
<string name="error_no_keys_to_trust_presence">Pro tento kontakt nejsou dostupné žádné klíče.\nUjistěte se, že oba máte zapnuté zasílání informací o změně stavu.</string>
<string name="error_trustkeys_title">Něco se pokazilo</string>
<string name="fetching_history_from_server">Načíst historii ze serveru</string> <string name="fetching_history_from_server">Načíst historii ze serveru</string>
<string name="no_more_history_on_server">Na serveru není žádná další historie</string> <string name="no_more_history_on_server">Na serveru není žádná další historie</string>
<string name="updating">Aktualizuji...</string> <string name="updating">Aktualizuji</string>
<string name="password_changed">Heslo změněno!</string> <string name="password_changed">Heslo změněno!</string>
<string name="could_not_change_password">Nelze změnit heslo</string> <string name="could_not_change_password">Nelze změnit heslo</string>
<string name="change_password">Změnit heslo</string> <string name="change_password">Změnit heslo</string>
<string name="current_password">Současné heslo</string> <string name="current_password">Současné heslo</string>
<string name="new_password">Nové heslo</string> <string name="new_password">Nové heslo</string>
<string name="password_should_not_be_empty">Heslo nesmí být prázdné</string>
<string name="enable_all_accounts">Povolit všechny účty</string> <string name="enable_all_accounts">Povolit všechny účty</string>
<string name="disable_all_accounts">Vypnout všechny účty</string> <string name="disable_all_accounts">Vypnout všechny účty</string>
<string name="perform_action_with">Provést akci s</string> <string name="perform_action_with">Provést akci s</string>
@ -279,17 +382,35 @@
<string name="outcast">Vyloučený</string> <string name="outcast">Vyloučený</string>
<string name="member">Člen</string> <string name="member">Člen</string>
<string name="advanced_mode">Pokročilý mód</string> <string name="advanced_mode">Pokročilý mód</string>
<string name="grant_membership">Udělit oprávnění člena</string>
<string name="remove_membership">Odebrat oprávnění člena</string>
<string name="grant_admin_privileges">Povolit administrátorská oprávnění</string> <string name="grant_admin_privileges">Povolit administrátorská oprávnění</string>
<string name="remove_admin_privileges">Odebrat administrátorská oprávnění</string> <string name="remove_admin_privileges">Odebrat administrátorská oprávnění</string>
<string name="grant_owner_privileges">Udělit práva vlastníka</string>
<string name="remove_owner_privileges">Odebrat práva vlastníka</string>
<string name="remove_from_room">Odebrat ze skupinového chatu</string>
<string name="remove_from_channel">Odebrat z kanálu</string>
<string name="could_not_change_affiliation">Nelze změnit připojení uživatele %s</string> <string name="could_not_change_affiliation">Nelze změnit připojení uživatele %s</string>
<string name="ban_from_conference">Blokovat ve skupinovém chatu</string>
<string name="ban_from_channel">Blokovat v kanálu</string>
<string name="removing_from_public_conference">Pokoušíte se odstranit %s z veřejného kanálu. Jediný způsob, jak toho docílit, je zablokovat tohoto uživatele navždy.</string>
<string name="ban_now">Vypovědět</string> <string name="ban_now">Vypovědět</string>
<string name="could_not_change_role">Nelze změnit roli uživatele %s</string> <string name="could_not_change_role">Nelze změnit roli uživatele %s</string>
<string name="conference_options">Nastavení soukromých skupinových chatů</string>
<string name="channel_options">Nastavení veřejných kanálů</string>
<string name="members_only">Soukromé, pouze pro členy</string> <string name="members_only">Soukromé, pouze pro členy</string>
<string name="non_anonymous">Ukázat XMPP adresy všem</string>
<string name="moderated">Nastavit kanál jako moderovaný</string>
<string name="you_are_not_participating">Neúčastníte se</string> <string name="you_are_not_participating">Neúčastníte se</string>
<string name="modified_conference_options">Nastavení skupinového chatu změněno!</string>
<string name="could_not_modify_conference_options">Nebylo možné změnit nastavení skupinového chatu</string>
<string name="never">Nikdy</string> <string name="never">Nikdy</string>
<string name="until_further_notice">Než opět změním</string> <string name="until_further_notice">Než opět změním</string>
<string name="reply">Odpovědět</string>
<string name="mark_as_read">Označit jako přečtené</string>
<string name="pref_input_options">Vstup</string> <string name="pref_input_options">Vstup</string>
<string name="pref_enter_is_send">Enter odesílá</string> <string name="pref_enter_is_send">Enter odesílá</string>
<string name="pref_enter_is_send_summary">Odeslat klávesou Enter. Vždy můžete zprávy odeslat pomocí Ctrl+Enter, i když tato možnost není povolena.</string>
<string name="pref_display_enter_key">Zobrazit klávesu enter</string> <string name="pref_display_enter_key">Zobrazit klávesu enter</string>
<string name="pref_display_enter_key_summary">Změnit klávesu emotikon na klávesu enter</string> <string name="pref_display_enter_key_summary">Změnit klávesu emotikon na klávesu enter</string>
<string name="audio">audio</string> <string name="audio">audio</string>
@ -302,12 +423,15 @@
<string name="sending_x_file">Odesílám %s</string> <string name="sending_x_file">Odesílám %s</string>
<string name="offering_x_file">Nabízím %s</string> <string name="offering_x_file">Nabízím %s</string>
<string name="hide_offline">Skrýt offline</string> <string name="hide_offline">Skrýt offline</string>
<string name="contact_is_typing">%s píše...</string> <string name="contact_is_typing">%s píše</string>
<string name="contact_has_stopped_typing">%s přestal(a) psát</string> <string name="contact_has_stopped_typing">%s přestal(a) psát</string>
<string name="contacts_are_typing">%s píší…</string>
<string name="contacts_have_stopped_typing">%s přestali psát</string>
<string name="pref_chat_states">Upozornění při psaní</string> <string name="pref_chat_states">Upozornění při psaní</string>
<string name="pref_chat_states_summary">Nechat kontaky vědět když jim píšete zprávu</string> <string name="pref_chat_states_summary">Nechat kontaky vědět když jim píšete zprávu</string>
<string name="send_location">Poslat pozici</string> <string name="send_location">Poslat pozici</string>
<string name="show_location">Zobrazit pozici</string> <string name="show_location">Zobrazit pozici</string>
<string name="no_application_found_to_display_location">Nebyla nalezena aplikace pro zobrazení pozice</string>
<string name="location">Pozice</string> <string name="location">Pozice</string>
<string name="title_undo_swipe_out_conversation">Conversation zavřena</string> <string name="title_undo_swipe_out_conversation">Conversation zavřena</string>
<string name="pref_dont_trust_system_cas_title">Nedůvěřovat systémovým CA</string> <string name="pref_dont_trust_system_cas_title">Nedůvěřovat systémovým CA</string>
@ -324,6 +448,7 @@
<item quantity="many">%d certifikátů smazáno</item> <item quantity="many">%d certifikátů smazáno</item>
<item quantity="other">%d certifikátů smazáno</item> <item quantity="other">%d certifikátů smazáno</item>
</plurals> </plurals>
<string name="pref_quick_action_summary">Nahradit tlačítko odeslání rychlou akcí</string>
<string name="pref_quick_action">Rychlá akce</string> <string name="pref_quick_action">Rychlá akce</string>
<string name="none">Žádná</string> <string name="none">Žádná</string>
<string name="recently_used">Naposledy použitá</string> <string name="recently_used">Naposledy použitá</string>
@ -331,6 +456,7 @@
<string name="search_contacts">Prohledat kontakty</string> <string name="search_contacts">Prohledat kontakty</string>
<string name="search_bookmarks">Prohledat záložky</string> <string name="search_bookmarks">Prohledat záložky</string>
<string name="send_private_message">Poslat soukromou zprávu</string> <string name="send_private_message">Poslat soukromou zprávu</string>
<string name="user_has_left_conference">%1$s opustil(a) skupinový chat</string>
<string name="username">Uživatelské jméno</string> <string name="username">Uživatelské jméno</string>
<string name="username_hint">Uživatelské jméno</string> <string name="username_hint">Uživatelské jméno</string>
<string name="invalid_username">Toto není platné uživatelské jméno</string> <string name="invalid_username">Toto není platné uživatelské jméno</string>
@ -341,14 +467,25 @@
<string name="account_status_tor_unavailable">Tor síť není dostupná</string> <string name="account_status_tor_unavailable">Tor síť není dostupná</string>
<string name="account_status_bind_failure">Bind chyba</string> <string name="account_status_bind_failure">Bind chyba</string>
<string name="server_info_broken">Rozbité</string> <string name="server_info_broken">Rozbité</string>
<string name="pref_presence_settings">Dostupnost</string>
<string name="pref_away_when_screen_off">Pryč při uzamčení zařízení</string>
<string name="pref_away_when_screen_off_summary">Při uzamčeném zařízení nastaví váš stav na \"pryč\"</string>
<string name="pref_dnd_on_silent_mode">Nedostupný při vypnutém zvuku</string>
<string name="pref_dnd_on_silent_mode_summary">Při ztišeném vyzvánění označí váš stav jako \"nedostupný\"</string>
<string name="pref_treat_vibrate_as_silent">Vibrační mód brát stejně jako tichý</string> <string name="pref_treat_vibrate_as_silent">Vibrační mód brát stejně jako tichý</string>
<string name="pref_treat_vibrate_as_dnd_summary">Při nastavení pouze na vibrace označí váš stav jako \"nedostupný\"</string>
<string name="pref_show_connection_options">Rozšířená nastavení připojení</string> <string name="pref_show_connection_options">Rozšířená nastavení připojení</string>
<string name="pref_show_connection_options_summary">Zobrazovat nastavení hostname a port při vytváření účtu</string> <string name="pref_show_connection_options_summary">Zobrazovat nastavení hostname a port při vytváření účtu</string>
<string name="hostname_example">xmpp.server.cz</string> <string name="hostname_example">xmpp.server.cz</string>
<string name="action_add_account_with_certificate">Přihlásit se pomocí certifikátu</string>
<string name="unable_to_parse_certificate">Nelze analyzovat certifikát</string>
<string name="mam_prefs">Nastavení archivace</string> <string name="mam_prefs">Nastavení archivace</string>
<string name="server_side_mam_prefs">Nastavení archivace na serveru</string> <string name="server_side_mam_prefs">Nastavení archivace na serveru</string>
<string name="fetching_mam_prefs">Získávání nastavení archivace. Chvíli strpení...</string> <string name="fetching_mam_prefs">Získávání nastavení archivace. Chvíli strpení…</string>
<string name="unable_to_fetch_mam_prefs">Nelze získat nastavení archivace</string>
<string name="captcha_required">Vyžadována CAPTCHA</string>
<string name="captcha_hint">Zadejte text z obrázku výše</string> <string name="captcha_hint">Zadejte text z obrázku výše</string>
<string name="jid_does_not_match_certificate">Adresa XMPP nesouhlasí s certifikátem</string>
<string name="action_renew_certificate">Obnovit certifikát</string> <string name="action_renew_certificate">Obnovit certifikát</string>
<string name="error_fetching_omemo_key">Chyba získání OMEMO klíče!</string> <string name="error_fetching_omemo_key">Chyba získání OMEMO klíče!</string>
<string name="verified_omemo_key_with_certificate">OMEMO klíč ověřen certifikátem!</string> <string name="verified_omemo_key_with_certificate">OMEMO klíč ověřen certifikátem!</string>
@ -368,23 +505,45 @@
<item quantity="other">%d zpráv</item> <item quantity="other">%d zpráv</item>
</plurals> </plurals>
<string name="load_more_messages">Načíst více zpráv</string> <string name="load_more_messages">Načíst více zpráv</string>
<string name="shared_file_with_x">Soubor sdílen s %s</string>
<string name="shared_image_with_x">Obrázek sdílen s %s</string>
<string name="shared_images_with_x">Obrázky sdíleny s %s</string>
<string name="shared_text_with_x">Text sdílen s %s</string>
<string name="no_storage_permission">Povolit %1$s přístup k externímu úložišti</string>
<string name="no_camera_permission">Povolit %1$s přístup ke kameře</string>
<string name="sync_with_contacts">Synchronizovat s kontakty</string> <string name="sync_with_contacts">Synchronizovat s kontakty</string>
<string name="sync_with_contacts_long">%1$s požaduje přístup k Vašim kontaktům za účelem spárování s Vašimi XMPP kontakty.\nU kontaktů se pak zobrazí celé jméno a avatar.\n\n%1$s bude kontakty pouze číst a párovat místně v zařízení, aniž by došlo k nahrání těchto dat na server.</string>
<string name="sync_with_contacts_quicksy"><![CDATA[Quicksy požaduje přístup k Vašim kontaktům, aby Vám mohl navrhnout uživatele, kteří jej již používají.<br><br>Tyto kontaktní údaje nebudeme kopírovat a ukládat.\n\nVíce informací najdete v našich <a href="https://quicksy.im/#privacy">zásadách pro ochranu osobních údajů</a>. <br><br>Nyní budete požádáni o udělení přístupu k Vašim kontaktům.]]></string>
<string name="notify_on_all_messages">Upozorňovat na všechny zprávy</string> <string name="notify_on_all_messages">Upozorňovat na všechny zprávy</string>
<string name="notify_only_when_highlighted">Upozornit pouze, když mě někdo zmíní</string>
<string name="notify_never">Upozornění vypnuta</string> <string name="notify_never">Upozornění vypnuta</string>
<string name="notify_paused">Upozornění pozastavena</string> <string name="notify_paused">Upozornění pozastavena</string>
<string name="pref_picture_compression">Komprese obrázků</string>
<string name="pref_picture_compression_summary">Tip: Pokud použijete \"Vybrat soubor\" místo \"Vybrat obrázek\", můžete poslat nekomprimovaný obrázek bez ohledu na toto nastavení. </string>
<string name="always">Vždy</string> <string name="always">Vždy</string>
<string name="large_images_only">Pouze pro velké obrázky</string>
<string name="battery_optimizations_enabled">Povolena optimalizace využití baterie</string> <string name="battery_optimizations_enabled">Povolena optimalizace využití baterie</string>
<string name="battery_optimizations_enabled_explained">Vaše zařízení používá agresivní optimalizaci výdrže baterie pro %1$s, což může vést k opožděným upozorněním nebo dokonce ke ztrátě některých zpráv.\nDoporučujeme optimalizaci vypnout.</string>
<string name="battery_optimizations_enabled_dialog">Vaše zařízení používá agresivní optimalizaci výdrže baterie pro %1$s, což může vést k opožděným upozorněním nebo dokonce ke ztrátě některých zpráv.\nNyní budete vyzváni k jejímu vypnutí.</string>
<string name="disable">Vypnout</string> <string name="disable">Vypnout</string>
<string name="selection_too_large">Vybraný obsah je příliš dlouhý</string> <string name="selection_too_large">Vybraný obsah je příliš dlouhý</string>
<string name="no_accounts">(Žádné aktivované účty)</string> <string name="no_accounts">(Žádné aktivované účty)</string>
<string name="this_field_is_required">Toto pole je vyžadováno</string> <string name="this_field_is_required">Toto pole je vyžadováno</string>
<string name="correct_message">Opravit zprávu</string> <string name="correct_message">Opravit zprávu</string>
<string name="send_corrected_message">Odeslat opravenou zprávu</string> <string name="send_corrected_message">Odeslat opravenou zprávu</string>
<string name="no_keys_just_confirm">Tento osobní otisk byl již bezpečně ověřen. Ťuknutím na \"Hotovo\" pouze potvrzujete, že %s je členem tohoto skupinového chatu.</string>
<string name="this_account_is_disabled">Tento účet byl vypnut</string> <string name="this_account_is_disabled">Tento účet byl vypnut</string>
<string name="share_uri_with">Sdílet URI s...</string> <string name="no_application_to_share_uri">Nebyla nalezena aplikace umožňující sdílení URI</string>
<string name="share_uri_with">Sdílet URI s…</string>
<string name="welcome_text_quicksy"><![CDATA[Quicksy je aplikace odvozená z populárního XMPP klientu Conversations, s funkcí automatického objevování kontaktů.<br><br>Po zadání Vašeho telefonního čísla Vám Quicksy automaticky—na základě čísel ve Vašem telefonním seznamu—navrhne možné kontakty.<br><br>Přihlášením se do služby potvrzujete souhlas s našimi <a href="https://quicksy.im/#privacy">zásadami pro ochranu osobních údajů</a>.]]></string>
<string name="agree_and_continue">Souhlasit a pokračovat</string>
<string name="magic_create_text">Průvodce je nastaven, aby vytvořil účet na serveru conversations.im.¹\nPokud si vyberete conversations.im jako svého poskytovatele, budete moci komunikovat s uživateli u ostatních poskytovatelů, budou-li mít vaši celou XMPP adresu.</string>
<string name="your_full_jid_will_be">Vaše celá XMPP adresa: %s</string>
<string name="create_account">Vytvořit účet</string> <string name="create_account">Vytvořit účet</string>
<string name="use_own_provider">Použít vlastního provozovatele</string> <string name="use_own_provider">Použít vlastního provozovatele</string>
<string name="pick_your_username">Zadejte své uživatelské jméno</string> <string name="pick_your_username">Zadejte své uživatelské jméno</string>
<string name="pref_manually_change_presence">Spravovat viditelnost ručně</string>
<string name="pref_manually_change_presence_summary">Nastavit viditelnost při úpravě statusové zprávy</string>
<string name="status_message">Stavová zpráva</string> <string name="status_message">Stavová zpráva</string>
<string name="presence_chat">Volný pro chat</string> <string name="presence_chat">Volný pro chat</string>
<string name="presence_online">Online</string> <string name="presence_online">Online</string>
@ -396,16 +555,23 @@
<string name="registration_please_wait">Registrace selhala: Zkuste znovu později</string> <string name="registration_please_wait">Registrace selhala: Zkuste znovu později</string>
<string name="registration_password_too_weak">Registrace selhala: Příliš slabé heslo</string> <string name="registration_password_too_weak">Registrace selhala: Příliš slabé heslo</string>
<string name="choose_participants">Vybrat účastníky</string> <string name="choose_participants">Vybrat účastníky</string>
<string name="creating_conference">Vytvářím skupinový chat…</string>
<string name="invite_again">Pozvat znovu</string> <string name="invite_again">Pozvat znovu</string>
<string name="gp_disable">Vypnout</string> <string name="gp_disable">Vypnout</string>
<string name="gp_short">Krátký</string> <string name="gp_short">Krátký</string>
<string name="gp_medium">Střední</string> <string name="gp_medium">Střední</string>
<string name="gp_long">Dlouhý</string> <string name="gp_long">Dlouhý</string>
<string name="pref_broadcast_last_activity">Informovat o používání</string>
<string name="pref_broadcast_last_activity_summary">Tato možnost dává vědět Vašim kontaktům, kdy používáte Conversations</string>
<string name="pref_privacy">Soukromí</string> <string name="pref_privacy">Soukromí</string>
<string name="pref_theme_options">Vzhled</string> <string name="pref_theme_options">Vzhled</string>
<string name="pref_theme_options_summary">Vybrat paletu barev</string> <string name="pref_theme_options_summary">Vybrat paletu barev</string>
<string name="pref_theme_automatic">Automaticky</string>
<string name="pref_theme_light">Světlý vzhled</string>
<string name="pref_theme_dark">Tmavý vzhled</string>
<string name="pref_use_green_background">Zelené pozadí</string> <string name="pref_use_green_background">Zelené pozadí</string>
<string name="pref_use_green_background_summary">Použít zelené pozadí pro přijaté zprávy</string> <string name="pref_use_green_background_summary">Použít zelené pozadí pro přijaté zprávy</string>
<string name="unable_to_connect_to_keychain">Nelze se spojit s OpenKeychain</string>
<string name="this_device_is_no_longer_in_use">Tento přístoj již není používán</string> <string name="this_device_is_no_longer_in_use">Tento přístoj již není používán</string>
<string name="type_pc">Počítač</string> <string name="type_pc">Počítač</string>
<string name="type_phone">Mobil</string> <string name="type_phone">Mobil</string>
@ -413,38 +579,389 @@
<string name="type_web">Prohlížeč</string> <string name="type_web">Prohlížeč</string>
<string name="type_console">Konzole</string> <string name="type_console">Konzole</string>
<string name="payment_required">Vyžadována platba</string> <string name="payment_required">Vyžadována platba</string>
<string name="missing_internet_permission">Udělte povolení pro přístup na Internet</string>
<string name="me"></string> <string name="me"></string>
<string name="contact_asks_for_presence_subscription">Kontakt žádá informace o změnách stavu</string> <string name="contact_asks_for_presence_subscription">Kontakt žádá informace o změnách stavu</string>
<string name="allow">Povolit</string> <string name="allow">Povolit</string>
<string name="no_permission_to_access_x">Chybí oprávnění přistupovat k %s</string> <string name="no_permission_to_access_x">Chybí oprávnění přistupovat k %s</string>
<string name="remote_server_not_found">Vzdálený server nebyl nalezen</string> <string name="remote_server_not_found">Vzdálený server nebyl nalezen</string>
<string name="remote_server_timeout">Vypršel čas spojení se vzdáleným serverem</string>
<string name="unable_to_update_account">Nelze aktualizovat účet</string>
<string name="report_jid_as_spammer">Nahlásit tuto XMPP adresu kvůli odesílání spamu.</string>
<string name="pref_delete_omemo_identities">Smazat OMEMO identity</string> <string name="pref_delete_omemo_identities">Smazat OMEMO identity</string>
<string name="pref_delete_omemo_identities_summary">Znovu vygenerovat OMEMO klíče. Vyžaduje potvrzení od všech vašich kontaktů. Použijte pouze jako poslední řešení.</string>
<string name="delete_selected_keys">Smazat vybrané klíče</string> <string name="delete_selected_keys">Smazat vybrané klíče</string>
<string name="error_publish_avatar_offline">Pro zveřejnění svého avatara musíte být online.</string> <string name="error_publish_avatar_offline">Pro zveřejnění svého avatara musíte být online.</string>
<string name="show_error_message">Zobrazit chybovou zprávu</string> <string name="show_error_message">Zobrazit chybovou zprávu</string>
<string name="error_message">Chybová zpráva</string> <string name="error_message">Chybová zpráva</string>
<string name="data_saver_enabled">Zapnuta úspora dat</string> <string name="data_saver_enabled">Zapnuta úspora dat</string>
<string name="data_saver_enabled_explained">Váš operační systém zabraňuje aplikaci %1$s v přístupu na Internet, pokud tato běží na pozadí. Pro příjem upozornění na nové zprávy musíte %1$s povolit neomezený přístup při zapnuté úspoře dat.\n%1$s se bude i přesto snažit omezovat přenos dat.</string>
<string name="device_does_not_support_data_saver">Tento přístroj nepodporuje vypnutí úspory dat pro aplikaci %1$s.</string>
<string name="error_unable_to_create_temporary_file">Nebylo možné vytvořit dočasný soubor</string>
<string name="this_device_has_been_verified">Tento přístroj byl ověřen</string> <string name="this_device_has_been_verified">Tento přístroj byl ověřen</string>
<string name="copy_fingerprint">Kopírovat identifikátor</string> <string name="copy_fingerprint">Kopírovat identifikátor</string>
<string name="all_omemo_keys_have_been_verified">Oveřil(a) jste všechny OMEMO klíče, které vlastníte.</string>
<string name="barcode_does_not_contain_fingerprints_for_this_conversation">Kód neobsahuje otisk pro tuto konverzaci.</string>
<string name="verified_fingerprints">Ověřené otisky</string>
<string name="use_camera_icon_to_scan_barcode">Naskenovat kód kontaktu pomocí fotoaparátu</string>
<string name="please_wait_for_keys_to_be_fetched">Prosím, počkejte na získání klíčů</string>
<string name="share_as_barcode">Sdílet jako čárový kód</string> <string name="share_as_barcode">Sdílet jako čárový kód</string>
<string name="share_as_uri">Sdílet jako XMPP URI</string> <string name="share_as_uri">Sdílet jako XMPP URI</string>
<string name="share_as_http">Sdílet jako HTTP odkaz</string> <string name="share_as_http">Sdílet jako HTTP odkaz</string>
<string name="pref_blind_trust_before_verification">Slepě důvěřovat před ověřením</string>
<string name="pref_blind_trust_before_verification_summary">Důvěřovat novým zařízením neověřených kontaktů, ale požadovat ruční potvrzení nových zařízení u ověřených kontaktů.</string>
<string name="not_trusted">Nedůvěryhodný</string>
<string name="invalid_barcode">Neplatný 2D kód</string> <string name="invalid_barcode">Neplatný 2D kód</string>
<string name="pref_clean_cache_summary">Vyčistit složku dočasných souborů (užitých aplikací fotoaparátu)</string>
<string name="pref_clean_cache">Vyčistit dočasné soubory</string>
<string name="pref_clean_private_storage">Vyčistit soukromé úložiště</string>
<string name="pref_clean_private_storage_summary">Vyčistit úložiště souborů (Mohou být znovu staženy ze serveru)</string>
<string name="i_followed_this_link_from_a_trusted_source">Tento odkaz pochází z důvěryhodného zdroje</string>
<string name="verifying_omemo_keys_trusted_source">Kliknutím na odkaz se chystáte ověřit OMEMO klíče patřící %1$s. To je bezpečné jedině tehdy, pokud jste odkaz získali z důvěryhodného zdroje, kdy pouze %2$s mohl tento odkaz zveřejnit.</string>
<string name="verify_omemo_keys">Ověřit OMEMO klíče</string>
<string name="show_inactive_devices">Zobrazit neaktivní</string>
<string name="hide_inactive_devices">Skrýt neaktivní</string>
<string name="distrust_omemo_key">Odebrat z důvěryhodných</string>
<string name="distrust_omemo_key_text">Jste si jisti, že chcete odebrat ověření tomuto zařízení?\nZařízení a příchozí zprávy z něj budou označeny jako \"Nedůvěryhodné\".</string>
<plurals name="seconds">
<item quantity="one">%d vteřina</item>
<item quantity="few">%d vteřiny</item>
<item quantity="many">%d vteřin</item>
<item quantity="other">%d vteřin</item>
</plurals>
<plurals name="minutes">
<item quantity="one">%d minuta</item>
<item quantity="few">%d minut</item>
<item quantity="many">%d minut</item>
<item quantity="other">%d minut</item>
</plurals>
<plurals name="hours">
<item quantity="one">%d hodina</item>
<item quantity="few">%d hodiny</item>
<item quantity="many">%d hodin</item>
<item quantity="other">%d hodin</item>
</plurals>
<plurals name="days">
<item quantity="one">%d den</item>
<item quantity="few">%d dny</item>
<item quantity="many">%d dnů</item>
<item quantity="other">%d dnů</item>
</plurals>
<plurals name="weeks">
<item quantity="one">%d týden</item>
<item quantity="few">%d týdny</item>
<item quantity="many">%d týdnů</item>
<item quantity="other">%d týdnů</item>
</plurals>
<plurals name="months">
<item quantity="one">%d měsíc</item>
<item quantity="few">%d měsíce</item>
<item quantity="many">%d měsíců</item>
<item quantity="other">%d měsíců</item>
</plurals>
<string name="pref_automatically_delete_messages">Automatické mazání zpráv</string>
<string name="pref_automatically_delete_messages_description">Automaticky z tohoto zařízení mazat zprávy, které jsou starší, než je nastaveno.</string>
<string name="encrypting_message">Šifruji zprávu</string>
<string name="transcoding_video">Komprimuji video</string>
<string name="corresponding_conversations_closed">Odpovídající konverzace uzavřena.</string>
<string name="contact_blocked_past_tense">Kontakt zablokován.</string>
<string name="pref_notifications_from_strangers">Upozornění od neznámých</string>
<string name="pref_notifications_from_strangers_summary">Upozornit na zprávy a hovory od neznámých kontaktů.</string>
<string name="received_message_from_stranger">Přijata zpráva od neznámého kontaktu</string>
<string name="block_stranger">Zablokovat neznámý kontakt</string>
<string name="block_entire_domain">Zablokovat celou doménu</string>
<string name="online_right_now">právě teď online</string>
<string name="retry_decryption">Zkusit znovu dešifrovat</string>
<string name="session_failure">Chyba sezení</string>
<string name="sasl_downgrade">Degradovaný SASL mechanismus</string>
<string name="account_status_regis_web">Server požaduje registraci přes webovou stránku</string>
<string name="open_website">Otevřít webovou stránku</string>
<string name="application_found_to_open_website">Nebyla nalezena aplikace umožňující otevření webové stránky</string>
<string name="pref_headsup_notifications">Heads-up upozornění</string>
<string name="pref_headsup_notifications_summary">Zobrazit heads-up upozornění</string>
<string name="today">Dnes</string>
<string name="yesterday">Včera</string>
<string name="pref_validate_hostname">Ověřit název hostitele pomocí DNSSEC</string>
<string name="pref_validate_hostname_summary">Certifikáty serverů obsahující ověřený název hostitele jsou považovány za ověřené</string>
<string name="certificate_does_not_contain_jid">Certifikát neobsahuje XMPP adresu</string>
<string name="server_info_partial">částečný</string>
<string name="attach_record_video">Nahrát video</string>
<string name="copy_to_clipboard">Kopírovat do schránky</string>
<string name="message_copied_to_clipboard">Zpráva zkopírována do schránky</string> <string name="message_copied_to_clipboard">Zpráva zkopírována do schránky</string>
<string name="message">Zpráva</string>
<string name="private_messages_are_disabled">Soukromé zprávy jsou zakázány</string>
<string name="huawei_protected_apps">Chráněné aplikace</string>
<string name="huawei_protected_apps_summary">Abyste mohli dostávat upozornění i při vypnuté obrazovce, musíte přidat Conversations mezi chráněné aplikace.</string>
<string name="mtm_accept_cert">Přijmout neznámý certifikát?</string>
<string name="mtm_trust_anchor">Certifikát není podepsaný žádnou známou certifikační autoritou.</string>
<string name="mtm_accept_servername">Přijmout nesouhlasící jméno serveru?</string>
<string name="mtm_hostname_mismatch">Server se nemohl prokázat jako \&quot;%s\&quot;. Certifikát je platný pouze pro:</string>
<string name="mtm_connect_anyway">Chcete se přesto připojit?</string>
<string name="mtm_cert_details">Detaily certifikátu:</string>
<string name="once">Jednou</string>
<string name="qr_code_scanner_needs_access_to_camera">Skener kódů QR potřebuje přístup k fotoaparátu</string>
<string name="pref_scroll_to_bottom">Posunout na konec</string>
<string name="pref_scroll_to_bottom_summary">Posunout na konec po odeslání zprávy</string>
<string name="edit_status_message_title">Upravit stavovou zprávu</string> <string name="edit_status_message_title">Upravit stavovou zprávu</string>
<string name="edit_status_message">Upravit stavovou zprávu</string> <string name="edit_status_message">Upravit stavovou zprávu</string>
<string name="disable_encryption">Zakázat šifrování</string>
<string name="error_trustkey_general">%1$s nemohl odeslat šifrované zprávy pro %2$s. To může být způsobeno tím, že kontakt používá zastaralý server nebo klient, který nepodporuje OMEMO šifrování.</string>
<string name="error_trustkey_device_list">Nelze získat seznam zařízení</string>
<string name="error_trustkey_bundle">Nelze získat šifrovací klíče</string>
<string name="error_trustkey_hint_mutual">Tip: V některých případech může být řešení vzájemné přidání kontaktů do seznamu kontaktů.</string>
<string name="disable_encryption_message">Opravdu chcete vypnout OMEMO šifrování pro tuto konverzaci?\nTím umožníte správci Vašeho serveru číst Vaše zprávy. Zároveň to však může být jediný způsob, jak komunikovat s kontakty, které používají zastaralé verze klientů.</string>
<string name="disable_now">Vypnout hned</string>
<string name="draft">Koncept:</string>
<string name="pref_omemo_setting">OMEMO šifrování</string>
<string name="pref_omemo_setting_summary_always">OMEMO bude vždy použito k šifrování zpráv v jednotlivých konverzacích i v soukromých skupinách.</string>
<string name="pref_omemo_setting_summary_default_on">OMEMO bude použito jako výchozí pro nové konverzace.</string>
<string name="pref_omemo_setting_summary_default_off">OMEMO bude nutné zapnout ručně pro každou každou novou konverzaci.</string>
<string name="create_shortcut">Vytvořit zástupce</string>
<string name="pref_font_size">Velikost písma</string>
<string name="pref_font_size_summary">Relativní velikost písma v aplikaci</string>
<string name="default_on">Zapnuto jako výchozí</string>
<string name="default_off">Vypnuto jako výchozí</string>
<string name="small">Malé</string>
<string name="medium">Střední</string> <string name="medium">Střední</string>
<string name="large">Velké</string>
<string name="not_encrypted_for_this_device">Zpráva nebyla pro toto zařízení zašifrována.</string>
<string name="omemo_decryption_failed">Chyba při dešifrování OMEMO zprávy.</string>
<string name="undo">zpět</string>
<string name="location_disabled">Sdílení polohy je vypnuto</string>
<string name="action_copy_location">Kopírovat pozici</string>
<string name="action_share_location">Sdílet pozici</string>
<string name="action_directions">Pokyny</string>
<string name="title_activity_share_location">Sdílet pozici</string>
<string name="title_activity_show_location">Zobrazit pozici</string> <string name="title_activity_show_location">Zobrazit pozici</string>
<string name="share">Sdílet</string>
<string name="unable_to_start_recording">Nebylo možné zahájit nahrávání</string>
<string name="please_wait">Chvíli strpení…</string>
<string name="no_microphone_permission">Povolit %1$s přístup k mikrofonu</string>
<string name="search_messages">Prohledat zprávy</string> <string name="search_messages">Prohledat zprávy</string>
<string name="gif">GIF</string>
<string name="view_conversation">Zobrazit konverzaci</string>
<string name="pref_use_share_location_plugin">Plugin pro sdílení pozice</string>
<string name="pref_use_share_location_plugin_summary">Použít Plugin pro sdílení pozice namísto interní mapy</string>
<string name="copy_link">Kopírovat webovou adresu</string>
<string name="copy_jabber_id">Kopírovat XMPP adresu</string>
<string name="pref_start_search">Přímé vyhledávání</string>
<string name="pref_start_search_summary">Na úvodní obrazovce otevřít klávesnici a umístit kurzor do vyhledávacího pole</string>
<string name="group_chat_avatar">Avatar skupinového chatu</string>
<string name="host_does_not_support_group_chat_avatars">Hostitel nepodporuje avatary pro skupinový chat</string>
<string name="only_the_owner_can_change_group_chat_avatar">Pouze vlastník může změnit avatar skupinového chatu</string>
<string name="contact_name">Jméno kontaktu</string>
<string name="nickname">Přezdívka</string>
<string name="group_chat_name">Jméno</string>
<string name="providing_a_name_is_optional">Poskytnutí jména je nepovinné</string>
<string name="create_dialog_group_chat_name">Jméno skupinového chatu</string> <string name="create_dialog_group_chat_name">Jméno skupinového chatu</string>
<string name="conference_destroyed">Tento skupinový chat byl zrušen</string>
<string name="unable_to_save_recording">Nebylo možné uložit nahrávku</string>
<string name="foreground_service_channel_name">Služba na popředí</string>
<string name="foreground_service_channel_description">Tato kategorie upozornění zobrazuje stálou notifikaci, že aplikace %1$s je spuštěná.</string>
<string name="notification_group_status_information">Informace o stavu</string>
<string name="error_channel_name">Problémy s připojením</string>
<string name="error_channel_description">Tato kategorie upozornění zobrazuje notifikaci v případě problémů s připojením k účtu.</string>
<string name="notification_group_messages">Zprávy</string>
<string name="notification_group_calls">Hovory</string>
<string name="messages_channel_name">Zprávy</string>
<string name="incoming_calls_channel_name">Příchozí hovory</string>
<string name="ongoing_calls_channel_name">Probíhající hovory</string>
<string name="silent_messages_channel_name">Tiché zprávy</string>
<string name="silent_messages_channel_description">Kategorie upozornění, která nejsou doprovázena žádným zvukem. Například když jste aktivní na jiném zařízení (ochranná doba).</string>
<string name="delivery_failed_channel_name">Neúspěšné přenosy</string>
<string name="pref_message_notification_settings">Nastavení upozornění na zprávy</string>
<string name="pref_incoming_call_notification_settings">Nastavení upozornění na příchozí hovory</string>
<string name="pref_more_notification_settings_summary">Důležitost, Zvuk, Vibrace</string>
<string name="video_compression_channel_name">Komprese videa</string>
<string name="view_media">Zobrazit média</string>
<string name="group_chat_members">Účastníci</string>
<string name="media_browser">Prohlížeč médií</string>
<string name="security_violation_not_attaching_file">Soubor byl vynechán kvůli porušení bezpečnosti.</string>
<string name="pref_video_compression">Kvalita videa</string>
<string name="pref_video_compression_summary">Nižší kvalita znamená menší soubory</string>
<string name="video_360p">Střední (360p)</string>
<string name="video_720p">Vysoká (720p)</string>
<string name="cancelled">zrušeno</string>
<string name="already_drafting_message">Již máte rozepsaný koncept zprávy.</string>
<string name="feature_not_implemented">Funkce není implemetována</string>
<string name="invalid_country_code">Neplatný kód země</string>
<string name="choose_a_country">Vyberte zemi</string>
<string name="phone_number">telefonní číslo</string>
<string name="verify_your_phone_number">Ověřte své telefonní číslo</string>
<string name="enter_country_code_and_phone_number">Quicksy Vám pošle zprávu SMS (mohou Vám být účtovány poplatky dle tarifu) k ověření Vašeho telefonního čísla. Zadejte kód země a telefonní číslo:</string>
<string name="we_will_be_verifying"><![CDATA[Ověření pro telefonní číslo <br/><br/><b>%s</b><br/><br/>. Je číslo v pořádku, nebo ho chete upravit?]]></string>
<string name="not_a_valid_phone_number">%s není platné telefonní číslo.</string>
<string name="please_enter_your_phone_number">Prosíme, zadejte své telefonní číslo.</string>
<string name="search_countries">Hledat zemi</string>
<string name="verify_x">Ověřit %s</string>
<string name="we_have_sent_you_an_sms_to_x"><![CDATA[Poslali jsme Vám SMS na <b>%s</b>.]]></string>
<string name="we_have_sent_you_another_sms">Poslali jsme Vám další SMS se 6místným kódem.</string>
<string name="please_enter_pin_below">Prosím, vložte 6místný pin.</string>
<string name="resend_sms">Poslat SMS znovu</string>
<string name="resend_sms_in">Poslat SMS znovu (%s)</string>
<string name="wait_x">Chvíli strpení (%s)</string>
<string name="back">zpět</string>
<string name="possible_pin">Automaticky vložen pravděpodobný pin ze schránky.</string>
<string name="please_enter_pin">Prosím, vložte svůj 6místný pin.</string>
<string name="abort_registration_procedure">Opravdu si přejete přerušit registraci?</string>
<string name="yes">Ano</string>
<string name="no">Ne</string>
<string name="verifying">Ověřuji…</string>
<string name="incorrect_pin">Pin, který jste zadali, je nesprávný.</string>
<string name="pin_expired">Pin, který jsme Vám poslali, vypršel.</string>
<string name="unknown_api_error_network">Neznámá chyba sítě.</string>
<string name="unknown_api_error_response">Neznámá odpověď serveru.</string>
<string name="unable_to_connect_to_server">Nebylo možné se připojit k serveru.</string>
<string name="unable_to_establish_secure_connection">Nebylo možné navázat zabezpečené spojení.</string>
<string name="unable_to_find_server">Server nenalezen.</string>
<string name="something_went_wrong_processing_your_request">Něco se pokazilo při zpracovávání Vašeho požadavku.</string>
<string name="temporarily_unavailable">Dočasně nedostupné. Zkuste to později.</string>
<string name="no_network_connection">Žádné připojení k síti.</string>
<string name="try_again_in_x">Prosíme, zkuste to znovu za %s</string>
<string name="too_many_attempts">Příliš mnoho pokusů</string>
<string name="the_app_is_out_of_date">Používáte zastaralou verzi této aplikace.</string>
<string name="update">Aktualizovat</string>
<string name="logged_in_with_another_device">Toto telefonní číslo je již přihlášeno z jiného zařízení.</string>
<string name="enter_your_name_instructions">Prosíme, vložte své jméno, aby ostatní, kteří Vás nemají v seznamu kontaktů, věděli, kdo jste.</string>
<string name="your_name">Vaše jméno</string> <string name="your_name">Vaše jméno</string>
<string name="enter_your_name">Vložte své jméno</string>
<string name="no_name_set_instructions">Pro nastavení jména klepněte na Upravit.</string>
<string name="reject_request">Odmítnout žádost</string>
<string name="install_orbot">Instalovat Orbot</string>
<string name="start_orbot">Spustit Orbot</string>
<string name="no_market_app_installed">Není nainstalován žádný správce aplikací.</string>
<string name="group_chat_will_make_your_jabber_id_public">Tento kanál zveřejní Vaši XMPP adresu</string>
<string name="video_original">Originální (nekomprimováno)</string>
<string name="open_with">Otevřít pomocí…</string>
<string name="set_profile_picture">Nastavit profilový obrázek</string>
<string name="choose_account">Vybrat účet</string>
<string name="restore_backup">Obnovit ze zálohy</string>
<string name="restore">Obnovit</string>
<string name="enter_password_to_restore">Pro obnovení ze zálohy zadejte heslo k účtu %s.</string>
<string name="restore_warning">Nepoužívejte funkci obnovy ze zálohy pro současný běh více instalací. Obnova ze zálohy je určena pouze pro případ přenosu na jinou instalaci nebo pokud došlo ke ztrátě původního zařízení.</string>
<string name="unable_to_restore_backup">Nebylo možné obnovit zálohu.</string>
<string name="unable_to_decrypt_backup">Nebylo možné dešifrovat zálohu. Zadal(a) jste správné heslo?</string>
<string name="backup_channel_name">Záloha &amp; Obnova</string>
<string name="enter_jabber_id">Zadejte XMPP adresu</string>
<string name="create_group_chat">Vytvořit skupinový chat</string> <string name="create_group_chat">Vytvořit skupinový chat</string>
<string name="join_public_channel">Připojit se k veřejnému kanálu</string>
<string name="create_private_group_chat">Vytvořit soukromý skupinový chat</string> <string name="create_private_group_chat">Vytvořit soukromý skupinový chat</string>
<string name="create_public_channel">Vytvořit veřejný kanál</string> <string name="create_public_channel">Vytvořit veřejný kanál</string>
<string name="create_dialog_channel_name">Jméno kanálu</string> <string name="create_dialog_channel_name">Jméno kanálu</string>
<string name="xmpp_address">Adresa XMPP</string> <string name="xmpp_address">Adresa XMPP</string>
<string name="please_enter_name">Prosím, zadejte název kanálu</string>
<string name="please_enter_xmpp_address">Zadejte XMPP adresu</string>
<string name="this_is_an_xmpp_address">Toto je XMPP adresa. Prosím, zadejte jméno.</string>
<string name="creating_channel">Vytváření veřejného kanálu…</string>
<string name="channel_already_exists">Tento kanál již existuje</string>
<string name="joined_an_existing_channel">Připojil(a) jste se k existujícímu kanálu</string>
<string name="unable_to_set_channel_configuration">Nebylo možné uložit nastavení kanálu</string>
<string name="allow_participants_to_edit_subject">Povolit komukoli změnit téma</string>
<string name="allow_participants_to_invite_others">Povolit komukoli pozvat další účastníky</string>
<string name="anyone_can_edit_subject">Kdokoli může změnit téma.</string>
<string name="owners_can_edit_subject">Vlastníci mohou měnit téma.</string>
<string name="admins_can_edit_subject">Správci mohou měnit téma.</string>
<string name="owners_can_invite_others">Vlastníci mohou pozvat další účastníky.</string>
<string name="anyone_can_invite_others">Kdokoli může pozvat další účastníky.</string>
<string name="jabber_ids_are_visible_to_admins">Správci mohou vidět XMPP adresy.</string>
<string name="jabber_ids_are_visible_to_anyone">Kdokoli může vidět XMPP adresy.</string>
<string name="no_users_hint_channel">Tento veřejný kanál nemá žádné účastníky. Pozvěte své kontakty nebo sdílejte XMPP adresu kanálu pomocí tlačítka Sdílet.</string>
<string name="no_users_hint_group_chat">Tento soukromý skupinový chat nemá žádné účastníky.</string>
<string name="manage_permission">Spravovat oprávnění</string>
<string name="search_participants">Hledat účastníky</string>
<string name="file_too_large">Soubor je příliš velký</string>
<string name="attach">Přiložit</string>
<string name="discover_channels">Najít kanály</string> <string name="discover_channels">Najít kanály</string>
<string name="search_channels">Prohledat kanály</string>
<string name="channel_discovery_opt_in_title">Možné porušení soukromí</string> <string name="channel_discovery_opt_in_title">Možné porušení soukromí</string>
<string name="channel_discover_opt_in_message"><![CDATA[Vyhledávání kanálů používá službu třetí strany jménem <a href="https://search.jabber.network">search.jabber.network</a>.<br><br>Používání této služby odešle vaši IP adresu a vyhledávaný termín této službě. Pro více informací konzultujte jejich <a href="https://search.jabber.network/privacy">Zásady ochrany osobních údajů</a>.]]></string> <string name="channel_discover_opt_in_message"><![CDATA[Vyhledávání kanálů používá službu třetí strany jménem <a href="https://search.jabber.network">search.jabber.network</a>.<br><br>Používání této služby odešle vaši IP adresu a vyhledávaný termín této službě. Pro více informací konzultujte jejich <a href="https://search.jabber.network/privacy">Zásady ochrany osobních údajů</a>.]]></string>
<string name="i_already_have_an_account">Již mám účet</string>
<string name="add_existing_account">Přidat existující účet</string>
<string name="register_new_account">Vytvořit nový účet</string>
<string name="this_looks_like_a_domain">Toto vypadá jako adresa domény</string>
<string name="add_anway">Přesto přidat</string>
<string name="this_looks_like_channel">Toto vypadá jako adresa kanálu</string>
<string name="share_backup_files">Sdílet soubory zálohy</string>
<string name="conversations_backup">Záloha Conversations</string>
<string name="open_backup">Otevřít zálohu</string>
<string name="not_a_backup_file">Soubor, který jste zvolili, není soubor zálohy Conversations</string>
<string name="account_already_setup">Tento účet byl již nastaven</string>
<string name="please_enter_password">Prosím, zadejte heslo k tomuto účtu</string>
<string name="unable_to_perform_this_action">Nebylo možné vykonat tuto akci</string>
<string name="open_join_dialog">Připojit se k veřejnému kanálu…</string>
<string name="sharing_application_not_grant_permission">Sdílející aplikace neudělila dostatečná oprávnění pro přístup k souboru.</string>
<string name="group_chats_and_channels"><![CDATA[Skupinové chaty & Kanály]]></string>
<string name="local_server">Místní server</string>
<string name="pref_channel_discovery_summary">Většině uživatelů doporučujeme použít \'jabber.network\' kvůli lepším návrhům z celého veřejného XMPP ekosystému.</string>
<string name="pref_channel_discovery">Metoda objevování kanálů</string>
<string name="backup">Záloha</string>
<string name="category_about">O</string>
<string name="please_enable_an_account">Prosíme, povolte účet</string>
<string name="make_call">Volat</string>
<string name="rtp_state_incoming_call">Příchozí hovor</string>
<string name="rtp_state_incoming_video_call">Příchozí videohovor</string>
<string name="rtp_state_connecting">Připojuji</string>
<string name="rtp_state_connected">Připojeno</string>
<string name="rtp_state_accepting_call">Přijímám hovor</string>
<string name="rtp_state_ending_call">Ukončuji hovor</string>
<string name="answer_call">Přijmout</string>
<string name="dismiss_call">Odmítnout</string>
<string name="rtp_state_finding_device">Vyhledávám zařízení</string>
<string name="rtp_state_ringing">Vyzvánění</string>
<string name="rtp_state_declined_or_busy">Zaneprázdněný</string> <string name="rtp_state_declined_or_busy">Zaneprázdněný</string>
</resources> <string name="rtp_state_connectivity_error">Hovor nebylo možné spojit</string>
<string name="rtp_state_connectivity_lost_error">Spojení ztraceno</string>
<string name="rtp_state_application_failure">Chyba aplikace</string>
<string name="hang_up">Zavěsit</string>
<string name="ongoing_call">Probíhající hovor</string>
<string name="ongoing_video_call">Probíhající videohovor</string>
<string name="disable_tor_to_make_call">Zakázat hovory přes Tor</string>
<string name="incoming_call">Příchozí hovor</string>
<string name="incoming_call_duration">Příchozí hovor · %s</string>
<string name="missed_call_timestamp">Zmeškané volání · %s</string>
<string name="outgoing_call">Odchozí hovor</string>
<string name="outgoing_call_duration">Odchozí hovor · %s</string>
<string name="missed_call">Zmeškané volání</string>
<string name="audio_call">Hovor</string>
<string name="video_call">Videohovor</string>
<string name="help">Nápověda</string>
<string name="switch_to_conversation">Přepnout na konverzaci</string>
<string name="microphone_unavailable">Váš mikrofon je nedostupný</string>
<string name="only_one_call_at_a_time">V jednu chvíli může probíhat pouze jeden hovor.</string>
<string name="return_to_ongoing_call">Návrat k probíhajícímu hovoru</string>
<string name="could_not_switch_camera">Nebylo možné přepnout kameru</string>
<string name="add_to_favorites">Připnout nahoru</string>
<string name="remove_from_favorites">Odepnout shora</string>
<string name="could_not_correct_message">Nebylo možné opravit zprávu</string>
<string name="search_all_conversations">Všechny konverzace</string>
<string name="search_this_conversation">Tato konverzace</string>
<string name="your_avatar">Váš avatar</string>
<string name="avatar_for_x">Avatar uživatele %s</string>
<string name="encrypted_with_omemo">Šifrováno pomocí OMEMO</string>
<string name="encrypted_with_openpgp">Šifrováno pomocí OpenPGP</string>
<string name="not_encrypted">Nešifrováno</string>
<string name="exit">Ukončit</string>
<string name="record_voice_mail">Nahrát hlasovou zprávu</string>
<string name="play_audio">Přehrát audio</string>
<string name="pause_audio">Pozastavit audio</string>
<string name="add_contact_or_create_or_join_group_chat">Přidat kontakt, vytvořit nebo se připojit ke skupinovému chatu nebo vyhledat kanály</string>
<plurals name="view_users">
<item quantity="one">Ukázat %1$d účastníka</item>
<item quantity="few">Ukázat %1$d účastníky</item>
<item quantity="many">Ukázat %1$d účastníků</item>
<item quantity="other">Ukázat %1$d účastníků</item>
</plurals>
<plurals name="some_messages_could_not_be_delivered">
<item quantity="one">Zpráva nemohla být doručena</item>
<item quantity="few">Několik zpráv nemohlo být doručeny</item>
<item quantity="many">Některé zprávy nemohly být doručeny</item>
<item quantity="other">Některé zprávy nemohly být doručeny</item>
</plurals>
<string name="failed_deliveries">Neúspěšné přenosy</string>
<string name="more_options">Více možností</string>
<string name="no_application_found">Nenalezena žádná aplikace</string>
<string name="invite_to_app">Pozvat do Conversations</string>
<string name="server_does_not_support_easy_onboarding_invites">Server nepodporuje vytváření pozvánek</string>
<string name="no_active_accounts_support_this">Žádný z aktivních účtů tuto funkci nepodporuje</string>
<string name="backup_started_message">Zálohování zahájeno. Budete upozorněni, jakmile bude záloha hotova.</string>
</resources>

View File

@ -465,8 +465,8 @@
<string name="account_status_host_unknown">Serveren er ikke ansvarlig for dette domæne</string> <string name="account_status_host_unknown">Serveren er ikke ansvarlig for dette domæne</string>
<string name="server_info_broken">Brudt</string> <string name="server_info_broken">Brudt</string>
<string name="pref_presence_settings">Tilgængelighed</string> <string name="pref_presence_settings">Tilgængelighed</string>
<string name="pref_away_when_screen_off">Væk når enhed er låst</string> <string name="pref_away_when_screen_off">Ude når enhed er låst</string>
<string name="pref_away_when_screen_off_summary">Vis som Væk når enheden er låst</string> <string name="pref_away_when_screen_off_summary">Vis som Ude når enheden er låst</string>
<string name="pref_dnd_on_silent_mode">Optaget i lydløs tilstand</string> <string name="pref_dnd_on_silent_mode">Optaget i lydløs tilstand</string>
<string name="pref_dnd_on_silent_mode_summary">Vis som Optaget når enhed er i lydløs tilstand</string> <string name="pref_dnd_on_silent_mode_summary">Vis som Optaget når enhed er i lydløs tilstand</string>
<string name="pref_treat_vibrate_as_silent">Behandl vibration som lydløs tilstand</string> <string name="pref_treat_vibrate_as_silent">Behandl vibration som lydløs tilstand</string>
@ -486,7 +486,7 @@
<string name="jid_does_not_match_certificate">XMPP-adresse matcher ikke certifikatet</string> <string name="jid_does_not_match_certificate">XMPP-adresse matcher ikke certifikatet</string>
<string name="action_renew_certificate">Forny certifikat</string> <string name="action_renew_certificate">Forny certifikat</string>
<string name="error_fetching_omemo_key">Fejl ved hentning af OMEMO-nøgle!</string> <string name="error_fetching_omemo_key">Fejl ved hentning af OMEMO-nøgle!</string>
<string name="verified_omemo_key_with_certificate">Bekræftet OMEMO-nøgler med Certifikat!</string> <string name="verified_omemo_key_with_certificate">Bekræftet OMEMO-nøgler med certifikat!</string>
<string name="device_does_not_support_certificates">Din enhed understøtter ikke valget af klientcertifikater!</string> <string name="device_does_not_support_certificates">Din enhed understøtter ikke valget af klientcertifikater!</string>
<string name="pref_connection_options">Forbindelse</string> <string name="pref_connection_options">Forbindelse</string>
<string name="pref_use_tor">Forbind via TOR</string> <string name="pref_use_tor">Forbind via TOR</string>
@ -545,7 +545,7 @@
<string name="status_message">Statusbesked</string> <string name="status_message">Statusbesked</string>
<string name="presence_chat">Gratis for Chat</string> <string name="presence_chat">Gratis for Chat</string>
<string name="presence_online">Online</string> <string name="presence_online">Online</string>
<string name="presence_away">Væk</string> <string name="presence_away">Ude</string>
<string name="presence_xa">Ikke tilgængelig</string> <string name="presence_xa">Ikke tilgængelig</string>
<string name="presence_dnd">Optaget</string> <string name="presence_dnd">Optaget</string>
<string name="secure_password_generated">Der er genereret en sikker adgangskode</string> <string name="secure_password_generated">Der er genereret en sikker adgangskode</string>
@ -959,4 +959,4 @@
<string name="unable_to_parse_invite">Kunne ikke analysere invitation</string> <string name="unable_to_parse_invite">Kunne ikke analysere invitation</string>
<string name="server_does_not_support_easy_onboarding_invites">Server understøtter ikke generering af invitationer</string> <string name="server_does_not_support_easy_onboarding_invites">Server understøtter ikke generering af invitationer</string>
<string name="no_active_accounts_support_this">Ingen aktive konti understøtter denne funktion</string> <string name="no_active_accounts_support_this">Ingen aktive konti understøtter denne funktion</string>
</resources> </resources>

View File

@ -363,7 +363,7 @@
<string name="fetching_history_from_server">Lade Chatverlauf…</string> <string name="fetching_history_from_server">Lade Chatverlauf…</string>
<string name="no_more_history_on_server">Keine weiteren Nachrichten vorhanden</string> <string name="no_more_history_on_server">Keine weiteren Nachrichten vorhanden</string>
<string name="updating">Aktualisieren…</string> <string name="updating">Aktualisieren…</string>
<string name="password_changed">Passwort geändert.</string> <string name="password_changed">Passwort geändert!</string>
<string name="could_not_change_password">Passwort konnte nicht geändert werden</string> <string name="could_not_change_password">Passwort konnte nicht geändert werden</string>
<string name="change_password">Passwort ändern</string> <string name="change_password">Passwort ändern</string>
<string name="current_password">Aktuelles Passwort</string> <string name="current_password">Aktuelles Passwort</string>
@ -959,4 +959,5 @@
<string name="unable_to_parse_invite">Einladung kann nicht gelesen werden</string> <string name="unable_to_parse_invite">Einladung kann nicht gelesen werden</string>
<string name="server_does_not_support_easy_onboarding_invites">Server unterstützt keine Generierung von Einladungen</string> <string name="server_does_not_support_easy_onboarding_invites">Server unterstützt keine Generierung von Einladungen</string>
<string name="no_active_accounts_support_this">Keine aktiven Konten unterstützen diese Funktion</string> <string name="no_active_accounts_support_this">Keine aktiven Konten unterstützen diese Funktion</string>
<string name="backup_started_message">Das Backup wurde gestartet. Du bekommst eine Benachrichtigung sobald es fertig ist.</string>
</resources> </resources>

View File

@ -956,4 +956,4 @@
<string name="unable_to_parse_invite">Αδυναμία ανάγνωσης πρόσκλησης</string> <string name="unable_to_parse_invite">Αδυναμία ανάγνωσης πρόσκλησης</string>
<string name="server_does_not_support_easy_onboarding_invites">Ο διακομιστής δεν υποστηρίζει την δημιουργία προσκλήσεων</string> <string name="server_does_not_support_easy_onboarding_invites">Ο διακομιστής δεν υποστηρίζει την δημιουργία προσκλήσεων</string>
<string name="no_active_accounts_support_this">Κανένας από τους ενεργούς λογαριασμούς δεν υποστηρίζει αυτό το χαρακτηριστικό</string> <string name="no_active_accounts_support_this">Κανένας από τους ενεργούς λογαριασμούς δεν υποστηρίζει αυτό το χαρακτηριστικό</string>
</resources> </resources>

View File

@ -954,4 +954,4 @@
<string name="unable_to_parse_invite">No se ha podido leer la invitación</string> <string name="unable_to_parse_invite">No se ha podido leer la invitación</string>
<string name="server_does_not_support_easy_onboarding_invites">El servidor no soporta la creación de invitaciones</string> <string name="server_does_not_support_easy_onboarding_invites">El servidor no soporta la creación de invitaciones</string>
<string name="no_active_accounts_support_this">Ninguna cuenta activa soporta esta característica</string> <string name="no_active_accounts_support_this">Ninguna cuenta activa soporta esta característica</string>
</resources> </resources>

View File

@ -959,4 +959,4 @@
<string name="unable_to_parse_invite">Non se puido enviar o convite</string> <string name="unable_to_parse_invite">Non se puido enviar o convite</string>
<string name="server_does_not_support_easy_onboarding_invites">O servidor non soporta a creación de convites</string> <string name="server_does_not_support_easy_onboarding_invites">O servidor non soporta a creación de convites</string>
<string name="no_active_accounts_support_this">Ningunha conta activa soporta esta función</string> <string name="no_active_accounts_support_this">Ningunha conta activa soporta esta función</string>
</resources> </resources>

View File

@ -959,4 +959,4 @@
<string name="unable_to_parse_invite">Impossibile analizzare l\'invito</string> <string name="unable_to_parse_invite">Impossibile analizzare l\'invito</string>
<string name="server_does_not_support_easy_onboarding_invites">Il server non supporta la generazione di inviti</string> <string name="server_does_not_support_easy_onboarding_invites">Il server non supporta la generazione di inviti</string>
<string name="no_active_accounts_support_this">Nessun account attivo supporta questa funzione</string> <string name="no_active_accounts_support_this">Nessun account attivo supporta questa funzione</string>
</resources> </resources>

View File

@ -986,4 +986,5 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż
<string name="unable_to_parse_invite">Nie można przetworzyć zaproszenia</string> <string name="unable_to_parse_invite">Nie można przetworzyć zaproszenia</string>
<string name="server_does_not_support_easy_onboarding_invites">Serwer nie wspiera tworzenia zaproszeń</string> <string name="server_does_not_support_easy_onboarding_invites">Serwer nie wspiera tworzenia zaproszeń</string>
<string name="no_active_accounts_support_this">Nie ma aktywnych kont wspierających tę funkcję</string> <string name="no_active_accounts_support_this">Nie ma aktywnych kont wspierających tę funkcję</string>
<string name="backup_started_message">Tworzenie kopii zapasowej się rozpoczęło. Dostaniesz powiadomienie kiedy się zakończy. </string>
</resources> </resources>

View File

@ -959,4 +959,4 @@
<string name="unable_to_parse_invite">Não foi possível processar o convite</string> <string name="unable_to_parse_invite">Não foi possível processar o convite</string>
<string name="server_does_not_support_easy_onboarding_invites">O servidor não suporta a criação de convites</string> <string name="server_does_not_support_easy_onboarding_invites">O servidor não suporta a criação de convites</string>
<string name="no_active_accounts_support_this">Nenhuma conta ativa suporta esse recurso</string> <string name="no_active_accounts_support_this">Nenhuma conta ativa suporta esse recurso</string>
</resources> </resources>

View File

@ -972,4 +972,5 @@
<string name="unable_to_parse_invite">Nu s-a putut procesa invitația</string> <string name="unable_to_parse_invite">Nu s-a putut procesa invitația</string>
<string name="server_does_not_support_easy_onboarding_invites">Serverul nu suportă generarea de invitații</string> <string name="server_does_not_support_easy_onboarding_invites">Serverul nu suportă generarea de invitații</string>
<string name="no_active_accounts_support_this">Nici un cont activ nu suporta această caracteristică</string> <string name="no_active_accounts_support_this">Nici un cont activ nu suporta această caracteristică</string>
<string name="backup_started_message">Se creează copia de siguranță. Veți primi o notificare când acesta este completă.</string>
</resources> </resources>

View File

@ -985,4 +985,4 @@
<string name="unable_to_parse_invite">Невозможно разобрать приглашение</string> <string name="unable_to_parse_invite">Невозможно разобрать приглашение</string>
<string name="server_does_not_support_easy_onboarding_invites">Сервер не поддерживает создание приглашений</string> <string name="server_does_not_support_easy_onboarding_invites">Сервер не поддерживает создание приглашений</string>
<string name="no_active_accounts_support_this">Ни один активный аккаунт не поддерживает эту функцию</string> <string name="no_active_accounts_support_this">Ни один активный аккаунт не поддерживает эту функцию</string>
</resources> </resources>

View File

@ -124,6 +124,7 @@
<string name="pref_ringtone">Zil sesi</string> <string name="pref_ringtone">Zil sesi</string>
<string name="pref_notification_sound">Bildirim sesi</string> <string name="pref_notification_sound">Bildirim sesi</string>
<string name="pref_notification_sound_summary">Yeni mesajlar için bildirim sesi</string> <string name="pref_notification_sound_summary">Yeni mesajlar için bildirim sesi</string>
<string name="pref_call_ringtone_summary">Gelen çağrılar için zil sesi</string>
<string name="pref_notification_grace_period">Mühlet</string> <string name="pref_notification_grace_period">Mühlet</string>
<string name="pref_notification_grace_period_summary">Cihazlarınızın birinde faaliyet tespit edilmesinden sonra zaman hatırlatmalarının susturulma uzunluğu.</string> <string name="pref_notification_grace_period_summary">Cihazlarınızın birinde faaliyet tespit edilmesinden sonra zaman hatırlatmalarının susturulma uzunluğu.</string>
<string name="pref_advanced_options">Gelişmiş</string> <string name="pref_advanced_options">Gelişmiş</string>
@ -464,6 +465,8 @@
<string name="account_status_host_unknown">Sunucu bu alan adı için sorumlu değil</string> <string name="account_status_host_unknown">Sunucu bu alan adı için sorumlu değil</string>
<string name="server_info_broken">Bozuk</string> <string name="server_info_broken">Bozuk</string>
<string name="pref_presence_settings">Mevcudiyet</string> <string name="pref_presence_settings">Mevcudiyet</string>
<string name="pref_away_when_screen_off">Telefon kilitliyken uzakta</string>
<string name="pref_away_when_screen_off_summary">Telefon kilitliyken Uzakta göster</string>
<string name="pref_dnd_on_silent_mode">Müsait değilken sessiz kipte olur</string> <string name="pref_dnd_on_silent_mode">Müsait değilken sessiz kipte olur</string>
<string name="pref_dnd_on_silent_mode_summary">Telefonunuz sessizdeyken, durum bildiriminizi müsait değil olarak gösterir.</string> <string name="pref_dnd_on_silent_mode_summary">Telefonunuz sessizdeyken, durum bildiriminizi müsait değil olarak gösterir.</string>
<string name="pref_treat_vibrate_as_silent">Titreşim kipini sessiz kip olarak değerlendir</string> <string name="pref_treat_vibrate_as_silent">Titreşim kipini sessiz kip olarak değerlendir</string>
@ -956,4 +959,4 @@
<string name="unable_to_parse_invite">Davet iletilemedi</string> <string name="unable_to_parse_invite">Davet iletilemedi</string>
<string name="server_does_not_support_easy_onboarding_invites">Sunucu, davet oluşturulmasını desteklemiyor</string> <string name="server_does_not_support_easy_onboarding_invites">Sunucu, davet oluşturulmasını desteklemiyor</string>
<string name="no_active_accounts_support_this">Bu özelliği destekleyen aktif bir hesap yok</string> <string name="no_active_accounts_support_this">Bu özelliği destekleyen aktif bir hesap yok</string>
</resources> </resources>

View File

@ -946,4 +946,5 @@
<string name="unable_to_parse_invite">无法解析邀请</string> <string name="unable_to_parse_invite">无法解析邀请</string>
<string name="server_does_not_support_easy_onboarding_invites">服务器不支持生成邀请</string> <string name="server_does_not_support_easy_onboarding_invites">服务器不支持生成邀请</string>
<string name="no_active_accounts_support_this">没有活跃帐户支持此功能</string> <string name="no_active_accounts_support_this">没有活跃帐户支持此功能</string>
<string name="backup_started_message">已启动备份。一旦完成,你会收到通知。</string>
</resources> </resources>

View File

@ -148,6 +148,7 @@
<string name="error_file_not_found">File not found</string> <string name="error_file_not_found">File not found</string>
<string name="error_io_exception">General I/O error. Maybe you ran out of storage space?</string> <string name="error_io_exception">General I/O error. Maybe you ran out of storage space?</string>
<string name="error_security_exception_during_image_copy">The app you used to select this image did not provide enough permissions to read the file.\n\n<small>Use a different file manager to choose an image</small>.</string> <string name="error_security_exception_during_image_copy">The app you used to select this image did not provide enough permissions to read the file.\n\n<small>Use a different file manager to choose an image</small>.</string>
<string name="error_security_exception">The app you used to share this file did not provide enough permissions.</string>
<string name="account_status_unknown">Unknown</string> <string name="account_status_unknown">Unknown</string>
<string name="account_status_disabled">Temporarily disabled</string> <string name="account_status_disabled">Temporarily disabled</string>
<string name="account_status_online">Online</string> <string name="account_status_online">Online</string>
@ -959,4 +960,6 @@
<string name="unable_to_parse_invite">Unable to parse invite</string> <string name="unable_to_parse_invite">Unable to parse invite</string>
<string name="server_does_not_support_easy_onboarding_invites">Server does not support generating invites</string> <string name="server_does_not_support_easy_onboarding_invites">Server does not support generating invites</string>
<string name="no_active_accounts_support_this">No active accounts support this feature</string> <string name="no_active_accounts_support_this">No active accounts support this feature</string>
<string name="backup_started_message">The backup has been started. Youll get a notification once it has been completed.</string>
<string name="unable_to_enable_video">Unable to enable video.</string>
</resources> </resources>