Merge tag '2.9.9' into develop
This commit is contained in:
commit
35e6c476dd
|
@ -11,7 +11,7 @@ android:
|
|||
- '.+'
|
||||
before_script:
|
||||
- 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:
|
||||
- ./gradlew assembleQuicksyFreeCompatDebug
|
||||
- ./gradlew assembleQuicksyFreeSystemDebug
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
# Changelog
|
||||
|
||||
### Version 2.9.9
|
||||
|
||||
* Various bug fixes around Tor support
|
||||
|
||||
### Version 2.9.8
|
||||
|
||||
* Verify A/V calls with preexisting OMEMO sessions
|
||||
|
|
|
@ -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` doesn’t 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).
|
||||
|
||||
|
|
14
build.gradle
14
build.gradle
|
@ -8,7 +8,7 @@ buildscript {
|
|||
jcenter()
|
||||
}
|
||||
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.squareup.retrofit2:retrofit: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'
|
||||
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'
|
||||
}
|
||||
|
||||
|
@ -91,8 +93,8 @@ android {
|
|||
defaultConfig {
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
versionCode 42006
|
||||
versionName "2.9.8"
|
||||
versionCode 42010
|
||||
versionName "2.9.9"
|
||||
archivesBaseName += "-$versionName"
|
||||
applicationId "eu.sum7.conversations"
|
||||
resValue "string", "applicationId", applicationId
|
||||
|
@ -101,6 +103,10 @@ android {
|
|||
}
|
||||
|
||||
|
||||
configurations {
|
||||
compile.exclude group: 'org.jetbrains' , module:'annotations'
|
||||
}
|
||||
|
||||
dataBinding {
|
||||
enabled true
|
||||
}
|
||||
|
|
|
@ -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 isn’t
|
||||
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 doesn’t 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 isn’t
|
||||
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 isn’t afraid to
|
||||
break with behavior patterns that have been proven ineffective.
|
32
docs/XEPs.md
32
docs/XEPs.md
|
@ -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
|
|
@ -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 wouldn’t 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 I’m
|
||||
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 don’t
|
||||
implement them. I’m 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 wouldn’t have to name oneself `userDesktop` and
|
||||
`userMobile` but just `user`. Both ejabberd and prosody support this but with
|
||||
strange side effects. Prosody for example doesn’t 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 I’m
|
||||
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 doesn’t
|
||||
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 don’t 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 doesn’t support carbons.
|
||||
|
||||
3. When dealing with “legacy clients” — meaning clients which don’t 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
|
|
@ -0,0 +1 @@
|
|||
• Various bug fixes around Tor support
|
|
@ -3,9 +3,11 @@ package eu.siacs.conversations;
|
|||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import eu.siacs.conversations.crypto.XmppDomainVerifier;
|
||||
import eu.siacs.conversations.xmpp.Jid;
|
||||
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 PROCESS_EXTMAP_ALLOW_MIXED = false;
|
||||
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 DISABLE_HTTP_UPLOAD = false;
|
||||
|
@ -174,7 +177,14 @@ public final class Config {
|
|||
|
||||
//if the contacts domain matches one of the following domains OMEMO won’t be turned on automatically
|
||||
//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() {
|
||||
|
|
|
@ -14,7 +14,6 @@ import java.io.FileOutputStream;
|
|||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
@ -209,7 +208,7 @@ public class PgpDecryptionService {
|
|||
message.setRelativeFilePath(path);
|
||||
}
|
||||
}
|
||||
URL url = message.getFileParams().url;
|
||||
final String url = message.getFileParams().url;
|
||||
mXmppConnectionService.getFileBackend().updateFileParams(message, url);
|
||||
message.setEncryption(Message.ENCRYPTION_DECRYPTED);
|
||||
mXmppConnectionService.updateMessage(message);
|
||||
|
|
|
@ -6,17 +6,14 @@ import android.util.Log;
|
|||
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import com.google.common.base.CharMatcher;
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.io.BaseEncoding;
|
||||
|
||||
import org.openintents.openpgp.OpenPgpError;
|
||||
import org.openintents.openpgp.OpenPgpSignatureResult;
|
||||
import org.openintents.openpgp.util.OpenPgpApi;
|
||||
import org.openintents.openpgp.util.OpenPgpApi.IOpenPgpCallback;
|
||||
import org.openintents.openpgp.util.OpenPgpUtils;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
@ -75,7 +72,7 @@ public class PgpEngine {
|
|||
params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true);
|
||||
String body;
|
||||
if (message.hasFileOnRemoteHost()) {
|
||||
body = message.getFileParams().url.toString();
|
||||
body = message.getFileParams().url;
|
||||
} else {
|
||||
body = message.getBody();
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package eu.siacs.conversations.crypto;
|
||||
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
|
@ -72,8 +71,8 @@ public class XmppDomainVerifier implements DomainHostnameVerifier {
|
|||
}
|
||||
}
|
||||
|
||||
private static boolean matchDomain(String needle, List<String> haystack) {
|
||||
for (String entry : haystack) {
|
||||
public static boolean matchDomain(final String needle, final List<String> haystack) {
|
||||
for (final String entry : haystack) {
|
||||
if (entry.startsWith("*.")) {
|
||||
int offset = 0;
|
||||
while (offset < needle.length()) {
|
||||
|
@ -81,16 +80,13 @@ public class XmppDomainVerifier implements DomainHostnameVerifier {
|
|||
if (i < 0) {
|
||||
break;
|
||||
}
|
||||
Log.d(LOGTAG, "comparing " + needle.substring(i) + " and " + entry.substring(1));
|
||||
if (needle.substring(i).equalsIgnoreCase(entry.substring(1))) {
|
||||
Log.d(LOGTAG, "domain " + needle + " matched " + entry);
|
||||
return true;
|
||||
}
|
||||
offset = i + 1;
|
||||
}
|
||||
} else {
|
||||
if (entry.equalsIgnoreCase(needle)) {
|
||||
Log.d(LOGTAG, "domain " + needle + " matched " + entry);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1169,7 +1169,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId());
|
||||
final String content;
|
||||
if (message.hasFileOnRemoteHost()) {
|
||||
content = message.getFileParams().url.toString();
|
||||
content = message.getFileParams().url;
|
||||
} else {
|
||||
content = message.getBody();
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ import android.content.ContentValues;
|
|||
import android.database.Cursor;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
@ -147,7 +146,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
|
|||
}
|
||||
|
||||
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() {
|
||||
|
|
|
@ -143,7 +143,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
|||
}
|
||||
final String contact = conversation.getJid().getDomain().toEscapedString();
|
||||
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 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) {
|
||||
String otherBody;
|
||||
if (message.hasFileOnRemoteHost()) {
|
||||
otherBody = message.getFileParams().url.toString();
|
||||
otherBody = message.getFileParams().url;
|
||||
} else {
|
||||
otherBody = message.body;
|
||||
}
|
||||
|
|
|
@ -12,8 +12,6 @@ import com.google.common.collect.ImmutableSet;
|
|||
import org.json.JSONException;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
@ -22,6 +20,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
|
||||
import eu.siacs.conversations.http.URL;
|
||||
import eu.siacs.conversations.services.AvatarService;
|
||||
import eu.siacs.conversations.ui.util.PresenceSelector;
|
||||
import eu.siacs.conversations.utils.CryptoHelper;
|
||||
|
@ -547,7 +546,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
|||
} else {
|
||||
String body, otherBody;
|
||||
if (this.hasFileOnRemoteHost()) {
|
||||
body = getFileParams().url.toString();
|
||||
body = getFileParams().url;
|
||||
otherBody = message.body == null ? null : message.body.trim();
|
||||
} else {
|
||||
body = this.body;
|
||||
|
@ -794,12 +793,11 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
|||
if (relativeFilePath != null) {
|
||||
extension = MimeUtils.extractRelevantExtension(relativeFilePath);
|
||||
} else {
|
||||
try {
|
||||
final URL url = new URL(body.split("\n")[0]);
|
||||
extension = MimeUtils.extractRelevantExtension(url);
|
||||
} catch (MalformedURLException e) {
|
||||
final String url = URL.tryParse(body.split("\n")[0]);
|
||||
if (url == null) {
|
||||
return null;
|
||||
}
|
||||
extension = MimeUtils.extractRelevantExtension(url);
|
||||
}
|
||||
return MimeUtils.guessMimeTypeFromExtension(extension);
|
||||
}
|
||||
|
@ -840,8 +838,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
|||
case 1:
|
||||
try {
|
||||
fileParams.size = Long.parseLong(parts[0]);
|
||||
} catch (NumberFormatException e) {
|
||||
fileParams.url = parseUrl(parts[0]);
|
||||
} catch (final NumberFormatException e) {
|
||||
fileParams.url = URL.tryParse(parts[0]);
|
||||
}
|
||||
break;
|
||||
case 5:
|
||||
|
@ -850,7 +848,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
|||
fileParams.width = parseInt(parts[2]);
|
||||
fileParams.height = parseInt(parts[3]);
|
||||
case 2:
|
||||
fileParams.url = parseUrl(parts[0]);
|
||||
fileParams.url = URL.tryParse(parts[0]);
|
||||
fileParams.size = parseLong(parts[1]);
|
||||
break;
|
||||
case 3:
|
||||
|
@ -879,14 +877,6 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
|||
}
|
||||
}
|
||||
|
||||
private static URL parseUrl(String value) {
|
||||
try {
|
||||
return new URL(value);
|
||||
} catch (MalformedURLException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void untie() {
|
||||
this.mNextMessage = null;
|
||||
this.mPreviousMessage = null;
|
||||
|
@ -900,6 +890,11 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
|||
return type == TYPE_FILE || type == TYPE_IMAGE || type == TYPE_PRIVATE_FILE;
|
||||
}
|
||||
|
||||
|
||||
public boolean isTypeText() {
|
||||
return type == TYPE_TEXT || type == TYPE_PRIVATE;
|
||||
}
|
||||
|
||||
public boolean hasFileOnRemoteHost() {
|
||||
return isFileOrImage() && getFileParams().url != null;
|
||||
}
|
||||
|
@ -908,8 +903,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
|||
return isFileOrImage() && getFileParams().url == null;
|
||||
}
|
||||
|
||||
public class FileParams {
|
||||
public URL url;
|
||||
public static class FileParams {
|
||||
public String url;
|
||||
public long size = 0;
|
||||
public int width = 0;
|
||||
public int height = 0;
|
||||
|
|
|
@ -408,20 +408,6 @@ public class IqGenerator extends AbstractGenerator {
|
|||
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) {
|
||||
int pos = name.indexOf('.');
|
||||
if (pos != -1) {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package eu.siacs.conversations.generator;
|
||||
|
||||
import java.net.URL;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
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.Conversational;
|
||||
import eu.siacs.conversations.entities.Message;
|
||||
import eu.siacs.conversations.http.P1S3UrlStreamHandler;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
import eu.siacs.conversations.xml.Namespace;
|
||||
|
@ -103,18 +101,9 @@ public class MessageGenerator extends AbstractGenerator {
|
|||
MessagePacket packet = preparePacket(message);
|
||||
String content;
|
||||
if (message.hasFileOnRemoteHost()) {
|
||||
Message.FileParams fileParams = message.getFileParams();
|
||||
final URL url = fileParams.url;
|
||||
if (P1S3UrlStreamHandler.PROTOCOL_NAME.equals(url.getProtocol())) {
|
||||
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();
|
||||
final Message.FileParams fileParams = message.getFileParams();
|
||||
content = fileParams.url;
|
||||
packet.addChild("x", Namespace.OOB).addChild("url").setContent(content);
|
||||
}
|
||||
} else {
|
||||
content = message.getBody();
|
||||
}
|
||||
|
@ -126,16 +115,9 @@ public class MessageGenerator extends AbstractGenerator {
|
|||
MessagePacket packet = preparePacket(message);
|
||||
if (message.hasFileOnRemoteHost()) {
|
||||
Message.FileParams fileParams = message.getFileParams();
|
||||
final URL url = fileParams.url;
|
||||
if (P1S3UrlStreamHandler.PROTOCOL_NAME.equals(url.getProtocol())) {
|
||||
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());
|
||||
} else {
|
||||
packet.setBody(url.toString());
|
||||
packet.addChild("x", Namespace.OOB).addChild("url").setContent(url.toString());
|
||||
}
|
||||
final String url = fileParams.url;
|
||||
packet.setBody(url);
|
||||
packet.addChild("x", Namespace.OOB).addChild("url").setContent(url);
|
||||
} else {
|
||||
if (Config.supportUnencrypted()) {
|
||||
packet.setBody(PGP_FALLBACK_MESSAGE);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,23 +1,25 @@
|
|||
package eu.siacs.conversations.http;
|
||||
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import org.apache.http.conn.ssl.StrictHostnameVerifier;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Proxy;
|
||||
import java.net.URL;
|
||||
import java.net.UnknownHostException;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
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.XmppConnectionService;
|
||||
import eu.siacs.conversations.utils.TLSSocketFactory;
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.ResponseBody;
|
||||
|
||||
public class HttpConnectionManager extends AbstractConnectionManager {
|
||||
|
||||
|
@ -39,8 +45,18 @@ public class HttpConnectionManager extends AbstractConnectionManager {
|
|||
super(service);
|
||||
}
|
||||
|
||||
public static Proxy getProxy() throws IOException {
|
||||
return new Proxy(Proxy.Type.SOCKS, new InetSocketAddress(InetAddress.getByAddress(new byte[]{127, 0, 0, 1}), 9050));
|
||||
public static Proxy getProxy() {
|
||||
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) {
|
||||
|
@ -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) {
|
||||
synchronized (this.downloadConnections) {
|
||||
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 HostnameVerifier hostnameVerifier = mXmppConnectionService.getMemorizingTrustManager().wrapHostnameVerifier(new StrictHostnameVerifier(), interactive);
|
||||
if (interactive) {
|
||||
|
@ -106,9 +127,27 @@ public class HttpConnectionManager extends AbstractConnectionManager {
|
|||
}
|
||||
try {
|
||||
final SSLSocketFactory sf = new TLSSocketFactory(new X509TrustManager[]{trustManager}, mXmppConnectionService.getRNG());
|
||||
connection.setSSLSocketFactory(sf);
|
||||
connection.setHostnameVerifier(hostnameVerifier);
|
||||
builder.sslSocketFactory(sf, trustManager);
|
||||
builder.hostnameVerifier(hostnameVerifier);
|
||||
} 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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,29 +1,23 @@
|
|||
package eu.siacs.conversations.http;
|
||||
|
||||
import android.os.PowerManager;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.io.ByteStreams;
|
||||
import com.google.common.primitives.Longs;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLHandshakeException;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.R;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.entities.DownloadableFile;
|
||||
import eu.siacs.conversations.entities.Message;
|
||||
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.FileWriterException;
|
||||
import eu.siacs.conversations.utils.MimeUtils;
|
||||
import eu.siacs.conversations.utils.WakeLockHelper;
|
||||
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
||||
import static eu.siacs.conversations.http.HttpConnectionManager.EXECUTOR;
|
||||
|
||||
public class HttpDownloadConnection implements Transferable {
|
||||
|
||||
private final Message message;
|
||||
private final boolean mUseTor;
|
||||
private final HttpConnectionManager mHttpConnectionManager;
|
||||
private final XmppConnectionService mXmppConnectionService;
|
||||
private URL mUrl;
|
||||
private HttpUrl mUrl;
|
||||
private DownloadableFile file;
|
||||
private int mStatus = Transferable.STATUS_UNKNOWN;
|
||||
private boolean acceptedAutomatically = false;
|
||||
private int mProgress = 0;
|
||||
private boolean canceled = false;
|
||||
private Method method = Method.HTTP_UPLOAD;
|
||||
private Call mostRecentCall;
|
||||
|
||||
HttpDownloadConnection(Message message, HttpConnectionManager manager) {
|
||||
this.message = message;
|
||||
this.mHttpConnectionManager = manager;
|
||||
this.mXmppConnectionService = manager.getXmppConnectionService();
|
||||
this.mUseTor = mXmppConnectionService.useTorToConnect();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -88,13 +82,13 @@ public class HttpDownloadConnection implements Transferable {
|
|||
try {
|
||||
final Message.FileParams fileParams = message.getFileParams();
|
||||
if (message.hasFileOnRemoteHost()) {
|
||||
mUrl = CryptoHelper.toHttpsUrl(fileParams.url);
|
||||
mUrl = AesGcmURL.of(fileParams.url);
|
||||
} else if (message.isOOb() && fileParams.url != null && fileParams.size > 0) {
|
||||
mUrl = fileParams.url;
|
||||
mUrl = AesGcmURL.of(fileParams.url);
|
||||
} 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)) {
|
||||
this.message.setEncryption(Message.ENCRYPTION_PGP);
|
||||
} 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) {
|
||||
this.message.setEncryption(Message.ENCRYPTION_NONE);
|
||||
}
|
||||
method = mUrl.getProtocol().equalsIgnoreCase(P1S3UrlStreamHandler.PROTOCOL_NAME) ? Method.P1_S3 : Method.HTTP_UPLOAD;
|
||||
long knownFileSize = message.getFileParams().size;
|
||||
if (knownFileSize > 0 && interactive && method != Method.P1_S3) {
|
||||
//TODO add auth tag size to knownFileSize
|
||||
final long knownFileSize = message.getFileParams().size;
|
||||
if (knownFileSize > 0 && interactive) {
|
||||
this.file.setExpectedSize(knownFileSize);
|
||||
download(true);
|
||||
} else {
|
||||
checkFileSize(interactive);
|
||||
}
|
||||
} catch (MalformedURLException e) {
|
||||
} catch (final IllegalArgumentException e) {
|
||||
this.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
private void setupFile() {
|
||||
final String reference = mUrl.getRef();
|
||||
if (reference != null && AesGcmURLStreamHandler.IV_KEY.matcher(reference).matches()) {
|
||||
final String reference = mUrl.fragment();
|
||||
if (reference != null && AesGcmURL.IV_KEY.matcher(reference).matches()) {
|
||||
this.file = new DownloadableFile(mXmppConnectionService.getCacheDir().getAbsolutePath() + "/" + message.getUuid());
|
||||
this.file.setKeyAndIv(CryptoHelper.hexToBytes(reference));
|
||||
Log.d(Config.LOGTAG, "create temporary OMEMO encrypted file: " + this.file.getAbsolutePath() + "(" + message.getMimeType() + ")");
|
||||
|
@ -145,7 +139,10 @@ public class HttpDownloadConnection implements Transferable {
|
|||
|
||||
@Override
|
||||
public void cancel() {
|
||||
this.canceled = true;
|
||||
final Call call = this.mostRecentCall;
|
||||
if (call != null && !call.isCanceled()) {
|
||||
call.cancel();
|
||||
}
|
||||
mHttpConnectionManager.finishConnection(this);
|
||||
message.setTransferable(null);
|
||||
if (message.isFileOrImage()) {
|
||||
|
@ -209,14 +206,19 @@ public class HttpDownloadConnection implements Transferable {
|
|||
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) {
|
||||
mXmppConnectionService.showErrorToastInUi(R.string.download_failed_server_not_found);
|
||||
} else if (e instanceof java.net.ConnectException) {
|
||||
mXmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_connect);
|
||||
} else if (e instanceof FileWriterException) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -260,41 +262,13 @@ public class HttpDownloadConnection implements Transferable {
|
|||
|
||||
@Override
|
||||
public void run() {
|
||||
if (mUrl.getProtocol().equalsIgnoreCase(P1S3UrlStreamHandler.PROTOCOL_NAME)) {
|
||||
retrieveUrl();
|
||||
} else {
|
||||
check();
|
||||
}
|
||||
}
|
||||
|
||||
private void retrieveUrl() {
|
||||
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) {
|
||||
private void retrieveFailed(@Nullable final Exception e) {
|
||||
changeStatus(STATUS_OFFER_CHECK_FILESIZE);
|
||||
if (interactive) {
|
||||
if (e != null) {
|
||||
showToastForException(e);
|
||||
}
|
||||
} else {
|
||||
HttpDownloadConnection.this.acceptedAutomatically = false;
|
||||
HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message);
|
||||
|
@ -306,7 +280,7 @@ public class HttpDownloadConnection implements Transferable {
|
|||
long size;
|
||||
try {
|
||||
size = retrieveFileSize();
|
||||
} catch (Exception e) {
|
||||
} catch (final Exception e) {
|
||||
Log.d(Config.LOGTAG, "io exception in http file size checker: " + e.getMessage());
|
||||
retrieveFailed(e);
|
||||
return;
|
||||
|
@ -330,46 +304,23 @@ public class HttpDownloadConnection implements Transferable {
|
|||
}
|
||||
|
||||
private long retrieveFileSize() throws IOException {
|
||||
try {
|
||||
Log.d(Config.LOGTAG, "retrieve file size. interactive:" + interactive);
|
||||
changeStatus(STATUS_CHECKING);
|
||||
HttpURLConnection connection;
|
||||
final String hostname = mUrl.getHost();
|
||||
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());
|
||||
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 {
|
||||
final Response response = mostRecentCall.execute();
|
||||
final String contentLength = response.header("Content-Length");
|
||||
final String contentType = response.header("Content-Type");
|
||||
final AbstractConnectionManager.Extension extension = AbstractConnectionManager.Extension.of(mUrl.encodedPath());
|
||||
if (Strings.isNullOrEmpty(extension.getExtension()) && contentType != null) {
|
||||
final String fileExtension = MimeUtils.guessExtensionFromMimeType(contentType);
|
||||
if (fileExtension != null) {
|
||||
|
@ -378,8 +329,7 @@ public class HttpDownloadConnection implements Transferable {
|
|||
setupFile();
|
||||
}
|
||||
}
|
||||
connection.disconnect();
|
||||
if (contentLength == null) {
|
||||
if (Strings.isNullOrEmpty(contentLength)) {
|
||||
throw new IOException("no content-length found in HEAD response");
|
||||
}
|
||||
return Long.parseLong(contentLength, 10);
|
||||
|
@ -397,8 +347,6 @@ public class HttpDownloadConnection implements Transferable {
|
|||
|
||||
private final boolean interactive;
|
||||
|
||||
private OutputStream os;
|
||||
|
||||
public FileDownloader(boolean interactive) {
|
||||
this.interactive = interactive;
|
||||
}
|
||||
|
@ -411,9 +359,10 @@ public class HttpDownloadConnection implements Transferable {
|
|||
decryptIfNeeded();
|
||||
updateImageBounds();
|
||||
finish();
|
||||
} catch (SSLHandshakeException e) {
|
||||
} catch (final SSLHandshakeException e) {
|
||||
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) {
|
||||
showToastForException(e);
|
||||
} else {
|
||||
|
@ -425,104 +374,77 @@ public class HttpDownloadConnection implements Transferable {
|
|||
}
|
||||
|
||||
private void download() throws Exception {
|
||||
InputStream is = null;
|
||||
HttpURLConnection connection = null;
|
||||
final PowerManager.WakeLock wakeLock = mHttpConnectionManager.createWakeLock(Thread.currentThread());
|
||||
try {
|
||||
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 OkHttpClient client = mHttpConnectionManager.buildHttpClient(
|
||||
mUrl,
|
||||
message.getConversation().getAccount(),
|
||||
interactive
|
||||
);
|
||||
|
||||
final Request.Builder requestBuilder = new Request.Builder().url(URL.stripFragment(mUrl));
|
||||
|
||||
final long expected = file.getExpectedSize();
|
||||
final boolean tryResume = file.exists() && file.getSize() > 0 && file.getSize() < expected;
|
||||
long resumeSize = 0;
|
||||
|
||||
final long resumeSize;
|
||||
if (tryResume) {
|
||||
resumeSize = file.getSize();
|
||||
Log.d(Config.LOGTAG, "http download trying resume after " + resumeSize + " of " + expected);
|
||||
connection.setRequestProperty("Range", "bytes=" + resumeSize + "-");
|
||||
requestBuilder.addHeader("Range", String.format(Locale.ENGLISH, "bytes=%d-", resumeSize));
|
||||
} else {
|
||||
resumeSize = 0;
|
||||
}
|
||||
connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
|
||||
connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000);
|
||||
connection.connect();
|
||||
is = new BufferedInputStream(connection.getInputStream());
|
||||
final String contentRange = connection.getHeaderField("Content-Range");
|
||||
boolean serverResumed = tryResume && contentRange != null && contentRange.startsWith("bytes " + resumeSize + "-");
|
||||
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;
|
||||
if (tryResume && serverResumed) {
|
||||
Log.d(Config.LOGTAG, "server resumed");
|
||||
transmitted = file.getSize();
|
||||
updateProgress(Math.round(((double) transmitted / expected) * 100));
|
||||
os = AbstractConnectionManager.createOutputStream(file, true, false);
|
||||
if (os == null) {
|
||||
throw new FileWriterException();
|
||||
}
|
||||
outputStream = AbstractConnectionManager.createOutputStream(file, true, false);
|
||||
} else {
|
||||
long reportedContentLengthOnGet;
|
||||
try {
|
||||
reportedContentLengthOnGet = Long.parseLong(connection.getHeaderField("Content-Length"));
|
||||
} catch (NumberFormatException | NullPointerException e) {
|
||||
reportedContentLengthOnGet = 0;
|
||||
}
|
||||
if (expected != reportedContentLengthOnGet) {
|
||||
Log.d(Config.LOGTAG, "content-length reported on GET (" + reportedContentLengthOnGet + ") did not match Content-Length reported on HEAD (" + expected + ")");
|
||||
final String contentLength = response.header("Content-Length");
|
||||
final long size = Strings.isNullOrEmpty(contentLength) ? 0 : Longs.tryParse(contentLength);
|
||||
if (expected != size) {
|
||||
Log.d(Config.LOGTAG, "content-length reported on GET (" + size + ") did not match Content-Length reported on HEAD (" + expected + ")");
|
||||
}
|
||||
file.getParentFile().mkdirs();
|
||||
if (!file.exists() && !file.createNewFile()) {
|
||||
throw new FileWriterException();
|
||||
}
|
||||
os = AbstractConnectionManager.createOutputStream(file, false, false);
|
||||
outputStream = AbstractConnectionManager.createOutputStream(file, false, false);
|
||||
}
|
||||
int count;
|
||||
byte[] buffer = new byte[4096];
|
||||
while ((count = is.read(buffer)) != -1) {
|
||||
final byte[] buffer = new byte[4096];
|
||||
while ((count = inputStream.read(buffer)) != -1) {
|
||||
transmitted += count;
|
||||
try {
|
||||
os.write(buffer, 0, count);
|
||||
outputStream.write(buffer, 0, count);
|
||||
} catch (IOException e) {
|
||||
throw new FileWriterException();
|
||||
}
|
||||
updateProgress(Math.round(((double) transmitted / expected) * 100));
|
||||
if (canceled) {
|
||||
throw new CancellationException();
|
||||
}
|
||||
}
|
||||
try {
|
||||
os.flush();
|
||||
} catch (IOException e) {
|
||||
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);
|
||||
outputStream.flush();
|
||||
} else {
|
||||
throw new IOException(String.format(Locale.ENGLISH, "HTTP Status code was %d", code));
|
||||
}
|
||||
}
|
||||
|
||||
private void updateImageBounds() {
|
||||
final boolean privateMessage = message.isPrivateMessage();
|
||||
message.setType(privateMessage ? Message.TYPE_PRIVATE_FILE : Message.TYPE_FILE);
|
||||
final URL url;
|
||||
final String ref = mUrl.getRef();
|
||||
if (method == Method.P1_S3) {
|
||||
url = message.getFileParams().url;
|
||||
} else if (ref != null && AesGcmURLStreamHandler.IV_KEY.matcher(ref).matches()) {
|
||||
url = CryptoHelper.toAesGcmUrl(mUrl);
|
||||
final String url;
|
||||
final String ref = mUrl.fragment();
|
||||
if (ref != null && AesGcmURL.IV_KEY.matcher(ref).matches()) {
|
||||
url = AesGcmURL.toAesGcmUrl(mUrl);
|
||||
} else {
|
||||
url = mUrl;
|
||||
url = mUrl.toString();
|
||||
}
|
||||
mXmppConnectionService.getFileBackend().updateFileParams(message, url);
|
||||
mXmppConnectionService.updateMessage(message);
|
||||
|
|
|
@ -1,35 +1,36 @@
|
|||
package eu.siacs.conversations.http;
|
||||
|
||||
import android.os.PowerManager;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Scanner;
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
|
||||
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.entities.Account;
|
||||
import eu.siacs.conversations.entities.DownloadableFile;
|
||||
import eu.siacs.conversations.entities.Message;
|
||||
import eu.siacs.conversations.entities.Transferable;
|
||||
import eu.siacs.conversations.persistance.FileBackend;
|
||||
import eu.siacs.conversations.services.AbstractConnectionManager;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
import eu.siacs.conversations.utils.Checksum;
|
||||
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 {
|
||||
public class HttpUploadConnection implements Transferable, AbstractConnectionManager.ProgressListener {
|
||||
|
||||
static final List<String> WHITE_LISTED_HEADERS = Arrays.asList(
|
||||
"Authorization",
|
||||
|
@ -39,10 +40,7 @@ public class HttpUploadConnection implements Transferable {
|
|||
|
||||
private final HttpConnectionManager mHttpConnectionManager;
|
||||
private final XmppConnectionService mXmppConnectionService;
|
||||
private final SlotRequester mSlotRequester;
|
||||
private final Method method;
|
||||
private final boolean mUseTor;
|
||||
private boolean cancelled = false;
|
||||
private boolean delayed = false;
|
||||
private DownloadableFile file;
|
||||
private final Message message;
|
||||
|
@ -51,14 +49,14 @@ public class HttpUploadConnection implements Transferable {
|
|||
private byte[] key = null;
|
||||
|
||||
private long transmitted = 0;
|
||||
private Call mostRecentCall;
|
||||
private ListenableFuture<SlotRequester.Slot> slotFuture;
|
||||
|
||||
public HttpUploadConnection(Message message, Method method, HttpConnectionManager httpConnectionManager) {
|
||||
this.message = message;
|
||||
this.method = method;
|
||||
this.mHttpConnectionManager = httpConnectionManager;
|
||||
this.mXmppConnectionService = httpConnectionManager.getXmppConnectionService();
|
||||
this.mSlotRequester = new SlotRequester(this.mXmppConnectionService);
|
||||
this.mUseTor = mXmppConnectionService.useTorToConnect();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -86,14 +84,29 @@ public class HttpUploadConnection implements Transferable {
|
|||
|
||||
@Override
|
||||
public void cancel() {
|
||||
this.cancelled = true;
|
||||
final ListenableFuture<SlotRequester.Slot> slotFuture = this.slotFuture;
|
||||
if (slotFuture != null && !slotFuture.isDone()) {
|
||||
slotFuture.cancel(true);
|
||||
}
|
||||
final Call call = this.mostRecentCall;
|
||||
if (call != null && !call.isCanceled()) {
|
||||
call.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
private void fail(String errorMessage) {
|
||||
finish();
|
||||
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 markAsCancelled() {
|
||||
finish();
|
||||
mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED, Message.ERROR_MESSAGE_CANCELLED);
|
||||
}
|
||||
|
||||
private void finish() {
|
||||
mHttpConnectionManager.finishUploadConnection(this);
|
||||
message.setTransferable(null);
|
||||
|
@ -116,108 +129,57 @@ public class HttpUploadConnection implements Transferable {
|
|||
mXmppConnectionService.getRNG().nextBytes(this.key);
|
||||
this.file.setKeyAndIv(this.key);
|
||||
}
|
||||
|
||||
final String md5;
|
||||
|
||||
if (method == Method.P1_S3) {
|
||||
try {
|
||||
md5 = Checksum.md5(AbstractConnectionManager.upgrade(file, new FileInputStream(file)));
|
||||
} catch (Exception e) {
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid()+": unable to calculate md5()", e);
|
||||
fail(e.getMessage());
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
md5 = null;
|
||||
}
|
||||
|
||||
this.file.setExpectedSize(originalFileSize + (file.getKey() != null ? 16 : 0));
|
||||
message.resetFileParams();
|
||||
this.mSlotRequester.request(method, account, file, mime, md5, new SlotRequester.OnSlotRequested() {
|
||||
this.slotFuture = new SlotRequester(mXmppConnectionService).request(method, account, file, mime);
|
||||
Futures.addCallback(this.slotFuture, new FutureCallback<SlotRequester.Slot>() {
|
||||
@Override
|
||||
public void success(SlotRequester.Slot slot) {
|
||||
if (!cancelled) {
|
||||
HttpUploadConnection.this.slot = slot;
|
||||
EXECUTOR.execute(HttpUploadConnection.this::upload);
|
||||
}
|
||||
public void onSuccess(@NullableDecl SlotRequester.Slot result) {
|
||||
HttpUploadConnection.this.slot = result;
|
||||
HttpUploadConnection.this.upload();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void failure(String message) {
|
||||
fail(message);
|
||||
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);
|
||||
}
|
||||
|
||||
private void upload() {
|
||||
OutputStream os = null;
|
||||
InputStream fileInputStream = null;
|
||||
HttpURLConnection connection = null;
|
||||
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");
|
||||
final OkHttpClient client = mHttpConnectionManager.buildHttpClient(
|
||||
slot.put,
|
||||
message.getConversation().getAccount(),
|
||||
true
|
||||
);
|
||||
final RequestBody requestBody = AbstractConnectionManager.requestBody(file, this);
|
||||
final Request request = new Request.Builder()
|
||||
.url(slot.put)
|
||||
.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());
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
|
||||
final int code = response.code();
|
||||
if (code == 200 || code == 201) {
|
||||
Log.d(Config.LOGTAG, "finished uploading file");
|
||||
final URL get;
|
||||
final String get;
|
||||
if (key != null) {
|
||||
if (method == Method.P1_S3) {
|
||||
get = new URL(slot.getGetUrl().toString()+"#"+CryptoHelper.bytesToHex(key));
|
||||
get = AesGcmURL.toAesGcmUrl(slot.get.newBuilder().fragment(CryptoHelper.bytesToHex(key)).build());
|
||||
} else {
|
||||
get = CryptoHelper.toAesGcmUrl(new URL(slot.getGetUrl().toString() + "#" + CryptoHelper.bytesToHex(key)));
|
||||
}
|
||||
} else {
|
||||
get = slot.getGetUrl();
|
||||
get = slot.get.toString();
|
||||
}
|
||||
mXmppConnectionService.getFileBackend().updateFileParams(message, get);
|
||||
mXmppConnectionService.getFileBackend().updateMediaScanner(file);
|
||||
|
@ -230,21 +192,17 @@ public class HttpUploadConnection implements Transferable {
|
|||
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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProgress(final long progress) {
|
||||
this.transmitted = progress;
|
||||
mHttpConnectionManager.updateConversationUi(false);
|
||||
}
|
||||
}
|
|
@ -33,7 +33,7 @@ import eu.siacs.conversations.entities.Account;
|
|||
import eu.siacs.conversations.xmpp.XmppConnection;
|
||||
|
||||
public enum Method {
|
||||
P1_S3, HTTP_UPLOAD, HTTP_UPLOAD_LEGACY;
|
||||
HTTP_UPLOAD, HTTP_UPLOAD_LEGACY;
|
||||
|
||||
public static Method determine(Account account) {
|
||||
XmppConnection.Features features = account.getXmppConnection() == null ? null : account.getXmppConnection().getFeatures();
|
||||
|
@ -44,8 +44,6 @@ public enum Method {
|
|||
return HTTP_UPLOAD_LEGACY;
|
||||
} else if (features.httpUpload(0)) {
|
||||
return HTTP_UPLOAD;
|
||||
} else if (features.p1S3FileTransfer()) {
|
||||
return P1_S3;
|
||||
} else {
|
||||
return HTTP_UPLOAD;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -29,21 +29,23 @@
|
|||
|
||||
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.net.URL;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.entities.DownloadableFile;
|
||||
import eu.siacs.conversations.parser.IqParser;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
import eu.siacs.conversations.xml.Namespace;
|
||||
import eu.siacs.conversations.xmpp.IqResponseException;
|
||||
import eu.siacs.conversations.xmpp.Jid;
|
||||
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
|
||||
import okhttp3.Headers;
|
||||
import okhttp3.HttpUrl;
|
||||
|
||||
public class SlotRequester {
|
||||
|
||||
|
@ -53,51 +55,52 @@ public class SlotRequester {
|
|||
this.service = service;
|
||||
}
|
||||
|
||||
public void request(Method method, Account account, DownloadableFile file, String mime, String md5, OnSlotRequested callback) {
|
||||
if (method == Method.HTTP_UPLOAD) {
|
||||
Jid host = account.getXmppConnection().findDiscoItemByFeature(Namespace.HTTP_UPLOAD);
|
||||
requestHttpUpload(account, host, file, mime, callback);
|
||||
} else if (method == Method.HTTP_UPLOAD_LEGACY) {
|
||||
Jid host = account.getXmppConnection().findDiscoItemByFeature(Namespace.HTTP_UPLOAD_LEGACY);
|
||||
requestHttpUploadLegacy(account, host, file, mime, callback);
|
||||
public ListenableFuture<Slot> request(Method method, Account account, DownloadableFile file, String mime) {
|
||||
if (method == Method.HTTP_UPLOAD_LEGACY) {
|
||||
final Jid host = account.getXmppConnection().findDiscoItemByFeature(Namespace.HTTP_UPLOAD_LEGACY);
|
||||
return requestHttpUploadLegacy(account, host, file, mime);
|
||||
} else {
|
||||
requestP1S3(account, account.getDomain(), file.getName(), md5, callback);
|
||||
final Jid host = account.getXmppConnection().findDiscoItemByFeature(Namespace.HTTP_UPLOAD);
|
||||
return requestHttpUpload(account, host, file, mime);
|
||||
}
|
||||
}
|
||||
|
||||
private void requestHttpUploadLegacy(Account account, Jid host, DownloadableFile file, String mime, OnSlotRequested callback) {
|
||||
IqPacket request = service.getIqGenerator().requestHttpUploadLegacySlot(host, file, mime);
|
||||
private ListenableFuture<Slot> requestHttpUploadLegacy(Account account, Jid host, DownloadableFile file, String mime) {
|
||||
final SettableFuture<Slot> future = SettableFuture.create();
|
||||
final IqPacket request = service.getIqGenerator().requestHttpUploadLegacySlot(host, file, mime);
|
||||
service.sendIqPacket(account, request, (a, packet) -> {
|
||||
if (packet.getType() == IqPacket.TYPE.RESULT) {
|
||||
Element slotElement = packet.findChild("slot", Namespace.HTTP_UPLOAD_LEGACY);
|
||||
final Element slotElement = packet.findChild("slot", Namespace.HTTP_UPLOAD_LEGACY);
|
||||
if (slotElement != null) {
|
||||
try {
|
||||
final String putUrl = slotElement.findChildContent("put");
|
||||
final String getUrl = slotElement.findChildContent("get");
|
||||
if (getUrl != null && putUrl != null) {
|
||||
Slot slot = new Slot(new URL(putUrl));
|
||||
slot.getUrl = new URL(getUrl);
|
||||
slot.headers = new HashMap<>();
|
||||
slot.headers.put("Content-Type", mime == null ? "application/octet-stream" : mime);
|
||||
callback.success(slot);
|
||||
final Slot slot = new Slot(
|
||||
HttpUrl.get(putUrl),
|
||||
HttpUrl.get(getUrl),
|
||||
Headers.of("Content-Type", mime == null ? "application/octet-stream" : mime)
|
||||
);
|
||||
future.set(slot);
|
||||
return;
|
||||
}
|
||||
} catch (MalformedURLException e) {
|
||||
//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 void requestHttpUpload(Account account, Jid host, DownloadableFile file, String mime, OnSlotRequested callback) {
|
||||
IqPacket request = service.getIqGenerator().requestHttpUploadSlot(host, file, mime);
|
||||
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) {
|
||||
Element slotElement = packet.findChild("slot", Namespace.HTTP_UPLOAD);
|
||||
final Element slotElement = packet.findChild("slot", Namespace.HTTP_UPLOAD);
|
||||
if (slotElement != null) {
|
||||
try {
|
||||
final Element put = slotElement.findChild("put");
|
||||
|
@ -105,86 +108,47 @@ public class SlotRequester {
|
|||
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()) {
|
||||
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")) {
|
||||
slot.headers.put(name, value.trim());
|
||||
headers.put(name, value.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
slot.headers.put("Content-Type", mime == null ? "application/octet-stream" : mime);
|
||||
callback.success(slot);
|
||||
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 (MalformedURLException e) {
|
||||
//fall through
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.d(Config.LOGTAG, account.getJid().toString() + ": invalid response to slot request " + packet);
|
||||
callback.failure(IqParser.extractErrorMessage(packet));
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private void requestP1S3(final Account account, Jid host, String filename, String md5, OnSlotRequested callback) {
|
||||
IqPacket request = service.getIqGenerator().requestP1S3Slot(host, md5);
|
||||
service.sendIqPacket(account, request, (a, packet) -> {
|
||||
if (packet.getType() == IqPacket.TYPE.RESULT) {
|
||||
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);
|
||||
} catch (final IllegalArgumentException e) {
|
||||
future.setException(e);
|
||||
return;
|
||||
}
|
||||
} catch (MalformedURLException e) {
|
||||
//fall through;
|
||||
}
|
||||
}
|
||||
callback.failure("unable to request slot");
|
||||
future.setException(new IqResponseException(IqParser.extractErrorMessage(packet)));
|
||||
});
|
||||
Log.d(Config.LOGTAG, "requesting slot with p1. md5=" + md5);
|
||||
}
|
||||
|
||||
|
||||
public interface OnSlotRequested {
|
||||
|
||||
void success(Slot slot);
|
||||
|
||||
void failure(String message);
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
public static class Slot {
|
||||
private final URL putUrl;
|
||||
private URL getUrl;
|
||||
private HashMap<String, String> headers;
|
||||
public final HttpUrl put;
|
||||
public final HttpUrl get;
|
||||
public final Headers headers;
|
||||
|
||||
private Slot(URL putUrl) {
|
||||
this.putUrl = putUrl;
|
||||
private Slot(HttpUrl put, HttpUrl get, Headers headers) {
|
||||
this.put = put;
|
||||
this.get = get;
|
||||
this.headers = headers;
|
||||
}
|
||||
|
||||
public URL getPutUrl() {
|
||||
return putUrl;
|
||||
}
|
||||
|
||||
public URL getGetUrl() {
|
||||
return getUrl;
|
||||
}
|
||||
|
||||
public HashMap<String, String> getHeaders() {
|
||||
return headers;
|
||||
private Slot(HttpUrl put, HttpUrl getUrl, Map<String, String> headers) {
|
||||
this.put = put;
|
||||
this.get = getUrl;
|
||||
this.headers = Headers.of(headers);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -3,7 +3,6 @@ package eu.siacs.conversations.parser;
|
|||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import java.net.URL;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
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.RtpSessionStatus;
|
||||
import eu.siacs.conversations.http.HttpConnectionManager;
|
||||
import eu.siacs.conversations.http.P1S3UrlStreamHandler;
|
||||
import eu.siacs.conversations.services.MessageArchiveService;
|
||||
import eu.siacs.conversations.services.QuickConversationsService;
|
||||
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 Element replaceElement = packet.findChild("replace", "urn:xmpp:message-correct:0");
|
||||
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 replacementId = replaceElement == null ? null : replaceElement.getAttribute("id");
|
||||
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 Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, counterpart.asBareJid(), conversationIsProbablyMuc, false, query, false);
|
||||
final boolean conversationMultiMode = conversation.getMode() == Conversation.MODE_MULTI;
|
||||
|
@ -504,13 +500,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|
|||
}
|
||||
}
|
||||
final Message message;
|
||||
if (xP1S3url != null) {
|
||||
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()) {
|
||||
if (pgpEncrypted != null && Config.supportOpenPgp()) {
|
||||
message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status);
|
||||
} else if (axolotlEncrypted != null && Config.supportOmemo()) {
|
||||
Jid origin;
|
||||
|
|
|
@ -19,7 +19,6 @@ import eu.siacs.conversations.entities.Presence;
|
|||
import eu.siacs.conversations.generator.IqGenerator;
|
||||
import eu.siacs.conversations.generator.PresenceGenerator;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
import eu.siacs.conversations.utils.CryptoHelper;
|
||||
import eu.siacs.conversations.utils.XmppUri;
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
import eu.siacs.conversations.xml.Namespace;
|
||||
|
|
|
@ -18,7 +18,6 @@ import android.net.Uri;
|
|||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.os.SystemClock;
|
||||
import android.provider.MediaStore;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.system.Os;
|
||||
|
@ -30,6 +29,7 @@ import android.util.Log;
|
|||
import android.util.LruCache;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.content.FileProvider;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
@ -44,7 +44,6 @@ import java.io.InputStream;
|
|||
import java.io.OutputStream;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.net.URL;
|
||||
import java.security.DigestOutputStream;
|
||||
import java.security.MessageDigest;
|
||||
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();
|
||||
body.append(url.toString()).append('|').append(size);
|
||||
body.append(url).append('|').append(size);
|
||||
message.setBody(body.toString());
|
||||
}
|
||||
|
||||
|
@ -648,12 +647,13 @@ public class FileBackend {
|
|||
} catch (IOException e) {
|
||||
throw new FileWriterException();
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
} catch (final FileNotFoundException e) {
|
||||
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);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} catch (final SecurityException e) {
|
||||
throw new FileCopyException(R.string.error_security_exception);
|
||||
} catch (final IOException e) {
|
||||
throw new FileCopyException(R.string.error_io_exception);
|
||||
} finally {
|
||||
close(os);
|
||||
|
@ -1305,7 +1305,7 @@ public class FileBackend {
|
|||
updateFileParams(message, null);
|
||||
}
|
||||
|
||||
public void updateFileParams(Message message, URL url) {
|
||||
public void updateFileParams(Message message, String url) {
|
||||
DownloadableFile file = getFile(message);
|
||||
final String mime = file.getMimeType();
|
||||
final boolean privateMessage = message.isPrivateMessage();
|
||||
|
@ -1315,7 +1315,7 @@ public class FileBackend {
|
|||
final boolean pdf = "application/pdf".equals(mime);
|
||||
final StringBuilder body = new StringBuilder();
|
||||
if (url != null) {
|
||||
body.append(url.toString());
|
||||
body.append(url);
|
||||
}
|
||||
body.append('|').append(file.getSize());
|
||||
if (image || video || (pdf && Compatibility.runsTwentyOne())) {
|
||||
|
@ -1464,11 +1464,11 @@ public class FileBackend {
|
|||
public static class FileCopyException extends Exception {
|
||||
private final int resId;
|
||||
|
||||
private FileCopyException(int resId) {
|
||||
private FileCopyException(@StringRes int resId) {
|
||||
this.resId = resId;
|
||||
}
|
||||
|
||||
public int getResId() {
|
||||
public @StringRes int getResId() {
|
||||
return resId;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,22 +13,25 @@ import org.bouncycastle.crypto.modes.GCMBlockCipher;
|
|||
import org.bouncycastle.crypto.params.AEADParameters;
|
||||
import org.bouncycastle.crypto.params.KeyParameter;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
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 javax.crypto.NoSuchPaddingException;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.R;
|
||||
import eu.siacs.conversations.entities.DownloadableFile;
|
||||
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;
|
||||
|
||||
|
@ -42,7 +45,7 @@ public class AbstractConnectionManager {
|
|||
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) {
|
||||
AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine());
|
||||
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) {
|
||||
FileOutputStream os;
|
||||
try {
|
||||
|
@ -121,6 +161,7 @@ public class AbstractConnectionManager {
|
|||
}
|
||||
|
||||
public static Extension of(String path) {
|
||||
//TODO accept List<String> pathSegments
|
||||
final int pos = path.lastIndexOf('/');
|
||||
final String filename = path.substring(pos + 1).toLowerCase();
|
||||
final String[] parts = filename.split("\\.");
|
||||
|
|
|
@ -3,13 +3,10 @@ package eu.siacs.conversations.services;
|
|||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import net.ypresto.androidtranscoder.MediaTranscoder;
|
||||
import net.ypresto.androidtranscoder.format.MediaFormatStrategy;
|
||||
|
||||
|
|
|
@ -51,13 +51,8 @@ public class ChannelDiscoveryService {
|
|||
|
||||
void initializeMuclumbusService() {
|
||||
final OkHttpClient.Builder builder = new OkHttpClient.Builder();
|
||||
|
||||
if (service.useTorToConnect()) {
|
||||
try {
|
||||
builder.proxy(HttpConnectionManager.getProxy());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Unable to use Tor proxy", e);
|
||||
}
|
||||
}
|
||||
Retrofit retrofit = new Retrofit.Builder()
|
||||
.client(builder.build())
|
||||
|
@ -73,7 +68,7 @@ public class ChannelDiscoveryService {
|
|||
}
|
||||
|
||||
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) {
|
||||
onChannelSearchResultsFound.onChannelSearchResultsFound(result);
|
||||
return;
|
||||
|
|
|
@ -31,6 +31,7 @@ import android.app.NotificationManager;
|
|||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.preference.PreferenceManager;
|
||||
|
@ -40,6 +41,9 @@ import android.util.SparseArray;
|
|||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import com.google.common.base.Charsets;
|
||||
import com.google.common.io.CharStreams;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
@ -52,7 +56,6 @@ import java.io.FileOutputStream;
|
|||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.URL;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.MessageDigest;
|
||||
|
@ -74,7 +77,6 @@ import java.util.logging.Logger;
|
|||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLSession;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
|
@ -83,6 +85,7 @@ import javax.net.ssl.X509TrustManager;
|
|||
import eu.siacs.conversations.R;
|
||||
import eu.siacs.conversations.crypto.DomainHostnameVerifier;
|
||||
import eu.siacs.conversations.entities.MTMDecision;
|
||||
import eu.siacs.conversations.http.HttpConnectionManager;
|
||||
import eu.siacs.conversations.persistance.FileBackend;
|
||||
import eu.siacs.conversations.ui.MemorizingActivity;
|
||||
|
||||
|
@ -486,15 +489,18 @@ public class MemorizingTrustManager {
|
|||
defaultTrustManager.checkServerTrusted(chain, authType);
|
||||
else
|
||||
defaultTrustManager.checkClientTrusted(chain, authType);
|
||||
} catch (CertificateException e) {
|
||||
boolean trustSystemCAs = !PreferenceManager.getDefaultSharedPreferences(master).getBoolean("dont_trust_system_cas", false);
|
||||
if (domain != null && isServer && trustSystemCAs && !isIp(domain)) {
|
||||
} catch (final CertificateException e) {
|
||||
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(master);
|
||||
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 List<String> fingerprints = getPoshFingerprints(domain);
|
||||
if (hash != null && fingerprints.size() > 0) {
|
||||
if (fingerprints.contains(hash)) {
|
||||
Log.d("mtm", "trusted cert fingerprint of " + domain + " via posh");
|
||||
return;
|
||||
} else {
|
||||
Log.d("mtm", "fingerprint " + hash + " not found in " + fingerprints);
|
||||
}
|
||||
if (getPoshCacheFile(domain).delete()) {
|
||||
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) {
|
||||
List<String> cached = getPoshFingerprintsFromCache(domain);
|
||||
final List<String> cached = getPoshFingerprintsFromCache(domain);
|
||||
if (cached == null) {
|
||||
return getPoshFingerprintsFromServer(domain);
|
||||
} else {
|
||||
|
@ -525,19 +531,13 @@ public class MemorizingTrustManager {
|
|||
|
||||
private List<String> getPoshFingerprintsFromServer(String domain, String url, int maxTtl, boolean followUrl) {
|
||||
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 {
|
||||
List<String> results = new ArrayList<>();
|
||||
HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection();
|
||||
connection.setConnectTimeout(5000);
|
||||
connection.setReadTimeout(5000);
|
||||
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();
|
||||
final List<String> results = new ArrayList<>();
|
||||
final InputStream inputStream = HttpConnectionManager.open(url, useTor);
|
||||
final String body = CharStreams.toString(new InputStreamReader(inputStream, Charsets.UTF_8));
|
||||
final JSONObject jsonObject = new JSONObject(body);
|
||||
int expires = jsonObject.getInt("expires");
|
||||
if (expires <= 0) {
|
||||
return new ArrayList<>();
|
||||
|
@ -554,17 +554,15 @@ public class MemorizingTrustManager {
|
|||
if (followUrl && redirect != null && redirect.toLowerCase().startsWith("https")) {
|
||||
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++) {
|
||||
JSONObject fingerprint = fingerprints.getJSONObject(i);
|
||||
String sha256 = fingerprint.getString("sha-256");
|
||||
if (sha256 != null) {
|
||||
final JSONObject fingerprint = fingerprints.getJSONObject(i);
|
||||
final String sha256 = fingerprint.getString("sha-256");
|
||||
results.add(sha256);
|
||||
}
|
||||
}
|
||||
writeFingerprintsToCache(domain, results, 1000L * expires + System.currentTimeMillis());
|
||||
return results;
|
||||
} catch (Exception e) {
|
||||
} catch (final Exception e) {
|
||||
Log.d("mtm", "error fetching posh " + e.getMessage());
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
@ -575,7 +573,7 @@ public class MemorizingTrustManager {
|
|||
}
|
||||
|
||||
private void writeFingerprintsToCache(String domain, List<String> results, long expires) {
|
||||
File file = getPoshCacheFile(domain);
|
||||
final File file = getPoshCacheFile(domain);
|
||||
file.getParentFile().mkdirs();
|
||||
try {
|
||||
file.createNewFile();
|
||||
|
@ -592,20 +590,11 @@ public class MemorizingTrustManager {
|
|||
}
|
||||
|
||||
private List<String> getPoshFingerprintsFromCache(String domain) {
|
||||
File file = getPoshCacheFile(domain);
|
||||
final File file = getPoshCacheFile(domain);
|
||||
try {
|
||||
InputStream is = new FileInputStream(file);
|
||||
BufferedReader buf = new BufferedReader(new InputStreamReader(is));
|
||||
|
||||
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();
|
||||
final InputStream inputStream = new FileInputStream(file);
|
||||
final String json = CharStreams.toString(new InputStreamReader(inputStream, Charsets.UTF_8));
|
||||
final JSONObject jsonObject = new JSONObject(json);
|
||||
long expires = jsonObject.getLong("expires");
|
||||
long expiresIn = expires - System.currentTimeMillis();
|
||||
if (expiresIn < 0) {
|
||||
|
@ -614,15 +603,13 @@ public class MemorizingTrustManager {
|
|||
} else {
|
||||
Log.d("mtm", "posh fingerprints expire in " + (expiresIn / 1000) + "s");
|
||||
}
|
||||
List<String> result = new ArrayList<>();
|
||||
JSONArray jsonArray = jsonObject.getJSONArray("fingerprints");
|
||||
final List<String> result = new ArrayList<>();
|
||||
final JSONArray jsonArray = jsonObject.getJSONArray("fingerprints");
|
||||
for (int i = 0; i < jsonArray.length(); ++i) {
|
||||
result.add(jsonArray.getString(i));
|
||||
}
|
||||
return result;
|
||||
} catch (FileNotFoundException e) {
|
||||
return null;
|
||||
} catch (IOException e) {
|
||||
} catch (final IOException e) {
|
||||
return null;
|
||||
} catch (JSONException e) {
|
||||
file.delete();
|
||||
|
|
|
@ -209,7 +209,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
|
|||
}
|
||||
|
||||
void executePendingQueries(final Account account) {
|
||||
List<Query> pending = new ArrayList<>();
|
||||
final List<Query> pending = new ArrayList<>();
|
||||
synchronized (this.pendingQueries) {
|
||||
for (Iterator<Query> iterator = this.pendingQueries.iterator(); iterator.hasNext(); ) {
|
||||
Query query = iterator.next();
|
||||
|
@ -390,8 +390,17 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
|
|||
}
|
||||
}
|
||||
|
||||
void kill(Conversation conversation) {
|
||||
void kill(final Conversation conversation) {
|
||||
final ArrayList<Query> toBeKilled = new ArrayList<>();
|
||||
synchronized (this.pendingQueries) {
|
||||
for (final Iterator<Query> iterator = this.pendingQueries.iterator(); iterator.hasNext(); ) {
|
||||
final Query query = iterator.next();
|
||||
if (query.getConversation() == conversation) {
|
||||
iterator.remove();
|
||||
Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": killed pending MAM query for archived conversation");
|
||||
}
|
||||
}
|
||||
}
|
||||
synchronized (this.queries) {
|
||||
for (final Query q : queries) {
|
||||
if (q.conversation == conversation) {
|
||||
|
@ -399,7 +408,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
|
|||
}
|
||||
}
|
||||
}
|
||||
for (Query q : toBeKilled) {
|
||||
for (final Query q : toBeKilled) {
|
||||
kill(q);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,7 +55,6 @@ import org.openintents.openpgp.util.OpenPgpApi;
|
|||
import org.openintents.openpgp.util.OpenPgpServiceConnection;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.URL;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.Security;
|
||||
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.MessageGenerator;
|
||||
import eu.siacs.conversations.generator.PresenceGenerator;
|
||||
import eu.siacs.conversations.http.CustomURLStreamHandlerFactory;
|
||||
import eu.siacs.conversations.http.HttpConnectionManager;
|
||||
import eu.siacs.conversations.parser.AbstractParser;
|
||||
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";
|
||||
|
||||
static {
|
||||
URL.setURLStreamHandlerFactory(new CustomURLStreamHandlerFactory());
|
||||
}
|
||||
|
||||
public final CountDownLatch restoredFromDatabaseLatch = new CountDownLatch(1);
|
||||
private final SerialSingleThreadExecutor mFileAddingExecutor = new SerialSingleThreadExecutor("FileAdding");
|
||||
private final SerialSingleThreadExecutor mVideoCompressionExecutor = new SerialSingleThreadExecutor("VideoCompression");
|
||||
|
@ -663,6 +657,7 @@ public class XmppConnectionService extends Service {
|
|||
if (Config.RESET_ATTEMPT_COUNT_ON_NETWORK_CHANGE) {
|
||||
resetAllAttemptCounts(true, false);
|
||||
}
|
||||
Resolver.clearCache();
|
||||
}
|
||||
break;
|
||||
case Intent.ACTION_SHUTDOWN:
|
||||
|
@ -999,7 +994,10 @@ public class XmppConnectionService extends Service {
|
|||
|
||||
public boolean isScreenLocked() {
|
||||
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() {
|
||||
|
@ -1794,7 +1792,7 @@ public class XmppConnectionService extends Service {
|
|||
IqPacket request = mIqGenerator.deleteItem(Namespace.BOOKMARKS2, bookmark.getJid().asBareJid().toEscapedString());
|
||||
sendIqPacket(account, request, (a, response) -> {
|
||||
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()) {
|
||||
|
@ -2864,13 +2862,12 @@ public class XmppConnectionService extends Service {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onFetchFailed(final Conversation conversation, Element error) {
|
||||
public void onFetchFailed(final Conversation conversation, final String errorCondition) {
|
||||
if (conversation.getStatus() == Conversation.STATUS_ARCHIVED) {
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": conversation (" + conversation.getJid() + ") got archived before IQ result");
|
||||
|
||||
return;
|
||||
}
|
||||
if (error != null && "remote-server-not-found".equals(error.getName())) {
|
||||
if ("remote-server-not-found".equals(errorCondition)) {
|
||||
synchronized (account.inProgressConferenceJoins) {
|
||||
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");
|
||||
} else {
|
||||
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) {
|
||||
callback.onAvatarPublicationSucceeded();
|
||||
} 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);
|
||||
}
|
||||
});
|
||||
|
@ -3964,7 +3961,9 @@ public class XmppConnectionService extends Service {
|
|||
if (message.getServerMsgId() == null) {
|
||||
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);
|
||||
if (body.count > 1) {
|
||||
message.setBodyLanguage(body.language);
|
||||
|
@ -4346,7 +4345,7 @@ public class XmppConnectionService extends Service {
|
|||
}
|
||||
|
||||
private void sendPresence(final Account account, final boolean includeIdleTimestamp) {
|
||||
Presence.Status status;
|
||||
final Presence.Status status;
|
||||
if (manuallyChangePresence()) {
|
||||
status = account.getPresenceStatus();
|
||||
} else {
|
||||
|
@ -4814,7 +4813,7 @@ public class XmppConnectionService extends Service {
|
|||
public interface OnConferenceConfigurationFetched {
|
||||
void onConferenceConfigurationFetched(Conversation conversation);
|
||||
|
||||
void onFetchFailed(Conversation conversation, Element error);
|
||||
void onFetchFailed(Conversation conversation, String errorCondition);
|
||||
}
|
||||
|
||||
public interface OnConferenceJoined {
|
||||
|
|
|
@ -986,7 +986,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
|||
menuCall.setVisible(false);
|
||||
menuOngoingCall.setVisible(false);
|
||||
} 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());
|
||||
if (ongoingRtpSession.isPresent()) {
|
||||
menuOngoingCall.setVisible(true);
|
||||
|
@ -994,8 +994,9 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
|||
} else {
|
||||
menuOngoingCall.setVisible(false);
|
||||
final RtpCapability.Capability rtpCapability = RtpCapability.check(conversation.getContact());
|
||||
final boolean cameraAvailable = activity != null && activity.isCameraFeatureAvailable();
|
||||
menuCall.setVisible(rtpCapability != RtpCapability.Capability.NONE);
|
||||
menuVideoCall.setVisible(rtpCapability == RtpCapability.Capability.VIDEO);
|
||||
menuVideoCall.setVisible(rtpCapability == RtpCapability.Capability.VIDEO && cameraAvailable);
|
||||
}
|
||||
menuContactDetails.setVisible(!this.conversation.withSelf());
|
||||
menuMucDetails.setVisible(false);
|
||||
|
@ -1605,7 +1606,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
|||
}
|
||||
|
||||
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();
|
||||
return;
|
||||
}
|
||||
|
@ -2991,6 +2992,11 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
|||
final Menu menu = popupMenu.getMenu();
|
||||
menu.findItem(R.id.action_manage_accounts).setVisible(QuickConversationsService.isConversations());
|
||||
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()) {
|
||||
case R.id.action_show_qr_code:
|
||||
activity.showQrCode(conversation.getAccount().getShareableUri());
|
||||
|
|
|
@ -38,7 +38,6 @@ import com.google.common.base.CharMatcher;
|
|||
|
||||
import org.openintents.openpgp.util.OpenPgpUtils;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
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.forms.Data;
|
||||
import eu.siacs.conversations.xmpp.pep.Avatar;
|
||||
import okhttp3.HttpUrl;
|
||||
|
||||
public class EditAccountActivity extends OmemoActivity implements OnAccountUpdate, OnUpdateBlocklist,
|
||||
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 openPaymentUrl = mAccount != null && mAccount.getStatus() == Account.State.PAYMENT_REQUIRED;
|
||||
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) {
|
||||
try {
|
||||
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url.toString())));
|
||||
|
@ -531,7 +531,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
|
|||
}
|
||||
} else {
|
||||
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) {
|
||||
this.binding.saveButton.setText(R.string.open_website);
|
||||
} else if (inNeedOfSaslAccept()) {
|
||||
|
@ -542,7 +542,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
|
|||
}
|
||||
} else {
|
||||
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) {
|
||||
this.binding.saveButton.setText(R.string.open_website);
|
||||
} else {
|
||||
|
@ -736,7 +736,8 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onNewIntent(Intent intent) {
|
||||
public void onNewIntent(final Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
if (intent != null && intent.getData() != null) {
|
||||
final XmppUri uri = new XmppUri(intent.getData());
|
||||
if (xmppConnectionServiceBound) {
|
||||
|
@ -1071,9 +1072,6 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
|
|||
} else {
|
||||
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 {
|
||||
this.binding.serverInfoHttpUpload.setText(R.string.server_info_unavailable);
|
||||
}
|
||||
|
|
|
@ -31,8 +31,6 @@ import org.osmdroid.views.MapView;
|
|||
import org.osmdroid.views.overlay.CopyrightOverlay;
|
||||
import org.osmdroid.views.overlay.Overlay;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import eu.siacs.conversations.BuildConfig;
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.R;
|
||||
|
@ -98,11 +96,7 @@ public abstract class LocationActivity extends ActionBarActivity implements Loca
|
|||
config.load(ctx, getPreferences());
|
||||
config.setUserAgentValue(BuildConfig.APPLICATION_ID + "/" + BuildConfig.VERSION_CODE);
|
||||
if (QuickConversationsService.isConversations() && getBooleanPreference("use_tor", R.bool.use_tor)) {
|
||||
try {
|
||||
config.setHttpProxy(HttpConnectionManager.getProxy());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Unable to configure proxy");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -874,7 +874,12 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
|||
}
|
||||
|
||||
private void enableVideo(View view) {
|
||||
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());
|
||||
}
|
||||
|
||||
|
|
|
@ -411,6 +411,10 @@ public class SettingsActivity extends XmppActivity implements
|
|||
|
||||
private void createBackup() {
|
||||
ContextCompat.startForegroundService(this, new Intent(this, ExportBackupService.class));
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setMessage(R.string.backup_started_message);
|
||||
builder.setPositiveButton(R.string.ok, null);
|
||||
builder.create().show();
|
||||
}
|
||||
|
||||
private void displayToast(final String msg) {
|
||||
|
|
|
@ -408,11 +408,7 @@ public abstract class XmppActivity extends ActionBarActivity {
|
|||
metrics = getResources().getDisplayMetrics();
|
||||
ExceptionHelper.init(getApplicationContext());
|
||||
new EmojiService(this).init();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
this.isCameraFeatureAvailable = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
|
||||
} else {
|
||||
this.isCameraFeatureAvailable = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA);
|
||||
}
|
||||
this.mTheme = findTheme();
|
||||
setTheme(this.mTheme);
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ import androidx.core.content.ContextCompat;
|
|||
|
||||
import com.google.common.base.Strings;
|
||||
|
||||
import java.net.URL;
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
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.RtpSessionStatus;
|
||||
import eu.siacs.conversations.entities.Transferable;
|
||||
import eu.siacs.conversations.http.P1S3UrlStreamHandler;
|
||||
import eu.siacs.conversations.persistance.FileBackend;
|
||||
import eu.siacs.conversations.services.MessageArchiveService;
|
||||
import eu.siacs.conversations.services.NotificationService;
|
||||
|
@ -798,21 +797,13 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
|||
displayEmojiMessage(viewHolder, message.getBody().trim(), darkBackground);
|
||||
} else if (message.treatAsDownloadable()) {
|
||||
try {
|
||||
URL url = new URL(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 {
|
||||
final URI uri = new URI(message.getBody());
|
||||
displayDownloadableMessage(viewHolder,
|
||||
message,
|
||||
activity.getString(R.string.check_x_filesize_on_host,
|
||||
UIHelper.getFileDescriptionString(activity, message),
|
||||
url.getHost()),
|
||||
uri.getHost()),
|
||||
darkBackground);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
displayDownloadableMessage(viewHolder,
|
||||
message,
|
||||
|
@ -890,10 +881,6 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
|||
this.highlightedTerm = terms == null ? null : StylingHelper.filterHighlightedWords(terms);
|
||||
}
|
||||
|
||||
public interface OnQuoteListener {
|
||||
void onQuote(String text);
|
||||
}
|
||||
|
||||
public interface OnContactPictureClicked {
|
||||
void onContactPictureClicked(Message message);
|
||||
}
|
||||
|
|
|
@ -94,10 +94,10 @@ public class ShareUtil {
|
|||
url = message.getBody();
|
||||
} else if (message.hasFileOnRemoteHost()) {
|
||||
resId = R.string.file_url;
|
||||
url = message.getFileParams().url.toString();
|
||||
url = message.getFileParams().url;
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
if (activity.copyTextToClipboard(url, resId)) {
|
||||
|
|
|
@ -17,7 +17,7 @@ import eu.siacs.conversations.ui.XmppActivity;
|
|||
|
||||
public class AccountUtils {
|
||||
|
||||
public static final Class MANAGE_ACCOUNT_ACTIVITY;
|
||||
public static final Class<?> MANAGE_ACCOUNT_ACTIVITY;
|
||||
|
||||
static {
|
||||
MANAGE_ACCOUNT_ACTIVITY = getManageAccountActivityClass();
|
||||
|
@ -78,7 +78,7 @@ public class AccountUtils {
|
|||
return pending;
|
||||
}
|
||||
|
||||
public static void launchManageAccounts(Activity activity) {
|
||||
public static void launchManageAccounts(final Activity activity) {
|
||||
if (MANAGE_ACCOUNT_ACTIVITY != null) {
|
||||
activity.startActivity(new Intent(activity, MANAGE_ACCOUNT_ACTIVITY));
|
||||
} else {
|
||||
|
@ -86,15 +86,15 @@ public class AccountUtils {
|
|||
}
|
||||
}
|
||||
|
||||
public static void launchManageAccount(XmppActivity xmppActivity) {
|
||||
Account account = getFirst(xmppActivity.xmppConnectionService);
|
||||
public static void launchManageAccount(final XmppActivity xmppActivity) {
|
||||
final Account account = getFirst(xmppActivity.xmppConnectionService);
|
||||
xmppActivity.switchToAccount(account);
|
||||
}
|
||||
|
||||
private static Class getManageAccountActivityClass() {
|
||||
private static Class<?> getManageAccountActivityClass() {
|
||||
try {
|
||||
return Class.forName("eu.siacs.conversations.ui.ManageAccountActivity");
|
||||
} catch (ClassNotFoundException e) {
|
||||
} catch (final ClassNotFoundException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,8 +9,6 @@ import org.bouncycastle.asn1.x500.style.BCStyle;
|
|||
import org.bouncycastle.asn1.x500.style.IETFUtils;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
@ -31,7 +29,6 @@ import eu.siacs.conversations.Config;
|
|||
import eu.siacs.conversations.R;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.entities.Message;
|
||||
import eu.siacs.conversations.http.AesGcmURLStreamHandler;
|
||||
import eu.siacs.conversations.xmpp.Jid;
|
||||
|
||||
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) {
|
||||
if (url == null) {
|
||||
return false;
|
||||
|
|
|
@ -31,14 +31,14 @@ package eu.siacs.conversations.utils;
|
|||
|
||||
import com.google.common.base.Strings;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import eu.siacs.conversations.entities.Conversational;
|
||||
import eu.siacs.conversations.entities.Message;
|
||||
import eu.siacs.conversations.http.AesGcmURLStreamHandler;
|
||||
import eu.siacs.conversations.http.P1S3UrlStreamHandler;
|
||||
import eu.siacs.conversations.http.AesGcmURL;
|
||||
import eu.siacs.conversations.http.URL;
|
||||
|
||||
public class MessageUtils {
|
||||
|
||||
|
@ -82,28 +82,32 @@ public class MessageUtils {
|
|||
}
|
||||
|
||||
public static boolean treatAsDownloadable(final String body, final boolean oob) {
|
||||
try {
|
||||
final String[] lines = body.split("\n");
|
||||
if (lines.length == 0) {
|
||||
return false;
|
||||
}
|
||||
for (String line : lines) {
|
||||
for (final String line : lines) {
|
||||
if (line.contains("\\s+")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
final URL url = new URL(lines[0]);
|
||||
final String ref = url.getRef();
|
||||
final String protocol = url.getProtocol();
|
||||
final boolean encrypted = ref != null && AesGcmURLStreamHandler.IV_KEY.matcher(ref).matches();
|
||||
final boolean followedByDataUri = lines.length == 2 && lines[1].startsWith("data:");
|
||||
final boolean validAesGcm = AesGcmURLStreamHandler.PROTOCOL_NAME.equalsIgnoreCase(protocol) && encrypted && (lines.length == 1 || followedByDataUri);
|
||||
final boolean validProtocol = "http".equalsIgnoreCase(protocol) || "https".equalsIgnoreCase(protocol) || P1S3UrlStreamHandler.PROTOCOL_NAME.equalsIgnoreCase(protocol);
|
||||
final boolean validOob = validProtocol && (oob || encrypted) && lines.length == 1;
|
||||
return validAesGcm || validOob;
|
||||
} catch (MalformedURLException e) {
|
||||
final URI uri;
|
||||
try {
|
||||
uri = new URI(lines[0]);
|
||||
} catch (final URISyntaxException e) {
|
||||
return false;
|
||||
}
|
||||
if (!URL.WELL_KNOWN_SCHEMES.contains(uri.getScheme())) {
|
||||
return false;
|
||||
}
|
||||
final String ref = uri.getFragment();
|
||||
final String protocol = uri.getScheme();
|
||||
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) {
|
||||
|
|
|
@ -25,7 +25,6 @@ import java.io.File;
|
|||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
@ -580,11 +579,6 @@ public final class MimeUtils {
|
|||
return null;
|
||||
}
|
||||
|
||||
public static String extractRelevantExtension(URL url) {
|
||||
String path = url.getPath();
|
||||
return extractRelevantExtension(path, true);
|
||||
}
|
||||
|
||||
public static String extractRelevantExtension(final String path) {
|
||||
return extractRelevantExtension(path, false);
|
||||
}
|
||||
|
|
|
@ -22,10 +22,12 @@ import java.util.concurrent.TimeUnit;
|
|||
import java.util.List;
|
||||
|
||||
import de.measite.minidns.AbstractDNSClient;
|
||||
import de.measite.minidns.DNSCache;
|
||||
import de.measite.minidns.DNSClient;
|
||||
import de.measite.minidns.DNSName;
|
||||
import de.measite.minidns.Question;
|
||||
import de.measite.minidns.Record;
|
||||
import de.measite.minidns.cache.LRUCache;
|
||||
import de.measite.minidns.dnssec.DNSSECResultNotAuthenticException;
|
||||
import de.measite.minidns.dnsserverlookup.AndroidUsingExec;
|
||||
import de.measite.minidns.hla.DnssecResolverApi;
|
||||
|
@ -75,9 +77,7 @@ public class Resolver {
|
|||
final Field useHardcodedDnsServers = DNSClient.class.getDeclaredField("useHardcodedDnsServers");
|
||||
useHardcodedDnsServers.setAccessible(true);
|
||||
useHardcodedDnsServers.setBoolean(dnsClient, false);
|
||||
} catch (NoSuchFieldException e) {
|
||||
Log.e(Config.LOGTAG, "Unable to disable hardcoded DNS servers", e);
|
||||
} catch (IllegalAccessException e) {
|
||||
} catch (NoSuchFieldException | IllegalAccessException 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));
|
||||
}
|
||||
|
||||
public static boolean useDirectTls(final int port) {
|
||||
return port == 443 || port == 5223;
|
||||
}
|
||||
|
||||
public static boolean invalidHostname(final String hostname) {
|
||||
try {
|
||||
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) {
|
||||
final Result ipResult = fromIpAddress(domain, DEFAULT_PORT_XMPP);
|
||||
if (ipResult != null) {
|
||||
ipResult.connect();
|
||||
return ipResult;
|
||||
|
||||
}
|
||||
final List<Result> results = new ArrayList<>();
|
||||
final List<Result> fallbackResults = new ArrayList<>();
|
||||
Thread[] threads = new Thread[3];
|
||||
final Thread[] threads = new Thread[3];
|
||||
threads[0] = new Thread(() -> {
|
||||
try {
|
||||
final List<Result> list = resolveSrv(domain, true);
|
||||
|
@ -139,7 +150,7 @@ public class Resolver {
|
|||
fallbackResults.addAll(list);
|
||||
}
|
||||
});
|
||||
for (Thread thread : threads) {
|
||||
for (final Thread thread : threads) {
|
||||
thread.start();
|
||||
}
|
||||
try {
|
||||
|
|
|
@ -32,7 +32,7 @@ public class XmppUri {
|
|||
private Map<String, String> parameters = Collections.emptyMap();
|
||||
private boolean safeSource = true;
|
||||
|
||||
public XmppUri(String uri) {
|
||||
public XmppUri(final String uri) {
|
||||
try {
|
||||
parse(Uri.parse(uri));
|
||||
} catch (IllegalArgumentException e) {
|
||||
|
|
|
@ -23,7 +23,6 @@ public final class Namespace {
|
|||
public static final String NICK = "http://jabber.org/protocol/nick";
|
||||
public static final String FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL = "http://jabber.org/protocol/offline";
|
||||
public static final String BIND = "urn:ietf:params:xml:ns:xmpp-bind";
|
||||
public static final String P1_S3_FILE_TRANSFER = "p1:s3filetransfer";
|
||||
public static final String BOOKMARKS_CONVERSION = "urn:xmpp:bookmarks-conversion:0";
|
||||
public static final String BOOKMARKS = "storage:bookmarks";
|
||||
public static final String SYNCHRONIZATION = "im.quicksy.synchronization:0";
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
package eu.siacs.conversations.xmpp;
|
||||
|
||||
public class IqResponseException extends Exception {
|
||||
|
||||
public IqResponseException(final String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -21,9 +21,7 @@ import java.net.ConnectException;
|
|||
import java.net.IDN;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.Socket;
|
||||
import java.net.URL;
|
||||
import java.net.UnknownHostException;
|
||||
import java.security.KeyManagementException;
|
||||
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.ServiceDiscoveryResult;
|
||||
import eu.siacs.conversations.generator.IqGenerator;
|
||||
import eu.siacs.conversations.http.HttpConnectionManager;
|
||||
import eu.siacs.conversations.persistance.FileBackend;
|
||||
import eu.siacs.conversations.services.MemorizingTrustManager;
|
||||
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.XmlReader;
|
||||
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.stanzas.JinglePacket;
|
||||
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.RequestPacket;
|
||||
import eu.siacs.conversations.xmpp.stanzas.streammgmt.ResumePacket;
|
||||
import okhttp3.HttpUrl;
|
||||
|
||||
public class XmppConnection implements Runnable {
|
||||
|
||||
|
@ -172,7 +171,7 @@ public class XmppConnection implements Runnable {
|
|||
private OnBindListener bindListener = null;
|
||||
private OnMessageAcknowledged acknowledgedListener = null;
|
||||
private SaslMechanism saslMechanism;
|
||||
private URL redirectionUrl = null;
|
||||
private HttpUrl redirectionUrl = null;
|
||||
private String verifiedHostname = null;
|
||||
private volatile Thread mThread;
|
||||
private CountDownLatch mStreamCountDownLatch;
|
||||
|
@ -356,9 +355,7 @@ public class XmppConnection implements Runnable {
|
|||
this.changeStatus(Account.State.MISSING_INTERNET_PERMISSION);
|
||||
} catch (final StateChangingException e) {
|
||||
this.changeStatus(e.state);
|
||||
} catch (final UnknownHostException | ConnectException e) {
|
||||
this.changeStatus(Account.State.SERVER_NOT_FOUND);
|
||||
} catch (final SocksSocketFactory.HostNotFoundException e) {
|
||||
} catch (final UnknownHostException | ConnectException | SocksSocketFactory.HostNotFoundException e) {
|
||||
this.changeStatus(Account.State.SERVER_NOT_FOUND);
|
||||
} catch (final SocksSocketFactory.SocksProxyNotFoundException e) {
|
||||
this.changeStatus(Account.State.TOR_NOT_AVAILABLE);
|
||||
|
@ -471,13 +468,14 @@ public class XmppConnection implements Runnable {
|
|||
if (failure.hasChild("account-disabled") && text != null) {
|
||||
Matcher matcher = Patterns.AUTOLINK_WEB_URL.matcher(text);
|
||||
if (matcher.find()) {
|
||||
final HttpUrl url;
|
||||
try {
|
||||
URL url = new URL(text.substring(matcher.start(), matcher.end()));
|
||||
if (url.getProtocol().equals("https")) {
|
||||
url = HttpUrl.get(text.substring(matcher.start(), matcher.end()));
|
||||
if (url.isHttps()) {
|
||||
this.redirectionUrl = url;
|
||||
throw new StateChangingException(Account.State.PAYMENT_REQUIRED);
|
||||
}
|
||||
} catch (MalformedURLException e) {
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new StateChangingException(Account.State.UNAUTHORIZED);
|
||||
}
|
||||
}
|
||||
|
@ -903,7 +901,7 @@ public class XmppConnection implements Runnable {
|
|||
if (response.getType() == IqPacket.TYPE.RESULT) {
|
||||
sendRegistryRequest();
|
||||
} else {
|
||||
final Element error = response.getError();
|
||||
final String error = response.getErrorCondition();
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": failed to pre auth. " + error);
|
||||
throw new StateChangingError(Account.State.REGISTRATION_INVALID_TOKEN);
|
||||
}
|
||||
|
@ -947,11 +945,19 @@ public class XmppConnection implements Runnable {
|
|||
is = null;
|
||||
}
|
||||
} else {
|
||||
final boolean useTor = mXmppConnectionService.useTorToConnect() || account.isOnion();
|
||||
try {
|
||||
Field field = data.getFieldByName("url");
|
||||
URL url = field != null && field.getValue() != null ? new URL(field.getValue()) : null;
|
||||
is = url != null ? url.openStream() : null;
|
||||
} catch (IOException e) {
|
||||
final String url = data.getValue("url");
|
||||
final String fallbackUrl = data.getValue("captcha-fallback-url");
|
||||
if (url != null) {
|
||||
is = HttpConnectionManager.open(url, useTor);
|
||||
} else if (fallbackUrl != null) {
|
||||
is = HttpConnectionManager.open(fallbackUrl, useTor);
|
||||
} else {
|
||||
is = null;
|
||||
}
|
||||
} catch (final IOException e) {
|
||||
Log.d(Config.LOGTAG,account.getJid().asBareJid()+": unable to fetch captcha", e);
|
||||
is = null;
|
||||
}
|
||||
}
|
||||
|
@ -974,7 +980,7 @@ public class XmppConnection implements Runnable {
|
|||
if (url != null) {
|
||||
setAccountCreationFailed(url);
|
||||
} else if (instructions != null) {
|
||||
Matcher matcher = Patterns.AUTOLINK_WEB_URL.matcher(instructions);
|
||||
final Matcher matcher = Patterns.AUTOLINK_WEB_URL.matcher(instructions);
|
||||
if (matcher.find()) {
|
||||
setAccountCreationFailed(instructions.substring(matcher.start(), matcher.end()));
|
||||
}
|
||||
|
@ -984,21 +990,16 @@ public class XmppConnection implements Runnable {
|
|||
}, true);
|
||||
}
|
||||
|
||||
private void setAccountCreationFailed(String url) {
|
||||
if (url != null) {
|
||||
try {
|
||||
this.redirectionUrl = new URL(url);
|
||||
if (this.redirectionUrl.getProtocol().equals("https")) {
|
||||
private void setAccountCreationFailed(final String url) {
|
||||
final HttpUrl httpUrl = url == null ? null : HttpUrl.parse(url);
|
||||
if (httpUrl != null && httpUrl.isHttps()) {
|
||||
this.redirectionUrl = httpUrl;
|
||||
throw new StateChangingError(Account.State.REGISTRATION_WEB);
|
||||
}
|
||||
} catch (MalformedURLException e) {
|
||||
//fall through
|
||||
}
|
||||
}
|
||||
throw new StateChangingError(Account.State.REGISTRATION_FAILED);
|
||||
}
|
||||
|
||||
public URL getRedirectionUrl() {
|
||||
public HttpUrl getRedirectionUrl() {
|
||||
return this.redirectionUrl;
|
||||
}
|
||||
|
||||
|
@ -1894,10 +1895,6 @@ public class XmppConnection implements Runnable {
|
|||
this.blockListRequested = value;
|
||||
}
|
||||
|
||||
public boolean p1S3FileTransfer() {
|
||||
return hasDiscoFeature(account.getDomain(), Namespace.P1_S3_FILE_TRANSFER);
|
||||
}
|
||||
|
||||
public boolean httpUpload(long filesize) {
|
||||
if (Config.DISABLE_HTTP_UPLOAD) {
|
||||
return false;
|
||||
|
|
|
@ -288,6 +288,9 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
|
|||
}
|
||||
final String sdpMid = content.getKey();
|
||||
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);
|
||||
Log.d(Config.LOGTAG, "received candidate: " + iceCandidate);
|
||||
this.webRTCWrapper.addIceCandidate(iceCandidate);
|
||||
|
|
|
@ -174,7 +174,7 @@ public class RtpContentMap {
|
|||
}
|
||||
|
||||
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);
|
||||
return new DescriptionTransport(rtpDescription, transportInfo);
|
||||
}
|
||||
|
|
|
@ -198,10 +198,10 @@ public class SessionDescription {
|
|||
checkNoWhitespace(type, "feedback negotiation type must not contain whitespace");
|
||||
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());
|
||||
}
|
||||
for (RtpDescription.RtpHeaderExtension extension : description.getHeaderExtensions()) {
|
||||
for (final RtpDescription.RtpHeaderExtension extension : description.getHeaderExtensions()) {
|
||||
final String id = extension.getId();
|
||||
final String uri = extension.getUri();
|
||||
if (Strings.isNullOrEmpty(id)) {
|
||||
|
@ -214,7 +214,12 @@ public class SessionDescription {
|
|||
checkNoWhitespace(uri, "feedback negotiation uri must not contain whitespace");
|
||||
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 List<String> groups = sourceGroup.getSsrcs();
|
||||
if (Strings.isNullOrEmpty(semantics)) {
|
||||
|
@ -226,8 +231,8 @@ public class SessionDescription {
|
|||
}
|
||||
mediaAttributes.put("ssrc-group", String.format("%s %s", semantics, Joiner.on(' ').join(groups)));
|
||||
}
|
||||
for (RtpDescription.Source source : description.getSources()) {
|
||||
for (RtpDescription.Source.Parameter parameter : source.getParameters()) {
|
||||
for (final RtpDescription.Source source : description.getSources()) {
|
||||
for (final RtpDescription.Source.Parameter parameter : source.getParameters()) {
|
||||
final String id = source.getSsrcId();
|
||||
final String parameterName = parameter.getParameterName();
|
||||
final String parameterValue = parameter.getParameterValue();
|
||||
|
|
|
@ -208,6 +208,14 @@ public class WebRTCWrapper {
|
|||
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 {
|
||||
try {
|
||||
PeerConnectionFactory.initialize(
|
||||
|
@ -247,7 +255,14 @@ public class WebRTCWrapper {
|
|||
.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();
|
||||
|
||||
|
@ -262,7 +277,7 @@ public class WebRTCWrapper {
|
|||
|
||||
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
|
||||
final AudioSource audioSource = peerConnectionFactory.createAudioSource(new MediaConstraints());
|
||||
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.setAudioRecording(true);
|
||||
this.peerConnection = peerConnection;
|
||||
|
@ -388,7 +393,7 @@ public class WebRTCWrapper {
|
|||
boolean isVideoEnabled() {
|
||||
final VideoTrack videoTrack = this.localVideoTrack;
|
||||
if (videoTrack == null) {
|
||||
throw new IllegalStateException("Local video track does not exist");
|
||||
return false;
|
||||
}
|
||||
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() {
|
||||
return requirePeerConnection().connectionState();
|
||||
}
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
package eu.siacs.conversations.xmpp.jingle.stanzas;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.primitives.Ints;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
import eu.siacs.conversations.xml.Namespace;
|
||||
|
||||
|
|
|
@ -6,11 +6,14 @@ import com.google.common.base.Preconditions;
|
|||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ArrayListMultimap;
|
||||
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.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
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 Map<String, List<Parameter>> parameterMap = new HashMap<>();
|
||||
final ArrayListMultimap<String, Element> feedbackNegotiationMap = 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")) {
|
||||
final String[] parts = rtcpFb.split(" ");
|
||||
if (parts.length >= 2) {
|
||||
|
@ -581,6 +588,9 @@ public class RtpDescription extends GenericDescription {
|
|||
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")) {
|
||||
final String[] parts = ssrcGroup.split(" ");
|
||||
if (parts.length >= 2) {
|
||||
|
|
|
@ -18,28 +18,22 @@ abstract public class AbstractAcknowledgeableStanza extends AbstractStanza {
|
|||
setAttribute("id", id);
|
||||
}
|
||||
|
||||
public Element getError() {
|
||||
Element error = findChild("error");
|
||||
if (error != null) {
|
||||
for(Element element : error.getChildren()) {
|
||||
private Element getErrorConditionElement() {
|
||||
final Element error = findChild("error");
|
||||
if (error == null) {
|
||||
return null;
|
||||
}
|
||||
for (final Element element : error.getChildren()) {
|
||||
if (!element.getName().equals("text")) {
|
||||
return element;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public String getErrorCondition() {
|
||||
Element error = findChild("error");
|
||||
if (error != null) {
|
||||
for(Element element : error.getChildren()) {
|
||||
if (!element.getName().equals("text")) {
|
||||
return element.getName();
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
final Element condition = getErrorConditionElement();
|
||||
return condition == null ? null : condition.getName();
|
||||
}
|
||||
|
||||
public boolean valid() {
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
<string name="action_unblock_contact">Odblokovat kontakt</string>
|
||||
<string name="action_block_domain">Zablokovat 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_settings">Nastavení</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="minute_ago">před minutou</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="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="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="owner">Vlastník</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="block_domain_text">Zablokovat 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="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="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="invite_contact">Pozvat kontakt</string>
|
||||
<string name="invite">Pozvat</string>
|
||||
|
@ -61,21 +77,26 @@
|
|||
<string name="unblock">Odblokovat</string>
|
||||
<string name="save">Uložit</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_never">Již se neptat</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="not_in_roster">Přidat chybějící kontakt do seznamu kontaktů?</string>
|
||||
<string name="add_contact">Přidat kontakt</string>
|
||||
<string name="send_failed">doručení selhalo</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="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="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="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="also_end_conversation">Poté zavřít tuto konverzaci</string>
|
||||
<string name="choose_presence">Vybrat přístroj</string>
|
||||
<string name="send_unencrypted_message">Odeslat nešifrovanou 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_x509_message">Odeslat v\\OMEMO š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="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_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="install">Instalovat</string>
|
||||
<string name="openkeychain_not_installed">Nainstalujte prosím OpenKeychain</string>
|
||||
<string name="offering">nabízí…</string>
|
||||
<string name="waiting">čekám…</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="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_accept_files">Přijímat soubory</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_summary">Blikat při přijetí nové zprávy</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_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_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_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="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="error">Došlo k chybě</string>
|
||||
<string name="recording_error">Chyba</string>
|
||||
|
@ -121,8 +152,10 @@
|
|||
<string name="attach_take_picture">Vyfotit obrázek</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_compressing_image">Nebylo možné převést obrázek</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_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_disabled">Dočasně vypnuto</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_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_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_incompatible_server">Nekompatibilní server</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_otr">OTR</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_publish_avatar">Zveřejnit avatar</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="mgmt_account_enable">Povolit účet</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="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="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="server_info_show_more">Údaje serveru</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="omemo_fingerprint">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="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="decrypt">Dešifrovat</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="save_as_bookmark">Uložit jako 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_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="contact_added_you">Kontakt přidán do seznamu</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="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="touch_to_choose_picture">Ťuknutím na avatar vyberete obrázek z galerie</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_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="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_to">pro %s</string>
|
||||
<string name="send_private_message_to">Zaslat soukromou zprávu pro %s</string>
|
||||
<string name="connect">Připojit</string>
|
||||
<string name="account_already_exists">Tento účet již existuje</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="disable_notifications">Vypnout upozornění</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="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="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_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_expert_options">Expertní nastavení</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_start_time">Odkdy</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="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_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="hosted_on">hostován na %s</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="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="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="send_again">Poslat znovu</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="show_qr_code">Zobrazit 2D kód</string>
|
||||
<string name="show_block_list">Zobrazit seznam blokovaných</string>
|
||||
<string name="account_details">Detaily účtu</string>
|
||||
<string name="confirm">Potvrdit</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_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="receiving_x_file">Přijímám %1$s (%2$d%% dokončeno)</string>
|
||||
<string name="download_x_file">Stáhnout %s</string>
|
||||
|
@ -255,22 +343,37 @@
|
|||
<string name="file">soubor</string>
|
||||
<string name="open_x_file">Otevřít %s</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="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="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="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="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="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="could_not_change_password">Nelze změnit heslo</string>
|
||||
<string name="change_password">Změnit heslo</string>
|
||||
<string name="current_password">Současné 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="disable_all_accounts">Vypnout všechny účty</string>
|
||||
<string name="perform_action_with">Provést akci s</string>
|
||||
|
@ -279,17 +382,35 @@
|
|||
<string name="outcast">Vyloučený</string>
|
||||
<string name="member">Člen</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="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="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="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="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="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="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_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_summary">Změnit klávesu emotikon na klávesu enter</string>
|
||||
<string name="audio">audio</string>
|
||||
|
@ -302,12 +423,15 @@
|
|||
<string name="sending_x_file">Odesílám %s</string>
|
||||
<string name="offering_x_file">Nabízím %s</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="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_summary">Nechat kontaky vědět když jim píšete zprávu</string>
|
||||
<string name="send_location">Poslat 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="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>
|
||||
|
@ -324,6 +448,7 @@
|
|||
<item quantity="many">%d certifikátů smazáno</item>
|
||||
<item quantity="other">%d certifikátů smazáno</item>
|
||||
</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="none">Žádná</string>
|
||||
<string name="recently_used">Naposledy použitá</string>
|
||||
|
@ -331,6 +456,7 @@
|
|||
<string name="search_contacts">Prohledat kontakty</string>
|
||||
<string name="search_bookmarks">Prohledat záložky</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_hint">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_bind_failure">Bind chyba</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_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_summary">Zobrazovat nastavení hostname a port při vytváření účtu</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="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="jid_does_not_match_certificate">Adresa XMPP nesouhlasí s certifikátem</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="verified_omemo_key_with_certificate">OMEMO klíč ověřen certifikátem!</string>
|
||||
|
@ -368,23 +505,45 @@
|
|||
<item quantity="other">%d zpráv</item>
|
||||
</plurals>
|
||||
<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_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_only_when_highlighted">Upozornit pouze, když mě někdo zmíní</string>
|
||||
<string name="notify_never">Upozornění vypnuta</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="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_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="selection_too_large">Vybraný obsah je příliš dlouhý</string>
|
||||
<string name="no_accounts">(Žádné aktivované účty)</string>
|
||||
<string name="this_field_is_required">Toto pole je vyžadováno</string>
|
||||
<string name="correct_message">Opravit 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="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="use_own_provider">Použít vlastního provozovatele</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="presence_chat">Volný pro chat</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_password_too_weak">Registrace selhala: Příliš slabé heslo</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="gp_disable">Vypnout</string>
|
||||
<string name="gp_short">Krátký</string>
|
||||
<string name="gp_medium">Střední</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_theme_options">Vzhled</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_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="type_pc">Počítač</string>
|
||||
<string name="type_phone">Mobil</string>
|
||||
|
@ -413,38 +579,389 @@
|
|||
<string name="type_web">Prohlížeč</string>
|
||||
<string name="type_console">Konzole</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">Já</string>
|
||||
<string name="contact_asks_for_presence_subscription">Kontakt žádá informace o změnách stavu</string>
|
||||
<string name="allow">Povolit</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_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_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="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="error_message">Chybová zpráva</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="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_uri">Sdílet jako XMPP URI</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="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">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 \"%s\". 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">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="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="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="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="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="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 & Obnova</string>
|
||||
<string name="enter_jabber_id">Zadejte XMPP adresu</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_public_channel">Vytvořit veřejný kanál</string>
|
||||
<string name="create_dialog_channel_name">Jméno kanálu</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="search_channels">Prohledat kanály</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="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_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>
|
||||
|
|
|
@ -465,8 +465,8 @@
|
|||
<string name="account_status_host_unknown">Serveren er ikke ansvarlig for dette domæne</string>
|
||||
<string name="server_info_broken">Brudt</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_summary">Vis som Væk når enheden 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 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_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>
|
||||
|
@ -486,7 +486,7 @@
|
|||
<string name="jid_does_not_match_certificate">XMPP-adresse matcher ikke certifikatet</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="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="pref_connection_options">Forbindelse</string>
|
||||
<string name="pref_use_tor">Forbind via TOR</string>
|
||||
|
@ -545,7 +545,7 @@
|
|||
<string name="status_message">Statusbesked</string>
|
||||
<string name="presence_chat">Gratis for Chat</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_dnd">Optaget</string>
|
||||
<string name="secure_password_generated">Der er genereret en sikker adgangskode</string>
|
||||
|
|
|
@ -363,7 +363,7 @@
|
|||
<string name="fetching_history_from_server">Lade Chatverlauf…</string>
|
||||
<string name="no_more_history_on_server">Keine weiteren Nachrichten vorhanden</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="change_password">Passwort ändern</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="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="backup_started_message">Das Backup wurde gestartet. Du bekommst eine Benachrichtigung sobald es fertig ist.</string>
|
||||
</resources>
|
||||
|
|
|
@ -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="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="backup_started_message">Tworzenie kopii zapasowej się rozpoczęło. Dostaniesz powiadomienie kiedy się zakończy. </string>
|
||||
</resources>
|
||||
|
|
|
@ -972,4 +972,5 @@
|
|||
<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="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>
|
||||
|
|
|
@ -124,6 +124,7 @@
|
|||
<string name="pref_ringtone">Zil 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_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_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>
|
||||
|
@ -464,6 +465,8 @@
|
|||
<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="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_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>
|
||||
|
|
|
@ -946,4 +946,5 @@
|
|||
<string name="unable_to_parse_invite">无法解析邀请</string>
|
||||
<string name="server_does_not_support_easy_onboarding_invites">服务器不支持生成邀请</string>
|
||||
<string name="no_active_accounts_support_this">没有活跃帐户支持此功能</string>
|
||||
<string name="backup_started_message">已启动备份。一旦完成,你会收到通知。</string>
|
||||
</resources>
|
||||
|
|
|
@ -148,6 +148,7 @@
|
|||
<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_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_disabled">Temporarily disabled</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="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="backup_started_message">The backup has been started. You’ll get a notification once it has been completed.</string>
|
||||
<string name="unable_to_enable_video">Unable to enable video.</string>
|
||||
</resources>
|
||||
|
|
Loading…
Reference in New Issue