diff --git a/.travis.yml b/.travis.yml index 65506e26c..3d647d9e8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index bf0934a2b..8fa67b9e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 34185b185..42eff67bd 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/build.gradle b/build.gradle index be52a5853..83d0bceee 100644 --- a/build.gradle +++ b/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 } diff --git a/docs/MISSION.md b/docs/MISSION.md deleted file mode 100644 index 5e8671943..000000000 --- a/docs/MISSION.md +++ /dev/null @@ -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. diff --git a/docs/XEPs.md b/docs/XEPs.md deleted file mode 100644 index eab494fc1..000000000 --- a/docs/XEPs.md +++ /dev/null @@ -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 diff --git a/docs/observations.md b/docs/observations.md deleted file mode 100644 index 0c0ef857e..000000000 --- a/docs/observations.md +++ /dev/null @@ -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 diff --git a/metadata/en-US/changelogs/42010.txt b/metadata/en-US/changelogs/42010.txt new file mode 100644 index 000000000..4dfbbe323 --- /dev/null +++ b/metadata/en-US/changelogs/42010.txt @@ -0,0 +1 @@ +• Various bug fixes around Tor support diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index 23bebde5c..4b771abc6 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -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; @@ -97,7 +99,7 @@ public final class Config { //remove *other* omemo devices from *your* device list announcement after not seeing any activity from them for 42 days. They will automatically add themselves after coming back online. public static final long OMEMO_AUTO_EXPIRY = 42 * MILLISECONDS_IN_DAY; - + public static final boolean REMOVE_BROKEN_DEVICES = false; public static final boolean OMEMO_PADDING = false; public static final boolean PUT_AUTH_TAG_INTO_KEY = true; @@ -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 CONTACT_DOMAINS = Collections.singletonList("cheogram.com"); + private static final List CONTACT_DOMAINS = Arrays.asList( + "cheogram.com", + "*.covid.monal.im" + ); + + public static boolean matchesContactDomain(final String domain) { + return XmppDomainVerifier.matchDomain(domain, CONTACT_DOMAINS); + } } private Config() { diff --git a/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java b/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java index 29d286a71..0ad103155 100644 --- a/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java +++ b/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java @@ -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); diff --git a/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java b/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java index 007a28f70..9652ad3eb 100644 --- a/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java +++ b/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java @@ -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(); } diff --git a/src/main/java/eu/siacs/conversations/crypto/XmppDomainVerifier.java b/src/main/java/eu/siacs/conversations/crypto/XmppDomainVerifier.java index b344ac55c..7b741b864 100644 --- a/src/main/java/eu/siacs/conversations/crypto/XmppDomainVerifier.java +++ b/src/main/java/eu/siacs/conversations/crypto/XmppDomainVerifier.java @@ -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 haystack) { - for (String entry : haystack) { + public static boolean matchDomain(final String needle, final List 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; } } diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index 67e8ca5da..6f233d871 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -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(); } diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index ce39aa737..969a9c765 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -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() { diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index a38dc5427..20db15da2 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -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; } diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index ab9c64940..5b8adb9cf 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -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; @@ -32,973 +31,969 @@ import eu.siacs.conversations.utils.MimeUtils; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.Jid; -public class Message extends AbstractEntity implements AvatarService.Avatarable { - - public static final String TABLENAME = "messages"; - - public static final int STATUS_RECEIVED = 0; - public static final int STATUS_UNSEND = 1; - public static final int STATUS_SEND = 2; - public static final int STATUS_SEND_FAILED = 3; - public static final int STATUS_WAITING = 5; - public static final int STATUS_OFFERED = 6; - public static final int STATUS_SEND_RECEIVED = 7; - public static final int STATUS_SEND_DISPLAYED = 8; - - public static final int ENCRYPTION_NONE = 0; - public static final int ENCRYPTION_PGP = 1; - public static final int ENCRYPTION_OTR = 2; - public static final int ENCRYPTION_DECRYPTED = 3; - public static final int ENCRYPTION_DECRYPTION_FAILED = 4; - public static final int ENCRYPTION_AXOLOTL = 5; - public static final int ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE = 6; - public static final int ENCRYPTION_AXOLOTL_FAILED = 7; - - public static final int TYPE_TEXT = 0; - public static final int TYPE_IMAGE = 1; - public static final int TYPE_FILE = 2; - public static final int TYPE_STATUS = 3; - public static final int TYPE_PRIVATE = 4; - public static final int TYPE_PRIVATE_FILE = 5; - public static final int TYPE_RTP_SESSION = 6; - - public static final String CONVERSATION = "conversationUuid"; - public static final String COUNTERPART = "counterpart"; - public static final String TRUE_COUNTERPART = "trueCounterpart"; - public static final String BODY = "body"; - public static final String BODY_LANGUAGE = "bodyLanguage"; - public static final String TIME_SENT = "timeSent"; - public static final String ENCRYPTION = "encryption"; - public static final String STATUS = "status"; - public static final String TYPE = "type"; - public static final String CARBON = "carbon"; - public static final String OOB = "oob"; - public static final String EDITED = "edited"; - public static final String REMOTE_MSG_ID = "remoteMsgId"; - public static final String SERVER_MSG_ID = "serverMsgId"; - public static final String RELATIVE_FILE_PATH = "relativeFilePath"; - public static final String FINGERPRINT = "axolotl_fingerprint"; - public static final String READ = "read"; - public static final String ERROR_MESSAGE = "errorMsg"; - public static final String READ_BY_MARKERS = "readByMarkers"; - public static final String MARKABLE = "markable"; - public static final String DELETED = "deleted"; - public static final String ME_COMMAND = "/me "; - - public static final String ERROR_MESSAGE_CANCELLED = "eu.siacs.conversations.cancelled"; - - - public boolean markable = false; - protected String conversationUuid; - protected Jid counterpart; - protected Jid trueCounterpart; - protected String body; - protected String encryptedBody; - protected long timeSent; - protected int encryption; - protected int status; - protected int type; - protected boolean deleted = false; - protected boolean carbon = false; - protected boolean oob = false; - protected List edits = new ArrayList<>(); - protected String relativeFilePath; - protected boolean read = true; - protected String remoteMsgId = null; - private String bodyLanguage = null; - protected String serverMsgId = null; - private final Conversational conversation; - protected Transferable transferable = null; - private Message mNextMessage = null; - private Message mPreviousMessage = null; - private String axolotlFingerprint = null; - private String errorMessage = null; - private Set readByMarkers = new CopyOnWriteArraySet<>(); - - private Boolean isGeoUri = null; - private Boolean isEmojisOnly = null; - private Boolean treatAsDownloadable = null; - private FileParams fileParams = null; - private List counterparts; - private WeakReference user; - - protected Message(Conversational conversation) { - this.conversation = conversation; - } - - public Message(Conversational conversation, String body, int encryption) { - this(conversation, body, encryption, STATUS_UNSEND); - } - - public Message(Conversational conversation, String body, int encryption, int status) { - this(conversation, java.util.UUID.randomUUID().toString(), - conversation.getUuid(), - conversation.getJid() == null ? null : conversation.getJid().asBareJid(), - null, - body, - System.currentTimeMillis(), - encryption, - status, - TYPE_TEXT, - false, - null, - null, - null, - null, - true, - null, - false, - null, - null, - false, - false, - null); - } - - public Message(Conversation conversation, int status, int type, final String remoteMsgId) { - this(conversation, java.util.UUID.randomUUID().toString(), - conversation.getUuid(), - conversation.getJid() == null ? null : conversation.getJid().asBareJid(), - null, - null, - System.currentTimeMillis(), - Message.ENCRYPTION_NONE, - status, - type, - false, - remoteMsgId, - null, - null, - null, - true, - null, - false, - null, - null, - false, - false, - null); - } - - protected Message(final Conversational conversation, final String uuid, final String conversationUUid, final Jid counterpart, - final Jid trueCounterpart, final String body, final long timeSent, - final int encryption, final int status, final int type, final boolean carbon, - final String remoteMsgId, final String relativeFilePath, - final String serverMsgId, final String fingerprint, final boolean read, - final String edited, final boolean oob, final String errorMessage, final Set readByMarkers, - final boolean markable, final boolean deleted, final String bodyLanguage) { - this.conversation = conversation; - this.uuid = uuid; - this.conversationUuid = conversationUUid; - this.counterpart = counterpart; - this.trueCounterpart = trueCounterpart; - this.body = body == null ? "" : body; - this.timeSent = timeSent; - this.encryption = encryption; - this.status = status; - this.type = type; - this.carbon = carbon; - this.remoteMsgId = remoteMsgId; - this.relativeFilePath = relativeFilePath; - this.serverMsgId = serverMsgId; - this.axolotlFingerprint = fingerprint; - this.read = read; - this.edits = Edit.fromJson(edited); - this.oob = oob; - this.errorMessage = errorMessage; - this.readByMarkers = readByMarkers == null ? new CopyOnWriteArraySet<>() : readByMarkers; - this.markable = markable; - this.deleted = deleted; - this.bodyLanguage = bodyLanguage; - } - - public static Message fromCursor(Cursor cursor, Conversation conversation) { - return new Message(conversation, - cursor.getString(cursor.getColumnIndex(UUID)), - cursor.getString(cursor.getColumnIndex(CONVERSATION)), - fromString(cursor.getString(cursor.getColumnIndex(COUNTERPART))), - fromString(cursor.getString(cursor.getColumnIndex(TRUE_COUNTERPART))), - cursor.getString(cursor.getColumnIndex(BODY)), - cursor.getLong(cursor.getColumnIndex(TIME_SENT)), - cursor.getInt(cursor.getColumnIndex(ENCRYPTION)), - cursor.getInt(cursor.getColumnIndex(STATUS)), - cursor.getInt(cursor.getColumnIndex(TYPE)), - cursor.getInt(cursor.getColumnIndex(CARBON)) > 0, - cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)), - cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)), - cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)), - cursor.getString(cursor.getColumnIndex(FINGERPRINT)), - cursor.getInt(cursor.getColumnIndex(READ)) > 0, - cursor.getString(cursor.getColumnIndex(EDITED)), - cursor.getInt(cursor.getColumnIndex(OOB)) > 0, - cursor.getString(cursor.getColumnIndex(ERROR_MESSAGE)), - ReadByMarker.fromJsonString(cursor.getString(cursor.getColumnIndex(READ_BY_MARKERS))), - cursor.getInt(cursor.getColumnIndex(MARKABLE)) > 0, - cursor.getInt(cursor.getColumnIndex(DELETED)) > 0, - cursor.getString(cursor.getColumnIndex(BODY_LANGUAGE)) - ); - } - - private static Jid fromString(String value) { - try { - if (value != null) { - return Jid.of(value); - } - } catch (IllegalArgumentException e) { - return null; - } - return null; - } - - public static Message createStatusMessage(Conversation conversation, String body) { - final Message message = new Message(conversation); - message.setType(Message.TYPE_STATUS); - message.setStatus(Message.STATUS_RECEIVED); - message.body = body; - return message; - } - - public static Message createLoadMoreMessage(Conversation conversation) { - final Message message = new Message(conversation); - message.setType(Message.TYPE_STATUS); - message.body = "LOAD_MORE"; - return message; - } - - @Override - public ContentValues getContentValues() { - ContentValues values = new ContentValues(); - values.put(UUID, uuid); - values.put(CONVERSATION, conversationUuid); - if (counterpart == null) { - values.putNull(COUNTERPART); - } else { - values.put(COUNTERPART, counterpart.toString()); - } - if (trueCounterpart == null) { - values.putNull(TRUE_COUNTERPART); - } else { - values.put(TRUE_COUNTERPART, trueCounterpart.toString()); - } - values.put(BODY, body.length() > Config.MAX_STORAGE_MESSAGE_CHARS ? body.substring(0, Config.MAX_STORAGE_MESSAGE_CHARS) : body); - values.put(TIME_SENT, timeSent); - values.put(ENCRYPTION, encryption); - values.put(STATUS, status); - values.put(TYPE, type); - values.put(CARBON, carbon ? 1 : 0); - values.put(REMOTE_MSG_ID, remoteMsgId); - values.put(RELATIVE_FILE_PATH, relativeFilePath); - values.put(SERVER_MSG_ID, serverMsgId); - values.put(FINGERPRINT, axolotlFingerprint); - values.put(READ, read ? 1 : 0); - try { - values.put(EDITED, Edit.toJson(edits)); - } catch (JSONException e) { - Log.e(Config.LOGTAG,"error persisting json for edits",e); - } - values.put(OOB, oob ? 1 : 0); - values.put(ERROR_MESSAGE, errorMessage); - values.put(READ_BY_MARKERS, ReadByMarker.toJson(readByMarkers).toString()); - values.put(MARKABLE, markable ? 1 : 0); - values.put(DELETED, deleted ? 1 : 0); - values.put(BODY_LANGUAGE, bodyLanguage); - return values; - } - - public String getConversationUuid() { - return conversationUuid; - } - - public Conversational getConversation() { - return this.conversation; - } - - public Jid getCounterpart() { - return counterpart; - } - - public void setCounterpart(final Jid counterpart) { - this.counterpart = counterpart; - } - - public Contact getContact() { - if (this.conversation.getMode() == Conversation.MODE_SINGLE) { - return this.conversation.getContact(); - } else { - if (this.trueCounterpart == null) { - return null; - } else { - return this.conversation.getAccount().getRoster() - .getContactFromContactList(this.trueCounterpart); - } - } - } - - public String getBody() { - return body; - } - - public synchronized void setBody(String body) { - if (body == null) { - throw new Error("You should not set the message body to null"); - } - this.body = body; - this.isGeoUri = null; - this.isEmojisOnly = null; - this.treatAsDownloadable = null; - this.fileParams = null; - } - - public void setMucUser(MucOptions.User user) { - this.user = new WeakReference<>(user); - } - - public boolean sameMucUser(Message otherMessage) { - final MucOptions.User thisUser = this.user == null ? null : this.user.get(); - final MucOptions.User otherUser = otherMessage.user == null ? null : otherMessage.user.get(); - return thisUser != null && thisUser == otherUser; - } - - public String getErrorMessage() { - return errorMessage; - } - - public boolean setErrorMessage(String message) { - boolean changed = (message != null && !message.equals(errorMessage)) - || (message == null && errorMessage != null); - this.errorMessage = message; - return changed; - } - - public long getTimeSent() { - return timeSent; - } - - public int getEncryption() { - return encryption; - } - - public void setEncryption(int encryption) { - this.encryption = encryption; - } - - public int getStatus() { - return status; - } - - public void setStatus(int status) { - this.status = status; - } - - public String getRelativeFilePath() { - return this.relativeFilePath; - } - - public void setRelativeFilePath(String path) { - this.relativeFilePath = path; - } - - public String getRemoteMsgId() { - return this.remoteMsgId; - } - - public void setRemoteMsgId(String id) { - this.remoteMsgId = id; - } - - public String getServerMsgId() { - return this.serverMsgId; - } - - public void setServerMsgId(String id) { - this.serverMsgId = id; - } - - public boolean isRead() { - return this.read; - } - - public boolean isDeleted() { - return this.deleted; - } - - public void setDeleted(boolean deleted) { - this.deleted = deleted; - } - - public void markRead() { - this.read = true; - } - - public void markUnread() { - this.read = false; - } - - public void setTime(long time) { - this.timeSent = time; - } - - public String getEncryptedBody() { - return this.encryptedBody; - } - - public void setEncryptedBody(String body) { - this.encryptedBody = body; - } - - public int getType() { - return this.type; - } - - public void setType(int type) { - this.type = type; - } - - public boolean isCarbon() { - return carbon; - } - - public void setCarbon(boolean carbon) { - this.carbon = carbon; - } - - public void putEdited(String edited, String serverMsgId) { - final Edit edit = new Edit(edited, serverMsgId); - if (this.edits.size() < 128 && !this.edits.contains(edit)) { - this.edits.add(edit); - } - } - - boolean remoteMsgIdMatchInEdit(String id) { - for(Edit edit : this.edits) { - if (id.equals(edit.getEditedId())) { - return true; - } - } - return false; - } - - public String getBodyLanguage() { - return this.bodyLanguage; - } - - public void setBodyLanguage(String language) { - this.bodyLanguage = language; - } - - public boolean edited() { - return this.edits.size() > 0; - } - - public void setTrueCounterpart(Jid trueCounterpart) { - this.trueCounterpart = trueCounterpart; - } - - public Jid getTrueCounterpart() { - return this.trueCounterpart; - } - - public Transferable getTransferable() { - return this.transferable; - } - - public synchronized void setTransferable(Transferable transferable) { - this.fileParams = null; - this.transferable = transferable; - } - - public boolean addReadByMarker(ReadByMarker readByMarker) { - if (readByMarker.getRealJid() != null) { - if (readByMarker.getRealJid().asBareJid().equals(trueCounterpart)) { - return false; - } - } else if (readByMarker.getFullJid() != null) { - if (readByMarker.getFullJid().equals(counterpart)) { - return false; - } - } - if (this.readByMarkers.add(readByMarker)) { - if (readByMarker.getRealJid() != null && readByMarker.getFullJid() != null) { - Iterator iterator = this.readByMarkers.iterator(); - while (iterator.hasNext()) { - ReadByMarker marker = iterator.next(); - if (marker.getRealJid() == null && readByMarker.getFullJid().equals(marker.getFullJid())) { - iterator.remove(); - } - } - } - return true; - } else { - return false; - } - } - - public Set getReadByMarkers() { - return ImmutableSet.copyOf(this.readByMarkers); - } - - boolean similar(Message message) { - if (!isPrivateMessage() && this.serverMsgId != null && message.getServerMsgId() != null) { - return this.serverMsgId.equals(message.getServerMsgId()) || Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId()); - } else if (Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId())) { - return true; - } else if (this.body == null || this.counterpart == null) { - return false; - } else { - String body, otherBody; - if (this.hasFileOnRemoteHost()) { - body = getFileParams().url.toString(); - otherBody = message.body == null ? null : message.body.trim(); - } else { - body = this.body; - otherBody = message.body; - } - final boolean matchingCounterpart = this.counterpart.equals(message.getCounterpart()); - if (message.getRemoteMsgId() != null) { - final boolean hasUuid = CryptoHelper.UUID_PATTERN.matcher(message.getRemoteMsgId()).matches(); - if (hasUuid && matchingCounterpart && Edit.wasPreviouslyEditedRemoteMsgId(edits, message.getRemoteMsgId())) { - return true; - } - return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid)) - && matchingCounterpart - && (body.equals(otherBody) || (message.getEncryption() == Message.ENCRYPTION_PGP && hasUuid)); - } else { - return this.remoteMsgId == null - && matchingCounterpart - && body.equals(otherBody) - && Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.MESSAGE_MERGE_WINDOW * 1000; - } - } - } - - public Message next() { - if (this.conversation instanceof Conversation) { - final Conversation conversation = (Conversation) this.conversation; - synchronized (conversation.messages) { - if (this.mNextMessage == null) { - int index = conversation.messages.indexOf(this); - if (index < 0 || index >= conversation.messages.size() - 1) { - this.mNextMessage = null; - } else { - this.mNextMessage = conversation.messages.get(index + 1); - } - } - return this.mNextMessage; - } - } else { - throw new AssertionError("Calling next should be disabled for stubs"); - } - } - - public Message prev() { - if (this.conversation instanceof Conversation) { - final Conversation conversation = (Conversation) this.conversation; - synchronized (conversation.messages) { - if (this.mPreviousMessage == null) { - int index = conversation.messages.indexOf(this); - if (index <= 0 || index > conversation.messages.size()) { - this.mPreviousMessage = null; - } else { - this.mPreviousMessage = conversation.messages.get(index - 1); - } - } - } - return this.mPreviousMessage; - } else { - throw new AssertionError("Calling prev should be disabled for stubs"); - } - } - - public boolean isLastCorrectableMessage() { - Message next = next(); - while (next != null) { - if (next.isEditable()) { - return false; - } - next = next.next(); - } - return isEditable(); - } - - public boolean isEditable() { - return status != STATUS_RECEIVED && !isCarbon() && type != Message.TYPE_RTP_SESSION; - } - - public boolean mergeable(final Message message) { - return message != null && - (message.getType() == Message.TYPE_TEXT && - this.getTransferable() == null && - message.getTransferable() == null && - message.getEncryption() != Message.ENCRYPTION_PGP && - message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED && - this.getType() == message.getType() && - //this.getStatus() == message.getStatus() && - isStatusMergeable(this.getStatus(), message.getStatus()) && - this.getEncryption() == message.getEncryption() && - this.getCounterpart() != null && - this.getCounterpart().equals(message.getCounterpart()) && - this.edited() == message.edited() && - (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) && - this.getBody().length() + message.getBody().length() <= Config.MAX_DISPLAY_MESSAGE_CHARS && - !message.isGeoUri() && - !this.isGeoUri() && - !message.isOOb() && - !this.isOOb() && - !message.treatAsDownloadable() && - !this.treatAsDownloadable() && - !message.hasMeCommand() && - !this.hasMeCommand() && - !this.bodyIsOnlyEmojis() && - !message.bodyIsOnlyEmojis() && - ((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint())) && - UIHelper.sameDay(message.getTimeSent(), this.getTimeSent()) && - this.getReadByMarkers().equals(message.getReadByMarkers()) && - !this.conversation.getJid().asBareJid().equals(Config.BUG_REPORTS) - ); - } - - private static boolean isStatusMergeable(int a, int b) { - return a == b || ( - (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND) - || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND) - || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_WAITING) - || (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND) - || (a == Message.STATUS_SEND && b == Message.STATUS_WAITING) - ); - } - - public void setCounterparts(List counterparts) { - this.counterparts = counterparts; - } - - public List getCounterparts() { - return this.counterparts; - } - - @Override - public int getAvatarBackgroundColor() { - if (type == Message.TYPE_STATUS && getCounterparts() != null && getCounterparts().size() > 1) { - return Color.TRANSPARENT; - } else { - return UIHelper.getColorForName(UIHelper.getMessageDisplayName(this)); - } - } - - @Override - public String getAvatarName() { - return UIHelper.getMessageDisplayName(this); - } - - public boolean isOOb() { - return oob; - } - - public static class MergeSeparator { - } - - public SpannableStringBuilder getMergedBody() { - SpannableStringBuilder body = new SpannableStringBuilder(MessageUtils.filterLtrRtl(this.body).trim()); - Message current = this; - while (current.mergeable(current.next())) { - current = current.next(); - if (current == null) { - break; - } - body.append("\n\n"); - body.setSpan(new MergeSeparator(), body.length() - 2, body.length(), - SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE); - body.append(MessageUtils.filterLtrRtl(current.getBody()).trim()); - } - return body; - } - - public boolean hasMeCommand() { - return this.body.trim().startsWith(ME_COMMAND); - } - - public int getMergedStatus() { - int status = this.status; - Message current = this; - while (current.mergeable(current.next())) { - current = current.next(); - if (current == null) { - break; - } - status = current.status; - } - return status; - } - - public long getMergedTimeSent() { - long time = this.timeSent; - Message current = this; - while (current.mergeable(current.next())) { - current = current.next(); - if (current == null) { - break; - } - time = current.timeSent; - } - return time; - } - - public boolean wasMergedIntoPrevious() { - Message prev = this.prev(); - return prev != null && prev.mergeable(this); - } - - public boolean trusted() { - Contact contact = this.getContact(); - return status > STATUS_RECEIVED || (contact != null && (contact.showInContactList() || contact.isSelf())); - } - - public boolean fixCounterpart() { - final Presences presences = conversation.getContact().getPresences(); - if (counterpart != null && presences.has(Strings.nullToEmpty(counterpart.getResource()))) { - return true; - } else if (presences.size() >= 1) { - counterpart = PresenceSelector.getNextCounterpart(getContact(),presences.toResourceArray()[0]); - return true; - } else { - counterpart = null; - return false; - } - } - - public void setUuid(String uuid) { - this.uuid = uuid; - } - - public String getEditedId() { - if (edits.size() > 0) { - return edits.get(edits.size() - 1).getEditedId(); - } else { - throw new IllegalStateException("Attempting to store unedited message"); - } - } - - public String getEditedIdWireFormat() { - if (edits.size() > 0) { - return edits.get(Config.USE_LMC_VERSION_1_1 ? 0 : edits.size() - 1).getEditedId(); - } else { - throw new IllegalStateException("Attempting to store unedited message"); - } - } - - public void setOob(boolean isOob) { - this.oob = isOob; - } - - public String getMimeType() { - String extension; - 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) { - return null; - } - } - return MimeUtils.guessMimeTypeFromExtension(extension); - } - - public synchronized boolean treatAsDownloadable() { - if (treatAsDownloadable == null) { - treatAsDownloadable = MessageUtils.treatAsDownloadable(this.body, this.oob); - } - return treatAsDownloadable; - } - - public synchronized boolean bodyIsOnlyEmojis() { - if (isEmojisOnly == null) { - isEmojisOnly = Emoticons.isOnlyEmoji(body.replaceAll("\\s", "")); - } - return isEmojisOnly; - } - - public synchronized boolean isGeoUri() { - if (isGeoUri == null) { - isGeoUri = GeoHelper.GEO_URI.matcher(body).matches(); - } - return isGeoUri; - } - - public synchronized void resetFileParams() { - this.fileParams = null; - } - - public synchronized FileParams getFileParams() { - if (fileParams == null) { - fileParams = new FileParams(); - if (this.transferable != null) { - fileParams.size = this.transferable.getFileSize(); - } - final String[] parts = body == null ? new String[0] : body.split("\\|"); - switch (parts.length) { - case 1: - try { - fileParams.size = Long.parseLong(parts[0]); - } catch (NumberFormatException e) { - fileParams.url = parseUrl(parts[0]); - } - break; - case 5: - fileParams.runtime = parseInt(parts[4]); - case 4: - fileParams.width = parseInt(parts[2]); - fileParams.height = parseInt(parts[3]); - case 2: - fileParams.url = parseUrl(parts[0]); - fileParams.size = parseLong(parts[1]); - break; - case 3: - fileParams.size = parseLong(parts[0]); - fileParams.width = parseInt(parts[1]); - fileParams.height = parseInt(parts[2]); - break; - } - } - return fileParams; - } - - private static long parseLong(String value) { - try { - return Long.parseLong(value); - } catch (NumberFormatException e) { - return 0; - } - } - - private static int parseInt(String value) { - try { - return Integer.parseInt(value); - } catch (NumberFormatException e) { - return 0; - } - } - - 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; - } - - public boolean isPrivateMessage() { - return type == TYPE_PRIVATE || type == TYPE_PRIVATE_FILE; - } - - public boolean isFileOrImage() { - return type == TYPE_FILE || type == TYPE_IMAGE || type == TYPE_PRIVATE_FILE; - } - - public boolean hasFileOnRemoteHost() { - return isFileOrImage() && getFileParams().url != null; - } - - public boolean needsUploading() { - return isFileOrImage() && getFileParams().url == null; - } - - public class FileParams { - public URL url; - public long size = 0; - public int width = 0; - public int height = 0; - public int runtime = 0; - } - - public void setFingerprint(String fingerprint) { - this.axolotlFingerprint = fingerprint; - } - - public String getFingerprint() { - return axolotlFingerprint; - } - - public boolean isTrusted() { - FingerprintStatus s = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint); - return s != null && s.isTrusted(); - } - - private int getPreviousEncryption() { - for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()) { - if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) { - continue; - } - return iterator.getEncryption(); - } - return ENCRYPTION_NONE; - } - - private int getNextEncryption() { - if (this.conversation instanceof Conversation) { - Conversation conversation = (Conversation) this.conversation; - for (Message iterator = this.next(); iterator != null; iterator = iterator.next()) { - if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) { - continue; - } - return iterator.getEncryption(); - } - return conversation.getNextEncryption(); - } else { - throw new AssertionError("This should never be called since isInValidSession should be disabled for stubs"); - } - } - - public boolean isValidInSession() { - int pastEncryption = getCleanedEncryption(this.getPreviousEncryption()); - int futureEncryption = getCleanedEncryption(this.getNextEncryption()); - - boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE - || futureEncryption == ENCRYPTION_NONE - || pastEncryption != futureEncryption; - - return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption; - } - - private static int getCleanedEncryption(int encryption) { - if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) { - return ENCRYPTION_PGP; - } - if (encryption == ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE || encryption == ENCRYPTION_AXOLOTL_FAILED) { - return ENCRYPTION_AXOLOTL; - } - return encryption; - } - - public static boolean configurePrivateMessage(final Message message) { - return configurePrivateMessage(message, false); - } - - public static boolean configurePrivateFileMessage(final Message message) { - return configurePrivateMessage(message, true); - } - - private static boolean configurePrivateMessage(final Message message, final boolean isFile) { - final Conversation conversation; - if (message.conversation instanceof Conversation) { - conversation = (Conversation) message.conversation; - } else { - return false; - } - if (conversation.getMode() == Conversation.MODE_MULTI) { - final Jid nextCounterpart = conversation.getNextCounterpart(); - if (nextCounterpart != null) { - message.setCounterpart(nextCounterpart); - message.setTrueCounterpart(conversation.getMucOptions().getTrueCounterpart(nextCounterpart)); - message.setType(isFile ? Message.TYPE_PRIVATE_FILE : Message.TYPE_PRIVATE); - return true; - } - } - return false; - } +public class Message extends AbstractEntity implements AvatarService.Avatarable { + + public static final String TABLENAME = "messages"; + + public static final int STATUS_RECEIVED = 0; + public static final int STATUS_UNSEND = 1; + public static final int STATUS_SEND = 2; + public static final int STATUS_SEND_FAILED = 3; + public static final int STATUS_WAITING = 5; + public static final int STATUS_OFFERED = 6; + public static final int STATUS_SEND_RECEIVED = 7; + public static final int STATUS_SEND_DISPLAYED = 8; + + public static final int ENCRYPTION_NONE = 0; + public static final int ENCRYPTION_PGP = 1; + public static final int ENCRYPTION_OTR = 2; + public static final int ENCRYPTION_DECRYPTED = 3; + public static final int ENCRYPTION_DECRYPTION_FAILED = 4; + public static final int ENCRYPTION_AXOLOTL = 5; + public static final int ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE = 6; + public static final int ENCRYPTION_AXOLOTL_FAILED = 7; + + public static final int TYPE_TEXT = 0; + public static final int TYPE_IMAGE = 1; + public static final int TYPE_FILE = 2; + public static final int TYPE_STATUS = 3; + public static final int TYPE_PRIVATE = 4; + public static final int TYPE_PRIVATE_FILE = 5; + public static final int TYPE_RTP_SESSION = 6; + + public static final String CONVERSATION = "conversationUuid"; + public static final String COUNTERPART = "counterpart"; + public static final String TRUE_COUNTERPART = "trueCounterpart"; + public static final String BODY = "body"; + public static final String BODY_LANGUAGE = "bodyLanguage"; + public static final String TIME_SENT = "timeSent"; + public static final String ENCRYPTION = "encryption"; + public static final String STATUS = "status"; + public static final String TYPE = "type"; + public static final String CARBON = "carbon"; + public static final String OOB = "oob"; + public static final String EDITED = "edited"; + public static final String REMOTE_MSG_ID = "remoteMsgId"; + public static final String SERVER_MSG_ID = "serverMsgId"; + public static final String RELATIVE_FILE_PATH = "relativeFilePath"; + public static final String FINGERPRINT = "axolotl_fingerprint"; + public static final String READ = "read"; + public static final String ERROR_MESSAGE = "errorMsg"; + public static final String READ_BY_MARKERS = "readByMarkers"; + public static final String MARKABLE = "markable"; + public static final String DELETED = "deleted"; + public static final String ME_COMMAND = "/me "; + + public static final String ERROR_MESSAGE_CANCELLED = "eu.siacs.conversations.cancelled"; + + + public boolean markable = false; + protected String conversationUuid; + protected Jid counterpart; + protected Jid trueCounterpart; + protected String body; + protected String encryptedBody; + protected long timeSent; + protected int encryption; + protected int status; + protected int type; + protected boolean deleted = false; + protected boolean carbon = false; + protected boolean oob = false; + protected List edits = new ArrayList<>(); + protected String relativeFilePath; + protected boolean read = true; + protected String remoteMsgId = null; + private String bodyLanguage = null; + protected String serverMsgId = null; + private final Conversational conversation; + protected Transferable transferable = null; + private Message mNextMessage = null; + private Message mPreviousMessage = null; + private String axolotlFingerprint = null; + private String errorMessage = null; + private Set readByMarkers = new CopyOnWriteArraySet<>(); + + private Boolean isGeoUri = null; + private Boolean isEmojisOnly = null; + private Boolean treatAsDownloadable = null; + private FileParams fileParams = null; + private List counterparts; + private WeakReference user; + + protected Message(Conversational conversation) { + this.conversation = conversation; + } + + public Message(Conversational conversation, String body, int encryption) { + this(conversation, body, encryption, STATUS_UNSEND); + } + + public Message(Conversational conversation, String body, int encryption, int status) { + this(conversation, java.util.UUID.randomUUID().toString(), + conversation.getUuid(), + conversation.getJid() == null ? null : conversation.getJid().asBareJid(), + null, + body, + System.currentTimeMillis(), + encryption, + status, + TYPE_TEXT, + false, + null, + null, + null, + null, + true, + null, + false, + null, + null, + false, + false, + null); + } + + public Message(Conversation conversation, int status, int type, final String remoteMsgId) { + this(conversation, java.util.UUID.randomUUID().toString(), + conversation.getUuid(), + conversation.getJid() == null ? null : conversation.getJid().asBareJid(), + null, + null, + System.currentTimeMillis(), + Message.ENCRYPTION_NONE, + status, + type, + false, + remoteMsgId, + null, + null, + null, + true, + null, + false, + null, + null, + false, + false, + null); + } + + protected Message(final Conversational conversation, final String uuid, final String conversationUUid, final Jid counterpart, + final Jid trueCounterpart, final String body, final long timeSent, + final int encryption, final int status, final int type, final boolean carbon, + final String remoteMsgId, final String relativeFilePath, + final String serverMsgId, final String fingerprint, final boolean read, + final String edited, final boolean oob, final String errorMessage, final Set readByMarkers, + final boolean markable, final boolean deleted, final String bodyLanguage) { + this.conversation = conversation; + this.uuid = uuid; + this.conversationUuid = conversationUUid; + this.counterpart = counterpart; + this.trueCounterpart = trueCounterpart; + this.body = body == null ? "" : body; + this.timeSent = timeSent; + this.encryption = encryption; + this.status = status; + this.type = type; + this.carbon = carbon; + this.remoteMsgId = remoteMsgId; + this.relativeFilePath = relativeFilePath; + this.serverMsgId = serverMsgId; + this.axolotlFingerprint = fingerprint; + this.read = read; + this.edits = Edit.fromJson(edited); + this.oob = oob; + this.errorMessage = errorMessage; + this.readByMarkers = readByMarkers == null ? new CopyOnWriteArraySet<>() : readByMarkers; + this.markable = markable; + this.deleted = deleted; + this.bodyLanguage = bodyLanguage; + } + + public static Message fromCursor(Cursor cursor, Conversation conversation) { + return new Message(conversation, + cursor.getString(cursor.getColumnIndex(UUID)), + cursor.getString(cursor.getColumnIndex(CONVERSATION)), + fromString(cursor.getString(cursor.getColumnIndex(COUNTERPART))), + fromString(cursor.getString(cursor.getColumnIndex(TRUE_COUNTERPART))), + cursor.getString(cursor.getColumnIndex(BODY)), + cursor.getLong(cursor.getColumnIndex(TIME_SENT)), + cursor.getInt(cursor.getColumnIndex(ENCRYPTION)), + cursor.getInt(cursor.getColumnIndex(STATUS)), + cursor.getInt(cursor.getColumnIndex(TYPE)), + cursor.getInt(cursor.getColumnIndex(CARBON)) > 0, + cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)), + cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)), + cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)), + cursor.getString(cursor.getColumnIndex(FINGERPRINT)), + cursor.getInt(cursor.getColumnIndex(READ)) > 0, + cursor.getString(cursor.getColumnIndex(EDITED)), + cursor.getInt(cursor.getColumnIndex(OOB)) > 0, + cursor.getString(cursor.getColumnIndex(ERROR_MESSAGE)), + ReadByMarker.fromJsonString(cursor.getString(cursor.getColumnIndex(READ_BY_MARKERS))), + cursor.getInt(cursor.getColumnIndex(MARKABLE)) > 0, + cursor.getInt(cursor.getColumnIndex(DELETED)) > 0, + cursor.getString(cursor.getColumnIndex(BODY_LANGUAGE)) + ); + } + + private static Jid fromString(String value) { + try { + if (value != null) { + return Jid.of(value); + } + } catch (IllegalArgumentException e) { + return null; + } + return null; + } + + public static Message createStatusMessage(Conversation conversation, String body) { + final Message message = new Message(conversation); + message.setType(Message.TYPE_STATUS); + message.setStatus(Message.STATUS_RECEIVED); + message.body = body; + return message; + } + + public static Message createLoadMoreMessage(Conversation conversation) { + final Message message = new Message(conversation); + message.setType(Message.TYPE_STATUS); + message.body = "LOAD_MORE"; + return message; + } + + @Override + public ContentValues getContentValues() { + ContentValues values = new ContentValues(); + values.put(UUID, uuid); + values.put(CONVERSATION, conversationUuid); + if (counterpart == null) { + values.putNull(COUNTERPART); + } else { + values.put(COUNTERPART, counterpart.toString()); + } + if (trueCounterpart == null) { + values.putNull(TRUE_COUNTERPART); + } else { + values.put(TRUE_COUNTERPART, trueCounterpart.toString()); + } + values.put(BODY, body.length() > Config.MAX_STORAGE_MESSAGE_CHARS ? body.substring(0, Config.MAX_STORAGE_MESSAGE_CHARS) : body); + values.put(TIME_SENT, timeSent); + values.put(ENCRYPTION, encryption); + values.put(STATUS, status); + values.put(TYPE, type); + values.put(CARBON, carbon ? 1 : 0); + values.put(REMOTE_MSG_ID, remoteMsgId); + values.put(RELATIVE_FILE_PATH, relativeFilePath); + values.put(SERVER_MSG_ID, serverMsgId); + values.put(FINGERPRINT, axolotlFingerprint); + values.put(READ, read ? 1 : 0); + try { + values.put(EDITED, Edit.toJson(edits)); + } catch (JSONException e) { + Log.e(Config.LOGTAG, "error persisting json for edits", e); + } + values.put(OOB, oob ? 1 : 0); + values.put(ERROR_MESSAGE, errorMessage); + values.put(READ_BY_MARKERS, ReadByMarker.toJson(readByMarkers).toString()); + values.put(MARKABLE, markable ? 1 : 0); + values.put(DELETED, deleted ? 1 : 0); + values.put(BODY_LANGUAGE, bodyLanguage); + return values; + } + + public String getConversationUuid() { + return conversationUuid; + } + + public Conversational getConversation() { + return this.conversation; + } + + public Jid getCounterpart() { + return counterpart; + } + + public void setCounterpart(final Jid counterpart) { + this.counterpart = counterpart; + } + + public Contact getContact() { + if (this.conversation.getMode() == Conversation.MODE_SINGLE) { + return this.conversation.getContact(); + } else { + if (this.trueCounterpart == null) { + return null; + } else { + return this.conversation.getAccount().getRoster() + .getContactFromContactList(this.trueCounterpart); + } + } + } + + public String getBody() { + return body; + } + + public synchronized void setBody(String body) { + if (body == null) { + throw new Error("You should not set the message body to null"); + } + this.body = body; + this.isGeoUri = null; + this.isEmojisOnly = null; + this.treatAsDownloadable = null; + this.fileParams = null; + } + + public void setMucUser(MucOptions.User user) { + this.user = new WeakReference<>(user); + } + + public boolean sameMucUser(Message otherMessage) { + final MucOptions.User thisUser = this.user == null ? null : this.user.get(); + final MucOptions.User otherUser = otherMessage.user == null ? null : otherMessage.user.get(); + return thisUser != null && thisUser == otherUser; + } + + public String getErrorMessage() { + return errorMessage; + } + + public boolean setErrorMessage(String message) { + boolean changed = (message != null && !message.equals(errorMessage)) + || (message == null && errorMessage != null); + this.errorMessage = message; + return changed; + } + + public long getTimeSent() { + return timeSent; + } + + public int getEncryption() { + return encryption; + } + + public void setEncryption(int encryption) { + this.encryption = encryption; + } + + public int getStatus() { + return status; + } + + public void setStatus(int status) { + this.status = status; + } + + public String getRelativeFilePath() { + return this.relativeFilePath; + } + + public void setRelativeFilePath(String path) { + this.relativeFilePath = path; + } + + public String getRemoteMsgId() { + return this.remoteMsgId; + } + + public void setRemoteMsgId(String id) { + this.remoteMsgId = id; + } + + public String getServerMsgId() { + return this.serverMsgId; + } + + public void setServerMsgId(String id) { + this.serverMsgId = id; + } + + public boolean isRead() { + return this.read; + } + + public boolean isDeleted() { + return this.deleted; + } + + public void setDeleted(boolean deleted) { + this.deleted = deleted; + } + + public void markRead() { + this.read = true; + } + + public void markUnread() { + this.read = false; + } + + public void setTime(long time) { + this.timeSent = time; + } + + public String getEncryptedBody() { + return this.encryptedBody; + } + + public void setEncryptedBody(String body) { + this.encryptedBody = body; + } + + public int getType() { + return this.type; + } + + public void setType(int type) { + this.type = type; + } + + public boolean isCarbon() { + return carbon; + } + + public void setCarbon(boolean carbon) { + this.carbon = carbon; + } + + public void putEdited(String edited, String serverMsgId) { + final Edit edit = new Edit(edited, serverMsgId); + if (this.edits.size() < 128 && !this.edits.contains(edit)) { + this.edits.add(edit); + } + } + + boolean remoteMsgIdMatchInEdit(String id) { + for (Edit edit : this.edits) { + if (id.equals(edit.getEditedId())) { + return true; + } + } + return false; + } + + public String getBodyLanguage() { + return this.bodyLanguage; + } + + public void setBodyLanguage(String language) { + this.bodyLanguage = language; + } + + public boolean edited() { + return this.edits.size() > 0; + } + + public void setTrueCounterpart(Jid trueCounterpart) { + this.trueCounterpart = trueCounterpart; + } + + public Jid getTrueCounterpart() { + return this.trueCounterpart; + } + + public Transferable getTransferable() { + return this.transferable; + } + + public synchronized void setTransferable(Transferable transferable) { + this.fileParams = null; + this.transferable = transferable; + } + + public boolean addReadByMarker(ReadByMarker readByMarker) { + if (readByMarker.getRealJid() != null) { + if (readByMarker.getRealJid().asBareJid().equals(trueCounterpart)) { + return false; + } + } else if (readByMarker.getFullJid() != null) { + if (readByMarker.getFullJid().equals(counterpart)) { + return false; + } + } + if (this.readByMarkers.add(readByMarker)) { + if (readByMarker.getRealJid() != null && readByMarker.getFullJid() != null) { + Iterator iterator = this.readByMarkers.iterator(); + while (iterator.hasNext()) { + ReadByMarker marker = iterator.next(); + if (marker.getRealJid() == null && readByMarker.getFullJid().equals(marker.getFullJid())) { + iterator.remove(); + } + } + } + return true; + } else { + return false; + } + } + + public Set getReadByMarkers() { + return ImmutableSet.copyOf(this.readByMarkers); + } + + boolean similar(Message message) { + if (!isPrivateMessage() && this.serverMsgId != null && message.getServerMsgId() != null) { + return this.serverMsgId.equals(message.getServerMsgId()) || Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId()); + } else if (Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId())) { + return true; + } else if (this.body == null || this.counterpart == null) { + return false; + } else { + String body, otherBody; + if (this.hasFileOnRemoteHost()) { + body = getFileParams().url; + otherBody = message.body == null ? null : message.body.trim(); + } else { + body = this.body; + otherBody = message.body; + } + final boolean matchingCounterpart = this.counterpart.equals(message.getCounterpart()); + if (message.getRemoteMsgId() != null) { + final boolean hasUuid = CryptoHelper.UUID_PATTERN.matcher(message.getRemoteMsgId()).matches(); + if (hasUuid && matchingCounterpart && Edit.wasPreviouslyEditedRemoteMsgId(edits, message.getRemoteMsgId())) { + return true; + } + return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid)) + && matchingCounterpart + && (body.equals(otherBody) || (message.getEncryption() == Message.ENCRYPTION_PGP && hasUuid)); + } else { + return this.remoteMsgId == null + && matchingCounterpart + && body.equals(otherBody) + && Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.MESSAGE_MERGE_WINDOW * 1000; + } + } + } + + public Message next() { + if (this.conversation instanceof Conversation) { + final Conversation conversation = (Conversation) this.conversation; + synchronized (conversation.messages) { + if (this.mNextMessage == null) { + int index = conversation.messages.indexOf(this); + if (index < 0 || index >= conversation.messages.size() - 1) { + this.mNextMessage = null; + } else { + this.mNextMessage = conversation.messages.get(index + 1); + } + } + return this.mNextMessage; + } + } else { + throw new AssertionError("Calling next should be disabled for stubs"); + } + } + + public Message prev() { + if (this.conversation instanceof Conversation) { + final Conversation conversation = (Conversation) this.conversation; + synchronized (conversation.messages) { + if (this.mPreviousMessage == null) { + int index = conversation.messages.indexOf(this); + if (index <= 0 || index > conversation.messages.size()) { + this.mPreviousMessage = null; + } else { + this.mPreviousMessage = conversation.messages.get(index - 1); + } + } + } + return this.mPreviousMessage; + } else { + throw new AssertionError("Calling prev should be disabled for stubs"); + } + } + + public boolean isLastCorrectableMessage() { + Message next = next(); + while (next != null) { + if (next.isEditable()) { + return false; + } + next = next.next(); + } + return isEditable(); + } + + public boolean isEditable() { + return status != STATUS_RECEIVED && !isCarbon() && type != Message.TYPE_RTP_SESSION; + } + + public boolean mergeable(final Message message) { + return message != null && + (message.getType() == Message.TYPE_TEXT && + this.getTransferable() == null && + message.getTransferable() == null && + message.getEncryption() != Message.ENCRYPTION_PGP && + message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED && + this.getType() == message.getType() && + //this.getStatus() == message.getStatus() && + isStatusMergeable(this.getStatus(), message.getStatus()) && + this.getEncryption() == message.getEncryption() && + this.getCounterpart() != null && + this.getCounterpart().equals(message.getCounterpart()) && + this.edited() == message.edited() && + (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) && + this.getBody().length() + message.getBody().length() <= Config.MAX_DISPLAY_MESSAGE_CHARS && + !message.isGeoUri() && + !this.isGeoUri() && + !message.isOOb() && + !this.isOOb() && + !message.treatAsDownloadable() && + !this.treatAsDownloadable() && + !message.hasMeCommand() && + !this.hasMeCommand() && + !this.bodyIsOnlyEmojis() && + !message.bodyIsOnlyEmojis() && + ((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint())) && + UIHelper.sameDay(message.getTimeSent(), this.getTimeSent()) && + this.getReadByMarkers().equals(message.getReadByMarkers()) && + !this.conversation.getJid().asBareJid().equals(Config.BUG_REPORTS) + ); + } + + private static boolean isStatusMergeable(int a, int b) { + return a == b || ( + (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND) + || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND) + || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_WAITING) + || (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND) + || (a == Message.STATUS_SEND && b == Message.STATUS_WAITING) + ); + } + + public void setCounterparts(List counterparts) { + this.counterparts = counterparts; + } + + public List getCounterparts() { + return this.counterparts; + } + + @Override + public int getAvatarBackgroundColor() { + if (type == Message.TYPE_STATUS && getCounterparts() != null && getCounterparts().size() > 1) { + return Color.TRANSPARENT; + } else { + return UIHelper.getColorForName(UIHelper.getMessageDisplayName(this)); + } + } + + @Override + public String getAvatarName() { + return UIHelper.getMessageDisplayName(this); + } + + public boolean isOOb() { + return oob; + } + + public static class MergeSeparator { + } + + public SpannableStringBuilder getMergedBody() { + SpannableStringBuilder body = new SpannableStringBuilder(MessageUtils.filterLtrRtl(this.body).trim()); + Message current = this; + while (current.mergeable(current.next())) { + current = current.next(); + if (current == null) { + break; + } + body.append("\n\n"); + body.setSpan(new MergeSeparator(), body.length() - 2, body.length(), + SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE); + body.append(MessageUtils.filterLtrRtl(current.getBody()).trim()); + } + return body; + } + + public boolean hasMeCommand() { + return this.body.trim().startsWith(ME_COMMAND); + } + + public int getMergedStatus() { + int status = this.status; + Message current = this; + while (current.mergeable(current.next())) { + current = current.next(); + if (current == null) { + break; + } + status = current.status; + } + return status; + } + + public long getMergedTimeSent() { + long time = this.timeSent; + Message current = this; + while (current.mergeable(current.next())) { + current = current.next(); + if (current == null) { + break; + } + time = current.timeSent; + } + return time; + } + + public boolean wasMergedIntoPrevious() { + Message prev = this.prev(); + return prev != null && prev.mergeable(this); + } + + public boolean trusted() { + Contact contact = this.getContact(); + return status > STATUS_RECEIVED || (contact != null && (contact.showInContactList() || contact.isSelf())); + } + + public boolean fixCounterpart() { + final Presences presences = conversation.getContact().getPresences(); + if (counterpart != null && presences.has(Strings.nullToEmpty(counterpart.getResource()))) { + return true; + } else if (presences.size() >= 1) { + counterpart = PresenceSelector.getNextCounterpart(getContact(), presences.toResourceArray()[0]); + return true; + } else { + counterpart = null; + return false; + } + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public String getEditedId() { + if (edits.size() > 0) { + return edits.get(edits.size() - 1).getEditedId(); + } else { + throw new IllegalStateException("Attempting to store unedited message"); + } + } + + public String getEditedIdWireFormat() { + if (edits.size() > 0) { + return edits.get(Config.USE_LMC_VERSION_1_1 ? 0 : edits.size() - 1).getEditedId(); + } else { + throw new IllegalStateException("Attempting to store unedited message"); + } + } + + public void setOob(boolean isOob) { + this.oob = isOob; + } + + public String getMimeType() { + String extension; + if (relativeFilePath != null) { + extension = MimeUtils.extractRelevantExtension(relativeFilePath); + } else { + final String url = URL.tryParse(body.split("\n")[0]); + if (url == null) { + return null; + } + extension = MimeUtils.extractRelevantExtension(url); + } + return MimeUtils.guessMimeTypeFromExtension(extension); + } + + public synchronized boolean treatAsDownloadable() { + if (treatAsDownloadable == null) { + treatAsDownloadable = MessageUtils.treatAsDownloadable(this.body, this.oob); + } + return treatAsDownloadable; + } + + public synchronized boolean bodyIsOnlyEmojis() { + if (isEmojisOnly == null) { + isEmojisOnly = Emoticons.isOnlyEmoji(body.replaceAll("\\s", "")); + } + return isEmojisOnly; + } + + public synchronized boolean isGeoUri() { + if (isGeoUri == null) { + isGeoUri = GeoHelper.GEO_URI.matcher(body).matches(); + } + return isGeoUri; + } + + public synchronized void resetFileParams() { + this.fileParams = null; + } + + public synchronized FileParams getFileParams() { + if (fileParams == null) { + fileParams = new FileParams(); + if (this.transferable != null) { + fileParams.size = this.transferable.getFileSize(); + } + final String[] parts = body == null ? new String[0] : body.split("\\|"); + switch (parts.length) { + case 1: + try { + fileParams.size = Long.parseLong(parts[0]); + } catch (final NumberFormatException e) { + fileParams.url = URL.tryParse(parts[0]); + } + break; + case 5: + fileParams.runtime = parseInt(parts[4]); + case 4: + fileParams.width = parseInt(parts[2]); + fileParams.height = parseInt(parts[3]); + case 2: + fileParams.url = URL.tryParse(parts[0]); + fileParams.size = parseLong(parts[1]); + break; + case 3: + fileParams.size = parseLong(parts[0]); + fileParams.width = parseInt(parts[1]); + fileParams.height = parseInt(parts[2]); + break; + } + } + return fileParams; + } + + private static long parseLong(String value) { + try { + return Long.parseLong(value); + } catch (NumberFormatException e) { + return 0; + } + } + + private static int parseInt(String value) { + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + return 0; + } + } + + public void untie() { + this.mNextMessage = null; + this.mPreviousMessage = null; + } + + public boolean isPrivateMessage() { + return type == TYPE_PRIVATE || type == TYPE_PRIVATE_FILE; + } + + public boolean isFileOrImage() { + 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; + } + + public boolean needsUploading() { + return isFileOrImage() && getFileParams().url == null; + } + + public static class FileParams { + public String url; + public long size = 0; + public int width = 0; + public int height = 0; + public int runtime = 0; + } + + public void setFingerprint(String fingerprint) { + this.axolotlFingerprint = fingerprint; + } + + public String getFingerprint() { + return axolotlFingerprint; + } + + public boolean isTrusted() { + FingerprintStatus s = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint); + return s != null && s.isTrusted(); + } + + private int getPreviousEncryption() { + for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()) { + if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) { + continue; + } + return iterator.getEncryption(); + } + return ENCRYPTION_NONE; + } + + private int getNextEncryption() { + if (this.conversation instanceof Conversation) { + Conversation conversation = (Conversation) this.conversation; + for (Message iterator = this.next(); iterator != null; iterator = iterator.next()) { + if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) { + continue; + } + return iterator.getEncryption(); + } + return conversation.getNextEncryption(); + } else { + throw new AssertionError("This should never be called since isInValidSession should be disabled for stubs"); + } + } + + public boolean isValidInSession() { + int pastEncryption = getCleanedEncryption(this.getPreviousEncryption()); + int futureEncryption = getCleanedEncryption(this.getNextEncryption()); + + boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE + || futureEncryption == ENCRYPTION_NONE + || pastEncryption != futureEncryption; + + return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption; + } + + private static int getCleanedEncryption(int encryption) { + if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) { + return ENCRYPTION_PGP; + } + if (encryption == ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE || encryption == ENCRYPTION_AXOLOTL_FAILED) { + return ENCRYPTION_AXOLOTL; + } + return encryption; + } + + public static boolean configurePrivateMessage(final Message message) { + return configurePrivateMessage(message, false); + } + + public static boolean configurePrivateFileMessage(final Message message) { + return configurePrivateMessage(message, true); + } + + private static boolean configurePrivateMessage(final Message message, final boolean isFile) { + final Conversation conversation; + if (message.conversation instanceof Conversation) { + conversation = (Conversation) message.conversation; + } else { + return false; + } + if (conversation.getMode() == Conversation.MODE_MULTI) { + final Jid nextCounterpart = conversation.getNextCounterpart(); + if (nextCounterpart != null) { + message.setCounterpart(nextCounterpart); + message.setTrueCounterpart(conversation.getMucOptions().getTrueCounterpart(nextCounterpart)); + message.setType(isFile ? Message.TYPE_PRIVATE_FILE : Message.TYPE_PRIVATE); + return true; + } + } + return false; + } } diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index ae9ce3bd5..198a2e71a 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -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) { diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index 3f1c5aa49..4b055e158 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -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(); - packet.addChild("x", Namespace.OOB).addChild("url").setContent(content); - } + 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); @@ -225,7 +207,7 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - public MessagePacket received(Account account, final Jid from, final String id, ArrayList namespaces, int type) { + public MessagePacket received(Account account, final Jid from, final String id, ArrayList namespaces, int type) { final MessagePacket receivedPacket = new MessagePacket(); receivedPacket.setType(type); receivedPacket.setTo(from); diff --git a/src/main/java/eu/siacs/conversations/http/AesGcmURL.java b/src/main/java/eu/siacs/conversations/http/AesGcmURL.java new file mode 100644 index 000000000..6cacf64d7 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/http/AesGcmURL.java @@ -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); + } + } + +} diff --git a/src/main/java/eu/siacs/conversations/http/AesGcmURLStreamHandler.java b/src/main/java/eu/siacs/conversations/http/AesGcmURLStreamHandler.java deleted file mode 100644 index 00b5985c2..000000000 --- a/src/main/java/eu/siacs/conversations/http/AesGcmURLStreamHandler.java +++ /dev/null @@ -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(); - } -} diff --git a/src/main/java/eu/siacs/conversations/http/CustomURLStreamHandlerFactory.java b/src/main/java/eu/siacs/conversations/http/CustomURLStreamHandlerFactory.java deleted file mode 100644 index 6974624e2..000000000 --- a/src/main/java/eu/siacs/conversations/http/CustomURLStreamHandlerFactory.java +++ /dev/null @@ -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; - } - } -} diff --git a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java index 6a4e05f58..5d7f7f6c8 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java @@ -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(); + } } diff --git a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java index e55de385a..09e9121ba 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java +++ b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java @@ -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(); - } + 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); - } + 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 { + Log.d(Config.LOGTAG, "retrieve file size. interactive:" + interactive); + changeStatus(STATUS_CHECKING); + final OkHttpClient client = mHttpConnectionManager.buildHttpClient( + mUrl, + message.getConversation().getAccount(), + interactive + ); + final Request request = new Request.Builder() + .url(URL.stripFragment(mUrl)) + .head() + .build(); + mostRecentCall = client.newCall(request); try { - 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 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 long expected = file.getExpectedSize(); - final boolean tryResume = file.exists() && file.getSize() > 0 && file.getSize() < expected; - long resumeSize = 0; + final OkHttpClient client = mHttpConnectionManager.buildHttpClient( + mUrl, + message.getConversation().getAccount(), + interactive + ); - if (tryResume) { - resumeSize = file.getSize(); - Log.d(Config.LOGTAG, "http download trying resume after " + resumeSize + " of " + expected); - connection.setRequestProperty("Range", "bytes=" + resumeSize + "-"); - } - 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.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; + final long resumeSize; + if (tryResume) { + resumeSize = file.getSize(); + Log.d(Config.LOGTAG, "http download trying resume after " + resumeSize + " of " + expected); + requestBuilder.addHeader("Range", String.format(Locale.ENGLISH, "bytes=%d-", resumeSize)); + } else { + resumeSize = 0; + } + final Request request = requestBuilder.build(); + mostRecentCall = client.newCall(request); + final Response response = mostRecentCall.execute(); + final int code = response.code(); + if (code >= 200 && code <= 299) { + final String contentRange = response.header("Content-Range"); + final boolean serverResumed = tryResume && contentRange != null && contentRange.startsWith("bytes " + resumeSize + "-"); + final InputStream inputStream = response.body().byteStream(); + final OutputStream outputStream; long transmitted = 0; 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); diff --git a/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java index a6161701f..a6bacfa62 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java +++ b/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java @@ -1,250 +1,208 @@ 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, AbstractConnectionManager.ProgressListener { -public class HttpUploadConnection implements Transferable { + static final List WHITE_LISTED_HEADERS = Arrays.asList( + "Authorization", + "Cookie", + "Expires" + ); - static final List WHITE_LISTED_HEADERS = Arrays.asList( - "Authorization", - "Cookie", - "Expires" - ); + private final HttpConnectionManager mHttpConnectionManager; + private final XmppConnectionService mXmppConnectionService; + private final Method method; + private boolean delayed = false; + private DownloadableFile file; + private final Message message; + private String mime; + private SlotRequester.Slot slot; + private byte[] key = null; - private final HttpConnectionManager mHttpConnectionManager; - private 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; - private String mime; - private SlotRequester.Slot slot; - private byte[] key = null; + private long transmitted = 0; + private Call mostRecentCall; + private ListenableFuture slotFuture; - private long transmitted = 0; + public HttpUploadConnection(Message message, Method method, HttpConnectionManager httpConnectionManager) { + this.message = message; + this.method = method; + this.mHttpConnectionManager = httpConnectionManager; + this.mXmppConnectionService = httpConnectionManager.getXmppConnectionService(); + } - public HttpUploadConnection(Message message, Method method, HttpConnectionManager httpConnectionManager) { - this.message = message; - this.method = method; - this.mHttpConnectionManager = httpConnectionManager; - this.mXmppConnectionService = httpConnectionManager.getXmppConnectionService(); - this.mSlotRequester = new SlotRequester(this.mXmppConnectionService); - this.mUseTor = mXmppConnectionService.useTorToConnect(); - } + @Override + public boolean start() { + return false; + } - @Override - public boolean start() { - return false; - } + @Override + public int getStatus() { + return STATUS_UPLOADING; + } - @Override - public int getStatus() { - return STATUS_UPLOADING; - } + @Override + public long getFileSize() { + return file == null ? 0 : file.getExpectedSize(); + } - @Override - public long getFileSize() { - return file == null ? 0 : file.getExpectedSize(); - } + @Override + public int getProgress() { + if (file == null) { + return 0; + } + return (int) ((((double) transmitted) / file.getExpectedSize()) * 100); + } - @Override - public int getProgress() { - if (file == null) { - return 0; - } - return (int) ((((double) transmitted) / file.getExpectedSize()) * 100); - } + @Override + public void cancel() { + final ListenableFuture slotFuture = this.slotFuture; + if (slotFuture != null && !slotFuture.isDone()) { + slotFuture.cancel(true); + } + final Call call = this.mostRecentCall; + if (call != null && !call.isCanceled()) { + call.cancel(); + } + } - @Override - public void cancel() { - this.cancelled = true; - } + private void fail(String errorMessage) { + finish(); + final Call call = this.mostRecentCall; + final Future slotFuture = this.slotFuture; + final boolean cancelled = (call != null && call.isCanceled()) || (slotFuture != null && slotFuture.isCancelled()); + mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED, cancelled ? Message.ERROR_MESSAGE_CANCELLED : errorMessage); + } - private void fail(String errorMessage) { - finish(); - 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); - } + private void finish() { + mHttpConnectionManager.finishUploadConnection(this); + message.setTransferable(null); + } - public void init(boolean delay) { - final Account account = message.getConversation().getAccount(); - this.file = mXmppConnectionService.getFileBackend().getFile(message, false); - if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { - this.mime = "application/pgp-encrypted"; - } else { - this.mime = this.file.getMimeType(); - } - final long originalFileSize = file.getSize(); - this.delayed = delay; - if (Config.ENCRYPT_ON_HTTP_UPLOADED - || message.getEncryption() == Message.ENCRYPTION_AXOLOTL - || message.getEncryption() == Message.ENCRYPTION_OTR) { - this.key = new byte[44]; - mXmppConnectionService.getRNG().nextBytes(this.key); - this.file.setKeyAndIv(this.key); - } + public void init(boolean delay) { + final Account account = message.getConversation().getAccount(); + this.file = mXmppConnectionService.getFileBackend().getFile(message, false); + if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { + this.mime = "application/pgp-encrypted"; + } else { + this.mime = this.file.getMimeType(); + } + final long originalFileSize = file.getSize(); + this.delayed = delay; + if (Config.ENCRYPT_ON_HTTP_UPLOADED + || message.getEncryption() == Message.ENCRYPTION_AXOLOTL + || message.getEncryption() == Message.ENCRYPTION_OTR) { + this.key = new byte[44]; + mXmppConnectionService.getRNG().nextBytes(this.key); + this.file.setKeyAndIv(this.key); + } + this.file.setExpectedSize(originalFileSize + (file.getKey() != null ? 16 : 0)); + message.resetFileParams(); + this.slotFuture = new SlotRequester(mXmppConnectionService).request(method, account, file, mime); + Futures.addCallback(this.slotFuture, new FutureCallback() { + @Override + public void onSuccess(@NullableDecl SlotRequester.Slot result) { + HttpUploadConnection.this.slot = result; + HttpUploadConnection.this.upload(); + } - final String md5; + @Override + public void onFailure(@NotNull final Throwable throwable) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to request slot", throwable); + fail(throwable.getMessage()); + } + }, MoreExecutors.directExecutor()); + message.setTransferable(this); + mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND); + } - if (method == Method.P1_S3) { - 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; - } + private void upload() { + 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()); + } - this.file.setExpectedSize(originalFileSize + (file.getKey() != null ? 16 : 0)); - message.resetFileParams(); - this.mSlotRequester.request(method, account, file, mime, md5, new SlotRequester.OnSlotRequested() { - @Override - public void success(SlotRequester.Slot slot) { - if (!cancelled) { - HttpUploadConnection.this.slot = slot; - EXECUTOR.execute(HttpUploadConnection.this::upload); - } - } + @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 String get; + if (key != null) { + get = AesGcmURL.toAesGcmUrl(slot.get.newBuilder().fragment(CryptoHelper.bytesToHex(key)).build()); + } else { + get = slot.get.toString(); + } + mXmppConnectionService.getFileBackend().updateFileParams(message, get); + mXmppConnectionService.getFileBackend().updateMediaScanner(file); + finish(); + if (!message.isPrivateMessage()) { + message.setCounterpart(message.getConversation().getJid().asBareJid()); + } + mXmppConnectionService.resendMessage(message, delayed); + } else { + Log.d(Config.LOGTAG, "http upload failed because response code was " + code); + fail("http upload failed because response code was " + code); + } + } + }); + } - @Override - public void failure(String message) { - fail(message); - } - }); - message.setTransferable(this); - mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND); - } + public Message getMessage() { + return message; + } - 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"); - - 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 entry : slot.getHeaders().entrySet()) { - connection.setRequestProperty(entry.getKey(),entry.getValue()); - } - } - connection.setDoOutput(true); - connection.setDoInput(true); - connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000); - connection.setReadTimeout(readTimeout * 1000); - connection.connect(); - final InputStream innerInputStream = AbstractConnectionManager.upgrade(file, fileInputStream); - os = connection.getOutputStream(); - transmitted = 0; - int count; - byte[] buffer = new byte[4096]; - while (((count = innerInputStream.read(buffer)) != -1) && !cancelled) { - transmitted += count; - os.write(buffer, 0, count); - mHttpConnectionManager.updateConversationUi(false); - } - os.flush(); - os.close(); - int code = connection.getResponseCode(); - InputStream is = connection.getErrorStream(); - if (is != null) { - try (Scanner scanner = new Scanner(is)) { - scanner.useDelimiter("\\Z"); - Log.d(Config.LOGTAG, "body: " + scanner.next()); - } - } - if (code == 200 || code == 201) { - Log.d(Config.LOGTAG, "finished uploading file"); - final URL get; - if (key != null) { - if (method == Method.P1_S3) { - get = new URL(slot.getGetUrl().toString()+"#"+CryptoHelper.bytesToHex(key)); - } else { - get = CryptoHelper.toAesGcmUrl(new URL(slot.getGetUrl().toString() + "#" + CryptoHelper.bytesToHex(key))); - } - } else { - get = slot.getGetUrl(); - } - mXmppConnectionService.getFileBackend().updateFileParams(message, get); - mXmppConnectionService.getFileBackend().updateMediaScanner(file); - finish(); - if (!message.isPrivateMessage()) { - message.setCounterpart(message.getConversation().getJid().asBareJid()); - } - mXmppConnectionService.resendMessage(message, delayed); - } else { - Log.d(Config.LOGTAG,"http upload failed because response code was "+code); - fail("http upload failed because response code was "+code); - } - } catch (Exception e) { - e.printStackTrace(); - Log.d(Config.LOGTAG,"http upload failed "+e.getMessage()); - fail(e.getMessage()); - } finally { - FileBackend.close(fileInputStream); - FileBackend.close(os); - if (connection != null) { - connection.disconnect(); - } - WakeLockHelper.release(wakeLock); - } - } - - public Message getMessage() { - return message; - } -} + @Override + public void onProgress(final long progress) { + this.transmitted = progress; + mHttpConnectionManager.updateConversationUi(false); + } +} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/http/Method.java b/src/main/java/eu/siacs/conversations/http/Method.java index 2f731995b..47dae2b30 100644 --- a/src/main/java/eu/siacs/conversations/http/Method.java +++ b/src/main/java/eu/siacs/conversations/http/Method.java @@ -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; } diff --git a/src/main/java/eu/siacs/conversations/http/P1S3UrlStreamHandler.java b/src/main/java/eu/siacs/conversations/http/P1S3UrlStreamHandler.java deleted file mode 100644 index 2ba92180c..000000000 --- a/src/main/java/eu/siacs/conversations/http/P1S3UrlStreamHandler.java +++ /dev/null @@ -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; - } - } -} diff --git a/src/main/java/eu/siacs/conversations/http/SlotRequester.java b/src/main/java/eu/siacs/conversations/http/SlotRequester.java index b52bab39a..5a3558855 100644 --- a/src/main/java/eu/siacs/conversations/http/SlotRequester.java +++ b/src/main/java/eu/siacs/conversations/http/SlotRequester.java @@ -29,162 +29,126 @@ 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 { - private final XmppConnectionService service; + private final XmppConnectionService service; - public SlotRequester(XmppConnectionService service) { - this.service = service; - } + public SlotRequester(XmppConnectionService service) { + 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); - } else { - requestP1S3(account, account.getDomain(), file.getName(), md5, callback); - } - } + public ListenableFuture 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 { + 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); - service.sendIqPacket(account, request, (a, packet) -> { - if (packet.getType() == IqPacket.TYPE.RESULT) { - 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); - 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 ListenableFuture requestHttpUploadLegacy(Account account, Jid host, DownloadableFile file, String mime) { + final SettableFuture future = SettableFuture.create(); + final IqPacket request = service.getIqGenerator().requestHttpUploadLegacySlot(host, file, mime); + service.sendIqPacket(account, request, (a, packet) -> { + if (packet.getType() == IqPacket.TYPE.RESULT) { + 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) { + 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 (final IllegalArgumentException e) { + future.setException(e); + return; + } + } + } + future.setException(new IqResponseException(IqParser.extractErrorMessage(packet))); + }); + return future; + } - } + private ListenableFuture requestHttpUpload(Account account, Jid host, DownloadableFile file, String mime) { + final SettableFuture future = SettableFuture.create(); + final IqPacket request = service.getIqGenerator().requestHttpUploadSlot(host, file, mime); + service.sendIqPacket(account, request, (a, packet) -> { + if (packet.getType() == IqPacket.TYPE.RESULT) { + final Element slotElement = packet.findChild("slot", Namespace.HTTP_UPLOAD); + if (slotElement != null) { + try { + final Element put = slotElement.findChild("put"); + final Element get = slotElement.findChild("get"); + final String putUrl = put == null ? null : put.getAttribute("url"); + final String getUrl = get == null ? null : get.getAttribute("url"); + if (getUrl != null && putUrl != null) { + final ImmutableMap.Builder headers = new ImmutableMap.Builder<>(); + for (final Element child : put.getChildren()) { + if ("header".equals(child.getName())) { + final String name = child.getAttribute("name"); + final String value = child.getContent(); + if (HttpUploadConnection.WHITE_LISTED_HEADERS.contains(name) && value != null && !value.trim().contains("\n")) { + headers.put(name, value.trim()); + } + } + } + headers.put("Content-Type", mime == null ? "application/octet-stream" : mime); + final Slot slot = new Slot(HttpUrl.get(putUrl), HttpUrl.get(getUrl), headers.build()); + future.set(slot); + return; + } + } catch (final IllegalArgumentException e) { + future.setException(e); + return; + } + } + } + future.setException(new IqResponseException(IqParser.extractErrorMessage(packet))); + }); + return future; + } - private void requestHttpUpload(Account account, Jid host, DownloadableFile file, String mime, OnSlotRequested callback) { - 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); - if (slotElement != null) { - try { - final Element put = slotElement.findChild("put"); - final Element get = slotElement.findChild("get"); - final String putUrl = put == null ? null : put.getAttribute("url"); - final String getUrl = get == null ? null : get.getAttribute("url"); - if (getUrl != null && putUrl != null) { - Slot slot = new Slot(new URL(putUrl)); - slot.getUrl = new URL(getUrl); - slot.headers = new HashMap<>(); - for (Element child : put.getChildren()) { - if ("header".equals(child.getName())) { - final String name = child.getAttribute("name"); - final String value = child.getContent(); - if (HttpUploadConnection.WHITE_LISTED_HEADERS.contains(name) && value != null && !value.trim().contains("\n")) { - slot.headers.put(name, value.trim()); - } - } - } - slot.headers.put("Content-Type", mime == null ? "application/octet-stream" : mime); - callback.success(slot); - return; - } - } catch (MalformedURLException e) { - //fall through - } - } - } - Log.d(Config.LOGTAG, account.getJid().toString() + ": invalid response to slot request " + packet); - callback.failure(IqParser.extractErrorMessage(packet)); - }); + public static class Slot { + public final HttpUrl put; + public final HttpUrl get; + public final Headers headers; - } + private Slot(HttpUrl put, HttpUrl get, Headers headers) { + this.put = put; + this.get = get; + this.headers = headers; + } - private void requestP1S3(final Account account, Jid host, String filename, String md5, OnSlotRequested callback) { - 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); - return; - } - } catch (MalformedURLException e) { - //fall through; - } - } - callback.failure("unable to request slot"); - }); - Log.d(Config.LOGTAG, "requesting slot with p1. md5=" + md5); - } - - - public interface OnSlotRequested { - - void success(Slot slot); - - void failure(String message); - - } - - public static class Slot { - private final URL putUrl; - private URL getUrl; - private HashMap headers; - - private Slot(URL putUrl) { - this.putUrl = putUrl; - } - - public URL getPutUrl() { - return putUrl; - } - - public URL getGetUrl() { - return getUrl; - } - - public HashMap getHeaders() { - return headers; - } - } + private Slot(HttpUrl put, HttpUrl getUrl, Map headers) { + this.put = put; + this.get = getUrl; + this.headers = Headers.of(headers); + } + } } diff --git a/src/main/java/eu/siacs/conversations/http/URL.java b/src/main/java/eu/siacs/conversations/http/URL.java new file mode 100644 index 000000000..e294ed8a0 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/http/URL.java @@ -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 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(); + } + +} diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index dae71abda..f45b1e89b 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -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; diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java index 723a3fee8..9b3e38b3a 100644 --- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java +++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java @@ -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; diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index 690caaafd..f9aea9da8 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -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; } } diff --git a/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java b/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java index dcf8848bf..40e381816 100644 --- a/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java @@ -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 pathSegments final int pos = path.lastIndexOf('/'); final String filename = path.substring(pos + 1).toLowerCase(); final String[] parts = filename.split("\\."); diff --git a/src/main/java/eu/siacs/conversations/services/AttachFileToConversationRunnable.java b/src/main/java/eu/siacs/conversations/services/AttachFileToConversationRunnable.java index 1bffb2bfe..5d39911ed 100644 --- a/src/main/java/eu/siacs/conversations/services/AttachFileToConversationRunnable.java +++ b/src/main/java/eu/siacs/conversations/services/AttachFileToConversationRunnable.java @@ -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; diff --git a/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java b/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java index a706dfb30..3d33eefa5 100644 --- a/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java +++ b/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java @@ -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); - } + builder.proxy(HttpConnectionManager.getProxy()); } 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 result = cache.getIfPresent(key(method, query)); + final List result = cache.getIfPresent(key(method, query)); if (result != null) { onChannelSearchResultsFound.onChannelSearchResultsFound(result); return; diff --git a/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java b/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java index 1aa82d8a3..9f2a5de36 100644 --- a/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java +++ b/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java @@ -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 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 getPoshFingerprints(String domain) { - List cached = getPoshFingerprintsFromCache(domain); + final List cached = getPoshFingerprintsFromCache(domain); if (cached == null) { return getPoshFingerprintsFromServer(domain); } else { @@ -525,19 +531,13 @@ public class MemorizingTrustManager { private List 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 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 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) { - results.add(sha256); - } + 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 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 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 result = new ArrayList<>(); - JSONArray jsonArray = jsonObject.getJSONArray("fingerprints"); + final List 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(); diff --git a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java index 592a36702..b382022b9 100644 --- a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java +++ b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java @@ -26,636 +26,645 @@ import eu.siacs.conversations.xmpp.stanzas.MessagePacket; public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { - private final XmppConnectionService mXmppConnectionService; + private final XmppConnectionService mXmppConnectionService; - private final HashSet queries = new HashSet<>(); - private final ArrayList pendingQueries = new ArrayList<>(); + private final HashSet queries = new HashSet<>(); + private final ArrayList pendingQueries = new ArrayList<>(); - public enum Version { - MAM_0("urn:xmpp:mam:0", true), - MAM_1("urn:xmpp:mam:1", false), - MAM_2("urn:xmpp:mam:2", false); + public enum Version { + MAM_0("urn:xmpp:mam:0", true), + MAM_1("urn:xmpp:mam:1", false), + MAM_2("urn:xmpp:mam:2", false); - public final boolean legacy; - public final String namespace; + public final boolean legacy; + public final String namespace; - Version(String namespace, boolean legacy) { - this.namespace = namespace; - this.legacy = legacy; - } + Version(String namespace, boolean legacy) { + this.namespace = namespace; + this.legacy = legacy; + } - public static Version get(Account account) { - return get(account,null); - } + public static Version get(Account account) { + return get(account, null); + } - public static Version get(Account account, Conversation conversation) { - if (conversation == null || conversation.getMode() == Conversation.MODE_SINGLE) { - return get(account.getXmppConnection().getFeatures().getAccountFeatures()); - } else { - return get(conversation.getMucOptions().getFeatures()); - } - } + public static Version get(Account account, Conversation conversation) { + if (conversation == null || conversation.getMode() == Conversation.MODE_SINGLE) { + return get(account.getXmppConnection().getFeatures().getAccountFeatures()); + } else { + return get(conversation.getMucOptions().getFeatures()); + } + } - private static Version get(List features) { - final Version[] values = values(); - for(int i = values.length -1; i >= 0; --i) { - for(String feature : features) { - if (values[i].namespace.equals(feature)) { - return values[i]; - } - } - } - return MAM_0; - } + private static Version get(List features) { + final Version[] values = values(); + for (int i = values.length - 1; i >= 0; --i) { + for (String feature : features) { + if (values[i].namespace.equals(feature)) { + return values[i]; + } + } + } + return MAM_0; + } - public static boolean has(List features) { - for(String feature : features) { - for(Version version : values()) { - if (version.namespace.equals(feature)) { - return true; - } - } - } - return false; - } + public static boolean has(List features) { + for (String feature : features) { + for (Version version : values()) { + if (version.namespace.equals(feature)) { + return true; + } + } + } + return false; + } - public static Element findResult(MessagePacket packet) { - for(Version version : values()) { - Element result = packet.findChild("result", version.namespace); - if (result != null) { - return result; - } - } - return null; - } + public static Element findResult(MessagePacket packet) { + for (Version version : values()) { + Element result = packet.findChild("result", version.namespace); + if (result != null) { + return result; + } + } + return null; + } - } + } MessageArchiveService(final XmppConnectionService service) { - this.mXmppConnectionService = service; - } + this.mXmppConnectionService = service; + } - private void catchup(final Account account) { - synchronized (this.queries) { - for (Iterator iterator = this.queries.iterator(); iterator.hasNext(); ) { - Query query = iterator.next(); - if (query.getAccount() == account) { - iterator.remove(); - } - } - } - MamReference mamReference = MamReference.max( - mXmppConnectionService.databaseBackend.getLastMessageReceived(account), - mXmppConnectionService.databaseBackend.getLastClearDate(account) - ); - mamReference = MamReference.max(mamReference, mXmppConnectionService.getAutomaticMessageDeletionDate()); - long endCatchup = account.getXmppConnection().getLastSessionEstablished(); - final Query query; - if (mamReference.getTimestamp() == 0) { - return; - } else if (endCatchup - mamReference.getTimestamp() >= Config.MAM_MAX_CATCHUP) { - long startCatchup = endCatchup - Config.MAM_MAX_CATCHUP; - List conversations = mXmppConnectionService.getConversations(); - for (Conversation conversation : conversations) { - if (conversation.getMode() == Conversation.MODE_SINGLE && conversation.getAccount() == account && startCatchup > conversation.getLastMessageTransmitted().getTimestamp()) { - this.query(conversation, startCatchup, true); - } - } - query = new Query(account, new MamReference(startCatchup), 0); - } else { - query = new Query(account, mamReference, 0); - } - synchronized (this.queries) { - this.queries.add(query); - } - this.execute(query); - } + private void catchup(final Account account) { + synchronized (this.queries) { + for (Iterator iterator = this.queries.iterator(); iterator.hasNext(); ) { + Query query = iterator.next(); + if (query.getAccount() == account) { + iterator.remove(); + } + } + } + MamReference mamReference = MamReference.max( + mXmppConnectionService.databaseBackend.getLastMessageReceived(account), + mXmppConnectionService.databaseBackend.getLastClearDate(account) + ); + mamReference = MamReference.max(mamReference, mXmppConnectionService.getAutomaticMessageDeletionDate()); + long endCatchup = account.getXmppConnection().getLastSessionEstablished(); + final Query query; + if (mamReference.getTimestamp() == 0) { + return; + } else if (endCatchup - mamReference.getTimestamp() >= Config.MAM_MAX_CATCHUP) { + long startCatchup = endCatchup - Config.MAM_MAX_CATCHUP; + List conversations = mXmppConnectionService.getConversations(); + for (Conversation conversation : conversations) { + if (conversation.getMode() == Conversation.MODE_SINGLE && conversation.getAccount() == account && startCatchup > conversation.getLastMessageTransmitted().getTimestamp()) { + this.query(conversation, startCatchup, true); + } + } + query = new Query(account, new MamReference(startCatchup), 0); + } else { + query = new Query(account, mamReference, 0); + } + synchronized (this.queries) { + this.queries.add(query); + } + this.execute(query); + } - void catchupMUC(final Conversation conversation) { - if (conversation.getLastMessageTransmitted().getTimestamp() < 0 && conversation.countMessages() == 0) { - query(conversation, - new MamReference(0), - 0, - true); - } else { - query(conversation, - conversation.getLastMessageTransmitted(), - 0, - true); - } - } + void catchupMUC(final Conversation conversation) { + if (conversation.getLastMessageTransmitted().getTimestamp() < 0 && conversation.countMessages() == 0) { + query(conversation, + new MamReference(0), + 0, + true); + } else { + query(conversation, + conversation.getLastMessageTransmitted(), + 0, + true); + } + } - public Query query(final Conversation conversation) { - if (conversation.getLastMessageTransmitted().getTimestamp() < 0 && conversation.countMessages() == 0) { - return query(conversation, - new MamReference(0), - System.currentTimeMillis(), - false); - } else { - return query(conversation, - conversation.getLastMessageTransmitted(), - conversation.getAccount().getXmppConnection().getLastSessionEstablished(), - false); - } - } + public Query query(final Conversation conversation) { + if (conversation.getLastMessageTransmitted().getTimestamp() < 0 && conversation.countMessages() == 0) { + return query(conversation, + new MamReference(0), + System.currentTimeMillis(), + false); + } else { + return query(conversation, + conversation.getLastMessageTransmitted(), + conversation.getAccount().getXmppConnection().getLastSessionEstablished(), + false); + } + } - public boolean isCatchingUp(Conversation conversation) { - final Account account = conversation.getAccount(); - if (account.getXmppConnection().isWaitingForSmCatchup()) { - return true; - } else { - synchronized (this.queries) { - for (Query query : this.queries) { - if (query.getAccount() == account && query.isCatchup() && ((conversation.getMode() == Conversation.MODE_SINGLE && query.getWith() == null) || query.getConversation() == conversation)) { - return true; - } - } - } - return false; - } - } + public boolean isCatchingUp(Conversation conversation) { + final Account account = conversation.getAccount(); + if (account.getXmppConnection().isWaitingForSmCatchup()) { + return true; + } else { + synchronized (this.queries) { + for (Query query : this.queries) { + if (query.getAccount() == account && query.isCatchup() && ((conversation.getMode() == Conversation.MODE_SINGLE && query.getWith() == null) || query.getConversation() == conversation)) { + return true; + } + } + } + return false; + } + } - public Query query(final Conversation conversation, long end, boolean allowCatchup) { - return this.query(conversation, conversation.getLastMessageTransmitted(), end, allowCatchup); - } + public Query query(final Conversation conversation, long end, boolean allowCatchup) { + return this.query(conversation, conversation.getLastMessageTransmitted(), end, allowCatchup); + } - public Query query(Conversation conversation, MamReference start, long end, boolean allowCatchup) { - synchronized (this.queries) { - final Query query; - final MamReference startActual = MamReference.max(start, mXmppConnectionService.getAutomaticMessageDeletionDate()); - if (start.getTimestamp() == 0) { - query = new Query(conversation, startActual, end, false); - query.reference = conversation.getFirstMamReference(); - } else { - if (allowCatchup) { - MamReference maxCatchup = MamReference.max(startActual, System.currentTimeMillis() - Config.MAM_MAX_CATCHUP); - if (maxCatchup.greaterThan(startActual)) { - Query reverseCatchup = new Query(conversation, startActual, maxCatchup.getTimestamp(), false); - this.queries.add(reverseCatchup); - this.execute(reverseCatchup); - } - query = new Query(conversation, maxCatchup, end, true); - } else { - query = new Query(conversation, startActual, end, false); - } - } - if (end != 0 && start.greaterThan(end)) { - return null; - } - this.queries.add(query); - this.execute(query); - return query; - } - } + public Query query(Conversation conversation, MamReference start, long end, boolean allowCatchup) { + synchronized (this.queries) { + final Query query; + final MamReference startActual = MamReference.max(start, mXmppConnectionService.getAutomaticMessageDeletionDate()); + if (start.getTimestamp() == 0) { + query = new Query(conversation, startActual, end, false); + query.reference = conversation.getFirstMamReference(); + } else { + if (allowCatchup) { + MamReference maxCatchup = MamReference.max(startActual, System.currentTimeMillis() - Config.MAM_MAX_CATCHUP); + if (maxCatchup.greaterThan(startActual)) { + Query reverseCatchup = new Query(conversation, startActual, maxCatchup.getTimestamp(), false); + this.queries.add(reverseCatchup); + this.execute(reverseCatchup); + } + query = new Query(conversation, maxCatchup, end, true); + } else { + query = new Query(conversation, startActual, end, false); + } + } + if (end != 0 && start.greaterThan(end)) { + return null; + } + this.queries.add(query); + this.execute(query); + return query; + } + } - void executePendingQueries(final Account account) { - List pending = new ArrayList<>(); - synchronized (this.pendingQueries) { - for (Iterator iterator = this.pendingQueries.iterator(); iterator.hasNext(); ) { - Query query = iterator.next(); - if (query.getAccount() == account) { - pending.add(query); - iterator.remove(); - } - } - } - for (Query query : pending) { - this.execute(query); - } - } + void executePendingQueries(final Account account) { + final List pending = new ArrayList<>(); + synchronized (this.pendingQueries) { + for (Iterator iterator = this.pendingQueries.iterator(); iterator.hasNext(); ) { + Query query = iterator.next(); + if (query.getAccount() == account) { + pending.add(query); + iterator.remove(); + } + } + } + for (Query query : pending) { + this.execute(query); + } + } - private void execute(final Query query) { - final Account account = query.getAccount(); - if (account.getStatus() == Account.State.ONLINE) { - final Conversation conversation = query.getConversation(); - if (conversation != null && conversation.getStatus() == Conversation.STATUS_ARCHIVED) { - throw new IllegalStateException("Attempted to run MAM query for archived conversation"); - } - Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": running mam query " + query.toString()); - final IqPacket packet = this.mXmppConnectionService.getIqGenerator().queryMessageArchiveManagement(query); - this.mXmppConnectionService.sendIqPacket(account, packet, (a, p) -> { - final Element fin = p.findChild("fin", query.version.namespace); - if (p.getType() == IqPacket.TYPE.TIMEOUT) { - synchronized (this.queries) { - this.queries.remove(query); - if (query.hasCallback()) { - query.callback(false); - } - } - } else if (p.getType() == IqPacket.TYPE.RESULT && fin != null) { - final boolean running; - synchronized (this.queries) { - running = this.queries.contains(query); - } - if (running) { - processFin(query, fin); - } else { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": ignoring MAM iq result because query had been killed"); - } - } else if (p.getType() == IqPacket.TYPE.RESULT && query.isLegacy()) { - //do nothing - } else { - Log.d(Config.LOGTAG, a.getJid().asBareJid().toString() + ": error executing mam: " + p.toString()); - finalizeQuery(query, true); - } - }); - } else { - synchronized (this.pendingQueries) { - this.pendingQueries.add(query); - } - } - } + private void execute(final Query query) { + final Account account = query.getAccount(); + if (account.getStatus() == Account.State.ONLINE) { + final Conversation conversation = query.getConversation(); + if (conversation != null && conversation.getStatus() == Conversation.STATUS_ARCHIVED) { + throw new IllegalStateException("Attempted to run MAM query for archived conversation"); + } + Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": running mam query " + query.toString()); + final IqPacket packet = this.mXmppConnectionService.getIqGenerator().queryMessageArchiveManagement(query); + this.mXmppConnectionService.sendIqPacket(account, packet, (a, p) -> { + final Element fin = p.findChild("fin", query.version.namespace); + if (p.getType() == IqPacket.TYPE.TIMEOUT) { + synchronized (this.queries) { + this.queries.remove(query); + if (query.hasCallback()) { + query.callback(false); + } + } + } else if (p.getType() == IqPacket.TYPE.RESULT && fin != null) { + final boolean running; + synchronized (this.queries) { + running = this.queries.contains(query); + } + if (running) { + processFin(query, fin); + } else { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring MAM iq result because query had been killed"); + } + } else if (p.getType() == IqPacket.TYPE.RESULT && query.isLegacy()) { + //do nothing + } else { + Log.d(Config.LOGTAG, a.getJid().asBareJid().toString() + ": error executing mam: " + p.toString()); + finalizeQuery(query, true); + } + }); + } else { + synchronized (this.pendingQueries) { + this.pendingQueries.add(query); + } + } + } - private void finalizeQuery(final Query query, boolean done) { - synchronized (this.queries) { - if (!this.queries.remove(query)) { - throw new IllegalStateException("Unable to remove query from queries"); - } - } - final Conversation conversation = query.getConversation(); - if (conversation != null) { - conversation.sort(); - conversation.setHasMessagesLeftOnServer(!done); - } else { - for (Conversation tmp : this.mXmppConnectionService.getConversations()) { - if (tmp.getAccount() == query.getAccount()) { - tmp.sort(); - } - } - } - if (query.hasCallback()) { - query.callback(done); - } else { - this.mXmppConnectionService.updateConversationUi(); - } - } + private void finalizeQuery(final Query query, boolean done) { + synchronized (this.queries) { + if (!this.queries.remove(query)) { + throw new IllegalStateException("Unable to remove query from queries"); + } + } + final Conversation conversation = query.getConversation(); + if (conversation != null) { + conversation.sort(); + conversation.setHasMessagesLeftOnServer(!done); + } else { + for (Conversation tmp : this.mXmppConnectionService.getConversations()) { + if (tmp.getAccount() == query.getAccount()) { + tmp.sort(); + } + } + } + if (query.hasCallback()) { + query.callback(done); + } else { + this.mXmppConnectionService.updateConversationUi(); + } + } - boolean inCatchup(Account account) { - synchronized (this.queries) { - for (Query query : queries) { - if (query.account == account && query.isCatchup() && query.getWith() == null) { - return true; - } - } - } - return false; - } + boolean inCatchup(Account account) { + synchronized (this.queries) { + for (Query query : queries) { + if (query.account == account && query.isCatchup() && query.getWith() == null) { + return true; + } + } + } + return false; + } - public boolean isCatchupInProgress(Conversation conversation) { - synchronized (this.queries) { - for(Query query : queries) { - if (query.account == conversation.getAccount() && query.isCatchup()) { - final Jid with = query.getWith() == null ? null : query.getWith().asBareJid(); - if ((conversation.getMode() == Conversational.MODE_SINGLE && with == null) || (conversation.getJid().asBareJid().equals(with))) { - return true; - } - } - } - } - return false; - } + public boolean isCatchupInProgress(Conversation conversation) { + synchronized (this.queries) { + for (Query query : queries) { + if (query.account == conversation.getAccount() && query.isCatchup()) { + final Jid with = query.getWith() == null ? null : query.getWith().asBareJid(); + if ((conversation.getMode() == Conversational.MODE_SINGLE && with == null) || (conversation.getJid().asBareJid().equals(with))) { + return true; + } + } + } + } + return false; + } - boolean queryInProgress(Conversation conversation, XmppConnectionService.OnMoreMessagesLoaded callback) { - synchronized (this.queries) { - for (Query query : queries) { - if (query.conversation == conversation) { - if (!query.hasCallback() && callback != null) { - query.setCallback(callback); - } - return true; - } - } - return false; - } - } + boolean queryInProgress(Conversation conversation, XmppConnectionService.OnMoreMessagesLoaded callback) { + synchronized (this.queries) { + for (Query query : queries) { + if (query.conversation == conversation) { + if (!query.hasCallback() && callback != null) { + query.setCallback(callback); + } + return true; + } + } + return false; + } + } - public boolean queryInProgress(Conversation conversation) { - return queryInProgress(conversation, null); - } + public boolean queryInProgress(Conversation conversation) { + return queryInProgress(conversation, null); + } - public void processFinLegacy(Element fin, Jid from) { - Query query = findQuery(fin.getAttribute("queryid")); - if (query != null && query.validFrom(from)) { - processFin(query, fin); - } - } + public void processFinLegacy(Element fin, Jid from) { + Query query = findQuery(fin.getAttribute("queryid")); + if (query != null && query.validFrom(from)) { + processFin(query, fin); + } + } - private void processFin(Query query, Element fin) { - boolean complete = fin.getAttributeAsBoolean("complete"); - Element set = fin.findChild("set", "http://jabber.org/protocol/rsm"); - Element last = set == null ? null : set.findChild("last"); - String count = set == null ? null : set.findChildContent("count"); - Element first = set == null ? null : set.findChild("first"); - Element relevant = query.getPagingOrder() == PagingOrder.NORMAL ? last : first; - boolean abort = (!query.isCatchup() && query.getTotalCount() >= Config.PAGE_SIZE) || query.getTotalCount() >= Config.MAM_MAX_MESSAGES; - if (query.getConversation() != null) { - query.getConversation().setFirstMamReference(first == null ? null : first.getContent()); - } - if (complete || relevant == null || abort) { - //TODO: FIX done logic to look at complete. using count is probably unreliable because it can be ommited and doesn’t work with paging. - boolean done; - if (query.isCatchup()) { - done = false; - } else { - if (count != null) { - try { - done = Integer.parseInt(count) <= query.getTotalCount(); - } catch (NumberFormatException e) { - done = false; - } - } else { - done = query.getTotalCount() == 0; - } - } - done = done || (query.getActualMessageCount() == 0 && !query.isCatchup()); - this.finalizeQuery(query, done); + private void processFin(Query query, Element fin) { + boolean complete = fin.getAttributeAsBoolean("complete"); + Element set = fin.findChild("set", "http://jabber.org/protocol/rsm"); + Element last = set == null ? null : set.findChild("last"); + String count = set == null ? null : set.findChildContent("count"); + Element first = set == null ? null : set.findChild("first"); + Element relevant = query.getPagingOrder() == PagingOrder.NORMAL ? last : first; + boolean abort = (!query.isCatchup() && query.getTotalCount() >= Config.PAGE_SIZE) || query.getTotalCount() >= Config.MAM_MAX_MESSAGES; + if (query.getConversation() != null) { + query.getConversation().setFirstMamReference(first == null ? null : first.getContent()); + } + if (complete || relevant == null || abort) { + //TODO: FIX done logic to look at complete. using count is probably unreliable because it can be ommited and doesn’t work with paging. + boolean done; + if (query.isCatchup()) { + done = false; + } else { + if (count != null) { + try { + done = Integer.parseInt(count) <= query.getTotalCount(); + } catch (NumberFormatException e) { + done = false; + } + } else { + done = query.getTotalCount() == 0; + } + } + done = done || (query.getActualMessageCount() == 0 && !query.isCatchup()); + this.finalizeQuery(query, done); - Log.d(Config.LOGTAG, query.getAccount().getJid().asBareJid() + ": finished mam after " + query.getTotalCount() + "(" + query.getActualMessageCount() + ") messages. messages left=" + !done + " count=" + count); - if (query.isCatchup() && query.getActualMessageCount() > 0) { - mXmppConnectionService.getNotificationService().finishBacklog(true, query.getAccount()); - } - processPostponed(query); - } else { - final Query nextQuery; - if (query.getPagingOrder() == PagingOrder.NORMAL) { - nextQuery = query.next(last == null ? null : last.getContent()); - } else { - nextQuery = query.prev(first == null ? null : first.getContent()); - } - this.execute(nextQuery); - this.finalizeQuery(query, false); - synchronized (this.queries) { - this.queries.add(nextQuery); - } - } - } + Log.d(Config.LOGTAG, query.getAccount().getJid().asBareJid() + ": finished mam after " + query.getTotalCount() + "(" + query.getActualMessageCount() + ") messages. messages left=" + !done + " count=" + count); + if (query.isCatchup() && query.getActualMessageCount() > 0) { + mXmppConnectionService.getNotificationService().finishBacklog(true, query.getAccount()); + } + processPostponed(query); + } else { + final Query nextQuery; + if (query.getPagingOrder() == PagingOrder.NORMAL) { + nextQuery = query.next(last == null ? null : last.getContent()); + } else { + nextQuery = query.prev(first == null ? null : first.getContent()); + } + this.execute(nextQuery); + this.finalizeQuery(query, false); + synchronized (this.queries) { + this.queries.add(nextQuery); + } + } + } - void kill(Conversation conversation) { - final ArrayList toBeKilled = new ArrayList<>(); - synchronized (this.queries) { - for (final Query q : queries) { - if (q.conversation == conversation) { - toBeKilled.add(q); - } - } - } - for (Query q : toBeKilled) { - kill(q); - } - } + void kill(final Conversation conversation) { + final ArrayList toBeKilled = new ArrayList<>(); + synchronized (this.pendingQueries) { + for (final Iterator 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) { + toBeKilled.add(q); + } + } + } + for (final Query q : toBeKilled) { + kill(q); + } + } - private void kill(Query query) { - Log.d(Config.LOGTAG, query.getAccount().getJid().asBareJid() + ": killing mam query prematurely"); - query.callback = null; - this.finalizeQuery(query, false); - if (query.isCatchup() && query.getActualMessageCount() > 0) { - mXmppConnectionService.getNotificationService().finishBacklog(true, query.getAccount()); - } - this.processPostponed(query); - } + private void kill(Query query) { + Log.d(Config.LOGTAG, query.getAccount().getJid().asBareJid() + ": killing mam query prematurely"); + query.callback = null; + this.finalizeQuery(query, false); + if (query.isCatchup() && query.getActualMessageCount() > 0) { + mXmppConnectionService.getNotificationService().finishBacklog(true, query.getAccount()); + } + this.processPostponed(query); + } - private void processPostponed(Query query) { - query.account.getAxolotlService().processPostponed(); - query.pendingReceiptRequests.removeAll(query.receiptRequests); - Log.d(Config.LOGTAG, query.getAccount().getJid().asBareJid() + ": found " + query.pendingReceiptRequests.size() + " pending receipt requests"); - Iterator iterator = query.pendingReceiptRequests.iterator(); - while (iterator.hasNext()) { - ReceiptRequest rr = iterator.next(); - mXmppConnectionService.sendMessagePacket(query.account, mXmppConnectionService.getMessageGenerator().received(query.account, rr.getJid(), rr.getId())); - iterator.remove(); - } - } + private void processPostponed(Query query) { + query.account.getAxolotlService().processPostponed(); + query.pendingReceiptRequests.removeAll(query.receiptRequests); + Log.d(Config.LOGTAG, query.getAccount().getJid().asBareJid() + ": found " + query.pendingReceiptRequests.size() + " pending receipt requests"); + Iterator iterator = query.pendingReceiptRequests.iterator(); + while (iterator.hasNext()) { + ReceiptRequest rr = iterator.next(); + mXmppConnectionService.sendMessagePacket(query.account, mXmppConnectionService.getMessageGenerator().received(query.account, rr.getJid(), rr.getId())); + iterator.remove(); + } + } - public Query findQuery(String id) { - if (id == null) { - return null; - } - synchronized (this.queries) { - for (Query query : this.queries) { - if (query.getQueryId().equals(id)) { - return query; - } - } - return null; - } - } + public Query findQuery(String id) { + if (id == null) { + return null; + } + synchronized (this.queries) { + for (Query query : this.queries) { + if (query.getQueryId().equals(id)) { + return query; + } + } + return null; + } + } - @Override - public void onAdvancedStreamFeaturesAvailable(Account account) { - if (account.getXmppConnection() != null && account.getXmppConnection().getFeatures().mam()) { - this.catchup(account); - } - } + @Override + public void onAdvancedStreamFeaturesAvailable(Account account) { + if (account.getXmppConnection() != null && account.getXmppConnection().getFeatures().mam()) { + this.catchup(account); + } + } - public enum PagingOrder { - NORMAL, - REVERSE - } + public enum PagingOrder { + NORMAL, + REVERSE + } - public class Query { - private HashSet pendingReceiptRequests = new HashSet<>(); - private HashSet receiptRequests = new HashSet<>(); - private int totalCount = 0; - private int actualCount = 0; - private int actualInThisQuery = 0; - private long start; - private final long end; - private final String queryId; - private String reference = null; - private final Account account; - private Conversation conversation; - private PagingOrder pagingOrder = PagingOrder.NORMAL; - private XmppConnectionService.OnMoreMessagesLoaded callback = null; - private boolean catchup = true; - public final Version version; + public class Query { + private HashSet pendingReceiptRequests = new HashSet<>(); + private HashSet receiptRequests = new HashSet<>(); + private int totalCount = 0; + private int actualCount = 0; + private int actualInThisQuery = 0; + private long start; + private final long end; + private final String queryId; + private String reference = null; + private final Account account; + private Conversation conversation; + private PagingOrder pagingOrder = PagingOrder.NORMAL; + private XmppConnectionService.OnMoreMessagesLoaded callback = null; + private boolean catchup = true; + public final Version version; - Query(Conversation conversation, MamReference start, long end, boolean catchup) { - this(conversation.getAccount(), Version.get(conversation.getAccount(), conversation), catchup ? start : start.timeOnly(), end); - this.conversation = conversation; - this.pagingOrder = catchup ? PagingOrder.NORMAL : PagingOrder.REVERSE; - this.catchup = catchup; - } + Query(Conversation conversation, MamReference start, long end, boolean catchup) { + this(conversation.getAccount(), Version.get(conversation.getAccount(), conversation), catchup ? start : start.timeOnly(), end); + this.conversation = conversation; + this.pagingOrder = catchup ? PagingOrder.NORMAL : PagingOrder.REVERSE; + this.catchup = catchup; + } - Query(Account account, MamReference start, long end) { - this(account, Version.get(account), start, end); - } + Query(Account account, MamReference start, long end) { + this(account, Version.get(account), start, end); + } - Query(Account account, Version version, MamReference start, long end) { - this.account = account; - if (start.getReference() != null) { - this.reference = start.getReference(); - } else { - this.start = start.getTimestamp(); - } - this.end = end; - this.queryId = new BigInteger(50, mXmppConnectionService.getRNG()).toString(32); - this.version = version; - } + Query(Account account, Version version, MamReference start, long end) { + this.account = account; + if (start.getReference() != null) { + this.reference = start.getReference(); + } else { + this.start = start.getTimestamp(); + } + this.end = end; + this.queryId = new BigInteger(50, mXmppConnectionService.getRNG()).toString(32); + this.version = version; + } - private Query page(String reference) { - Query query = new Query(this.account, this.version, new MamReference(this.start, reference), this.end); - query.conversation = conversation; - query.totalCount = totalCount; - query.actualCount = actualCount; - query.pendingReceiptRequests = pendingReceiptRequests; - query.receiptRequests = receiptRequests; - query.callback = callback; - query.catchup = catchup; - return query; - } + private Query page(String reference) { + Query query = new Query(this.account, this.version, new MamReference(this.start, reference), this.end); + query.conversation = conversation; + query.totalCount = totalCount; + query.actualCount = actualCount; + query.pendingReceiptRequests = pendingReceiptRequests; + query.receiptRequests = receiptRequests; + query.callback = callback; + query.catchup = catchup; + return query; + } - public void removePendingReceiptRequest(ReceiptRequest receiptRequest) { - if (!this.pendingReceiptRequests.remove(receiptRequest)) { - this.receiptRequests.add(receiptRequest); - } - } + public void removePendingReceiptRequest(ReceiptRequest receiptRequest) { + if (!this.pendingReceiptRequests.remove(receiptRequest)) { + this.receiptRequests.add(receiptRequest); + } + } - public void addPendingReceiptRequest(ReceiptRequest receiptRequest) { - this.pendingReceiptRequests.add(receiptRequest); - } + public void addPendingReceiptRequest(ReceiptRequest receiptRequest) { + this.pendingReceiptRequests.add(receiptRequest); + } - public boolean isLegacy() { - return version.legacy; - } + public boolean isLegacy() { + return version.legacy; + } - public boolean safeToExtractTrueCounterpart() { - return muc() && !isLegacy(); - } + public boolean safeToExtractTrueCounterpart() { + return muc() && !isLegacy(); + } - public Query next(String reference) { - Query query = page(reference); - query.pagingOrder = PagingOrder.NORMAL; - return query; - } + public Query next(String reference) { + Query query = page(reference); + query.pagingOrder = PagingOrder.NORMAL; + return query; + } - Query prev(String reference) { - Query query = page(reference); - query.pagingOrder = PagingOrder.REVERSE; - return query; - } + Query prev(String reference) { + Query query = page(reference); + query.pagingOrder = PagingOrder.REVERSE; + return query; + } - public String getReference() { - return reference; - } + public String getReference() { + return reference; + } - public PagingOrder getPagingOrder() { - return this.pagingOrder; - } + public PagingOrder getPagingOrder() { + return this.pagingOrder; + } - public String getQueryId() { - return queryId; - } + public String getQueryId() { + return queryId; + } - public Jid getWith() { - return conversation == null ? null : conversation.getJid().asBareJid(); - } + public Jid getWith() { + return conversation == null ? null : conversation.getJid().asBareJid(); + } - public boolean muc() { - return conversation != null && conversation.getMode() == Conversation.MODE_MULTI; - } + public boolean muc() { + return conversation != null && conversation.getMode() == Conversation.MODE_MULTI; + } - public long getStart() { - return start; - } + public long getStart() { + return start; + } - public boolean isCatchup() { - return catchup; - } + public boolean isCatchup() { + return catchup; + } - public void setCallback(XmppConnectionService.OnMoreMessagesLoaded callback) { - this.callback = callback; - } + public void setCallback(XmppConnectionService.OnMoreMessagesLoaded callback) { + this.callback = callback; + } - public void callback(boolean done) { - if (this.callback != null) { - this.callback.onMoreMessagesLoaded(actualCount, conversation); - if (done) { - this.callback.informUser(R.string.no_more_history_on_server); - } - } - } + public void callback(boolean done) { + if (this.callback != null) { + this.callback.onMoreMessagesLoaded(actualCount, conversation); + if (done) { + this.callback.informUser(R.string.no_more_history_on_server); + } + } + } - public long getEnd() { - return end; - } + public long getEnd() { + return end; + } - public Conversation getConversation() { - return conversation; - } + public Conversation getConversation() { + return conversation; + } - public Account getAccount() { - return this.account; - } + public Account getAccount() { + return this.account; + } - public void incrementMessageCount() { - this.totalCount++; - } + public void incrementMessageCount() { + this.totalCount++; + } - public void incrementActualMessageCount() { - this.actualInThisQuery++; - this.actualCount++; - } + public void incrementActualMessageCount() { + this.actualInThisQuery++; + this.actualCount++; + } - int getTotalCount() { - return this.totalCount; - } + int getTotalCount() { + return this.totalCount; + } - int getActualMessageCount() { - return this.actualCount; - } + int getActualMessageCount() { + return this.actualCount; + } - public int getActualInThisQuery() { - return this.actualInThisQuery; - } + public int getActualInThisQuery() { + return this.actualInThisQuery; + } - public boolean validFrom(Jid from) { - if (muc()) { - return getWith().equals(from); - } else { - return (from == null) || account.getJid().asBareJid().equals(from.asBareJid()); - } - } + public boolean validFrom(Jid from) { + if (muc()) { + return getWith().equals(from); + } else { + return (from == null) || account.getJid().asBareJid().equals(from.asBareJid()); + } + } - @NotNull - @Override - public String toString() { - StringBuilder builder = new StringBuilder(); - if (this.muc()) { - builder.append("to="); - builder.append(this.getWith().toString()); - } else { - builder.append("with="); - if (this.getWith() == null) { - builder.append("*"); - } else { - builder.append(getWith().toString()); - } - } - if (this.start != 0) { - builder.append(", start="); - builder.append(AbstractGenerator.getTimestamp(this.start)); - } - if (this.end != 0) { - builder.append(", end="); - builder.append(AbstractGenerator.getTimestamp(this.end)); - } - builder.append(", order=").append(pagingOrder.toString()); - if (this.reference != null) { - if (this.pagingOrder == PagingOrder.NORMAL) { - builder.append(", after="); - } else { - builder.append(", before="); - } - builder.append(this.reference); - } - builder.append(", catchup=").append(catchup); - builder.append(", ns=").append(version.namespace); - return builder.toString(); - } + @NotNull + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (this.muc()) { + builder.append("to="); + builder.append(this.getWith().toString()); + } else { + builder.append("with="); + if (this.getWith() == null) { + builder.append("*"); + } else { + builder.append(getWith().toString()); + } + } + if (this.start != 0) { + builder.append(", start="); + builder.append(AbstractGenerator.getTimestamp(this.start)); + } + if (this.end != 0) { + builder.append(", end="); + builder.append(AbstractGenerator.getTimestamp(this.end)); + } + builder.append(", order=").append(pagingOrder.toString()); + if (this.reference != null) { + if (this.pagingOrder == PagingOrder.NORMAL) { + builder.append(", after="); + } else { + builder.append(", before="); + } + builder.append(this.reference); + } + builder.append(", catchup=").append(catchup); + builder.append(", ns=").append(version.namespace); + return builder.toString(); + } - boolean hasCallback() { - return this.callback != null; - } - } + boolean hasCallback() { + return this.callback != null; + } + } } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 5880dfa56..044331698 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -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 { diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 12048ebcb..725d11a4d 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -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 = 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()); diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index e7c042c06..25d3a0738 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -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); } diff --git a/src/main/java/eu/siacs/conversations/ui/LocationActivity.java b/src/main/java/eu/siacs/conversations/ui/LocationActivity.java index 3e830c7c0..ed08fa0d2 100644 --- a/src/main/java/eu/siacs/conversations/ui/LocationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/LocationActivity.java @@ -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"); - } + config.setHttpProxy(HttpConnectionManager.getProxy()); } } diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 7f046def8..496244300 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -874,7 +874,12 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } private void enableVideo(View view) { - requireRtpConnection().setVideoEnabled(true); + try { + requireRtpConnection().setVideoEnabled(true); + } catch (final IllegalStateException e) { + Toast.makeText(this, R.string.unable_to_enable_video, Toast.LENGTH_SHORT).show(); + return; + } updateInCallButtonConfigurationVideo(true, requireRtpConnection().isCameraSwitchable()); } diff --git a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java index b2ba8fd32..68cc42921 100644 --- a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java @@ -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) { diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java index af57d7cc9..021ec4a5e 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -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.isCameraFeatureAvailable = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY); this.mTheme = findTheme(); setTheme(this.mTheme); } diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index 76a0a1277..d8c9c5704 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -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 { 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 { this.highlightedTerm = terms == null ? null : StylingHelper.filterHighlightedWords(terms); } - public interface OnQuoteListener { - void onQuote(String text); - } - public interface OnContactPictureClicked { void onContactPictureClicked(Message message); } diff --git a/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java b/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java index fb92f47bf..2470a428f 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java +++ b/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java @@ -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)) { diff --git a/src/main/java/eu/siacs/conversations/utils/AccountUtils.java b/src/main/java/eu/siacs/conversations/utils/AccountUtils.java index a324b242e..8f0453218 100644 --- a/src/main/java/eu/siacs/conversations/utils/AccountUtils.java +++ b/src/main/java/eu/siacs/conversations/utils/AccountUtils.java @@ -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; } } diff --git a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java index 3174c4198..4ee826c3c 100644 --- a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java @@ -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; diff --git a/src/main/java/eu/siacs/conversations/utils/MessageUtils.java b/src/main/java/eu/siacs/conversations/utils/MessageUtils.java index d8b991894..9fc6c5ca8 100644 --- a/src/main/java/eu/siacs/conversations/utils/MessageUtils.java +++ b/src/main/java/eu/siacs/conversations/utils/MessageUtils.java @@ -31,86 +31,90 @@ 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 { - private static final Pattern LTR_RTL = Pattern.compile("(\\u200E[^\\u200F]*\\u200F){3,}"); + private static final Pattern LTR_RTL = Pattern.compile("(\\u200E[^\\u200F]*\\u200F){3,}"); - private static final String EMPTY_STRING = ""; + private static final String EMPTY_STRING = ""; - public static String prepareQuote(Message message) { - final StringBuilder builder = new StringBuilder(); - final String body; - if (message.hasMeCommand()) { - final String nick; - if (message.getStatus() == Message.STATUS_RECEIVED) { - if (message.getConversation().getMode() == Conversational.MODE_MULTI) { - nick = Strings.nullToEmpty(message.getCounterpart().getResource()); - } else { - nick = message.getContact().getPublicDisplayName(); - } - } else { - nick = UIHelper.getMessageDisplayName(message); - } - body = nick + " " + message.getBody().substring(Message.ME_COMMAND.length()); - } else { - body = message.getMergedBody().toString(); + public static String prepareQuote(Message message) { + final StringBuilder builder = new StringBuilder(); + final String body; + if (message.hasMeCommand()) { + final String nick; + if (message.getStatus() == Message.STATUS_RECEIVED) { + if (message.getConversation().getMode() == Conversational.MODE_MULTI) { + nick = Strings.nullToEmpty(message.getCounterpart().getResource()); + } else { + nick = message.getContact().getPublicDisplayName(); + } + } else { + nick = UIHelper.getMessageDisplayName(message); + } + body = nick + " " + message.getBody().substring(Message.ME_COMMAND.length()); + } else { + body = message.getMergedBody().toString(); } - for (String line : body.split("\n")) { - if (line.length() <= 0) { - continue; - } - final char c = line.charAt(0); - if (c == '>' && UIHelper.isPositionFollowedByQuoteableCharacter(line, 0) - || (c == '\u00bb' && !UIHelper.isPositionFollowedByQuote(line, 0))) { - continue; - } - if (builder.length() != 0) { - builder.append('\n'); - } - builder.append(line.trim()); - } - return builder.toString(); - } + for (String line : body.split("\n")) { + if (line.length() <= 0) { + continue; + } + final char c = line.charAt(0); + if (c == '>' && UIHelper.isPositionFollowedByQuoteableCharacter(line, 0) + || (c == '\u00bb' && !UIHelper.isPositionFollowedByQuote(line, 0))) { + continue; + } + if (builder.length() != 0) { + builder.append('\n'); + } + builder.append(line.trim()); + } + return builder.toString(); + } - 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) { - 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) { - return false; - } - } + public static boolean treatAsDownloadable(final String body, final boolean oob) { + final String[] lines = body.split("\n"); + if (lines.length == 0) { + return false; + } + for (final String line : lines) { + if (line.contains("\\s+")) { + return false; + } + } + 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) { - return LTR_RTL.matcher(body).replaceFirst(EMPTY_STRING); - } + public static String filterLtrRtl(String body) { + return LTR_RTL.matcher(body).replaceFirst(EMPTY_STRING); + } - public static boolean unInitiatedButKnownSize(Message message) { - return message.getType() == Message.TYPE_TEXT && message.getTransferable() == null && message.isOOb() && message.getFileParams().size > 0 && message.getFileParams().url != null; - } + public static boolean unInitiatedButKnownSize(Message message) { + return message.getType() == Message.TYPE_TEXT && message.getTransferable() == null && message.isOOb() && message.getFileParams().size > 0 && message.getFileParams().url != null; + } } diff --git a/src/main/java/eu/siacs/conversations/utils/MimeUtils.java b/src/main/java/eu/siacs/conversations/utils/MimeUtils.java index 1aea44565..53e2e1917 100644 --- a/src/main/java/eu/siacs/conversations/utils/MimeUtils.java +++ b/src/main/java/eu/siacs/conversations/utils/MimeUtils.java @@ -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); } diff --git a/src/main/java/eu/siacs/conversations/utils/Resolver.java b/src/main/java/eu/siacs/conversations/utils/Resolver.java index 22c898f15..3344e0290 100644 --- a/src/main/java/eu/siacs/conversations/utils/Resolver.java +++ b/src/main/java/eu/siacs/conversations/utils/Resolver.java @@ -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 results = new ArrayList<>(); final List fallbackResults = new ArrayList<>(); - Thread[] threads = new Thread[3]; + final Thread[] threads = new Thread[3]; threads[0] = new Thread(() -> { try { final List 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 { diff --git a/src/main/java/eu/siacs/conversations/utils/XmppUri.java b/src/main/java/eu/siacs/conversations/utils/XmppUri.java index f3aa65155..d0f4cf421 100644 --- a/src/main/java/eu/siacs/conversations/utils/XmppUri.java +++ b/src/main/java/eu/siacs/conversations/utils/XmppUri.java @@ -32,7 +32,7 @@ public class XmppUri { private Map parameters = Collections.emptyMap(); private boolean safeSource = true; - public XmppUri(String uri) { + public XmppUri(final String uri) { try { parse(Uri.parse(uri)); } catch (IllegalArgumentException e) { diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index 29b4bf395..b0c4fe85c 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -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"; diff --git a/src/main/java/eu/siacs/conversations/xmpp/IqResponseException.java b/src/main/java/eu/siacs/conversations/xmpp/IqResponseException.java new file mode 100644 index 000000000..84357eaa3 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/IqResponseException.java @@ -0,0 +1,8 @@ +package eu.siacs.conversations.xmpp; + +public class IqResponseException extends Exception { + + public IqResponseException(final String message) { + super(message); + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 19cbdb537..bc20d3c5f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -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")) { - throw new StateChangingError(Account.State.REGISTRATION_WEB); - } - } catch (MalformedURLException e) { - //fall through - } + 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); } 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; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index ecbf73729..638fa8cf8 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -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); @@ -305,7 +308,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web throw new SecurityException("Unable to verify DTLS Fingerprint with OMEMO", e); } this.omemoVerification.setOrEnsureEqual(omemoVerifiedPayload); - Log.d(Config.LOGTAG,id.account.getJid().asBareJid()+": received verifiable DTLS fingerprint via "+this.omemoVerification); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received verifiable DTLS fingerprint via " + this.omemoVerification); return omemoVerifiedPayload.getPayload(); } else if (expectVerification) { throw new SecurityException("DTLS fingerprint was unexpectedly not verifiable"); @@ -677,7 +680,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web this.omemoVerification.setDeviceId(remoteDeviceId); } else { if (remoteDeviceId != null) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid()+": remote party signaled support for OMEMO verification but we have OMEMO disabled"); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": remote party signaled support for OMEMO verification but we have OMEMO disabled"); } this.omemoVerification.setDeviceId(null); } @@ -781,7 +784,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web try { verifiedPayload = id.account.getAxolotlService().encrypt(rtpContentMap, id.with, omemoVerification.getDeviceId()); } catch (final CryptoFailedException e) { - Log.w(Config.LOGTAG,id.account.getJid().asBareJid()+": unable to use OMEMO DTLS verification on outgoing session initiate. falling back", e); + Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to use OMEMO DTLS verification on outgoing session initiate. falling back", e); return rtpContentMap; } this.omemoVerification.setSessionFingerprint(verifiedPayload.getFingerprint()); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java index 38935d8fb..3e02cc29b 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java @@ -40,7 +40,7 @@ public class RtpContentMap { } public static RtpContentMap of(final JinglePacket jinglePacket) { - final Map contents = DescriptionTransport.of(jinglePacket.getJingleContents()); + final Map contents = DescriptionTransport.of(jinglePacket.getJingleContents()); if (isOmemoVerified(contents)) { return new OmemoVerifiedRtpContentMap(jinglePacket.getGroup(), contents); } else { @@ -53,7 +53,7 @@ public class RtpContentMap { if (values.size() == 0) { return false; } - for(final DescriptionTransport descriptionTransport : values) { + for (final DescriptionTransport descriptionTransport : values) { if (descriptionTransport.transport instanceof OmemoVerifiedIceUdpTransportInfo) { continue; } @@ -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); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java index 8f0b0d4fe..9a506513b 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java @@ -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 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(); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index 454bb7a73..dca04c000 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -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 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(); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Proceed.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Proceed.java index a9c399754..b7b9da600 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Proceed.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Proceed.java @@ -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; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java index 9a1630f80..650c26bef 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java @@ -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> parameterMap = new HashMap<>(); final ArrayListMultimap feedbackNegotiationMap = ArrayListMultimap.create(); final ArrayListMultimap sourceParameterMap = ArrayListMultimap.create(); + final Set attributes = Sets.newHashSet(Iterables.concat( + sessionDescription.attributes.keySet(), + media.attributes.keySet() + )); 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) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractAcknowledgeableStanza.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractAcknowledgeableStanza.java index cd087f3ab..2291a9896 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractAcknowledgeableStanza.java +++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractAcknowledgeableStanza.java @@ -5,44 +5,38 @@ import eu.siacs.conversations.xmpp.InvalidJid; abstract public class AbstractAcknowledgeableStanza extends AbstractStanza { - protected AbstractAcknowledgeableStanza(String name) { - super(name); - } + protected AbstractAcknowledgeableStanza(String name) { + super(name); + } - public String getId() { - return this.getAttribute("id"); - } + public String getId() { + return this.getAttribute("id"); + } - public void setId(final String id) { - setAttribute("id", id); - } + public void setId(final String id) { + setAttribute("id", id); + } - public Element getError() { - Element error = findChild("error"); - if (error != null) { - for(Element element : error.getChildren()) { - if (!element.getName().equals("text")) { - return element; - } - } - } - return null; - } + 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; - } + public String getErrorCondition() { + final Element condition = getErrorConditionElement(); + return condition == null ? null : condition.getName(); + } - public boolean valid() { - return InvalidJid.isValid(getFrom()) && InvalidJid.isValid(getTo()); - } + public boolean valid() { + return InvalidJid.isValid(getFrom()) && InvalidJid.isValid(getTo()); + } } diff --git a/src/main/res/values-ca/strings.xml b/src/main/res/values-ca/strings.xml index 822a20e30..4e7d077d7 100644 --- a/src/main/res/values-ca/strings.xml +++ b/src/main/res/values-ca/strings.xml @@ -942,4 +942,4 @@ que l\'administrador del servidor llegeixi els missatges, però pot ser l\'únic No es pot processar la invitació El servidor no admet la generació d\'invitacions Cap compte actiu admet aquesta funció - + diff --git a/src/main/res/values-cs/strings.xml b/src/main/res/values-cs/strings.xml index 8c344440f..7e1b66976 100644 --- a/src/main/res/values-cs/strings.xml +++ b/src/main/res/values-cs/strings.xml @@ -16,6 +16,8 @@ Odblokovat kontakt Zablokovat doménu Odblokovat doménu + Blokovat účastníka + Odblokovat účatníka Nastavení účtů Nastavení Sdílet s konverzací @@ -27,10 +29,24 @@ právě teď před minutou před %d minutami + + %d nepřečtená konverzace + + + %d nepřečtené konverzace + + + %d nepřečtených konverzací + + + %d nepřečtených konverzací + + odesílám… - Dešifrování zprávy. Chvíli strpení... + Dešifrování zprávy. Chvíli strpení… OpenPGP šifrovaná zpráva Přezdívka se již používá + Neplatná přezdívka Administrátor Vlastník Moderátor @@ -41,12 +57,12 @@ Chcete odblokovat příjem zpráv od %s? Zablokovat všechny kontakty z %s? Odblokovat všechny kontakty z %s? - Kontakty zablokovány + Kontakt zablokován Zablokovaný Přejete si odstranit %s ze záložek? Předešlé rozhovory pod záložkou nebudou odstraněny. Registrovat nový účet na serveru Změnit heslo na serveru - Sdílet s... + Sdílet s… Začít konverzaci Pozvat kontakt Pozvat @@ -61,21 +77,26 @@ Odblokovat Uložit OK + %1$s přestal reagovat + Zasíláním detailů o důvodu selhání z Vašeho XMPP účtu pomůžete dalšímu vývoji %1$s. Odeslat teď Již se neptat Nelze se připojit k účtu + Nebylo možné se připojit k několika účtům + Ťukněte pro nastavení účtů Přiložit soubor Přidat chybějící kontakt do seznamu kontaktů? Přidat kontakt doručení selhalo Připravuji odeslání obrázku Připravuji odeslání obrázků - Sdílení souborů. Chvíli strpení... + Sdílení souborů. Chvíli strpení… Smazat historii Smaže historii konverzací Opravdu chcete smazat všechny zprávy v této konverzace?\n\nVarováníToto neovlivní zprávy uložené na jiných zařízeních či serverech. Smazat soubor Opravdu chcete smazat tento soubor?\n\nVarováníToto neovlivní kopie uložené na jiných zařízeních či serverech. + Poté zavřít tuto konverzaci Vybrat přístroj Odeslat nešifrovanou zprávu Odeslat zprávu @@ -83,16 +104,20 @@ Poslat OMEMO šifrovanou zprávu Odeslat v\\OMEMO šifrovanou zprávu Poslat OpenPGP šifrovanou zprávu + Přezdívka změněna Poslat nešifrované Zašifrování se nezdařilo. Možná nemáte správný privátní klíč. OpenKeychain + OpenKeychain k šifrování a dešifrování zpráv a ke správě Vašich veřejných klíčů.

OpenKeychain je vydána pod licencí GPLv3+ a dostupná na F-Droid nebo Google Play.

(Po instalaci, prosím, restartujte%1$s.)]]>
Restartovat Instalovat Nainstalujte prosím OpenKeychain nabízí… čekám… Nebyl nalezen žádný OpenPGP klíč + Není možné zašifrovat zprávy, protože kontakt neoznamuje svůj veřejný klíč.\n\nPožádejte kontakt, aby si nastavil OpenPGP. Nebyly nalezeny žádné OpenPGP klíče + Není možné zašifrovat zprávy, protože kontakty neoznamují svůj veřejný klíč.\n\nPožádejte je, aby si nastavili OpenPGP. Obecné Přijímat soubory Automaticky přijímat soubory menší než… @@ -103,13 +128,19 @@ LED upozornění Blikat při přijetí nové zprávy Vyzváněcí tón + Zvuk upozornění + Zvuk upozornění na nové zprávy + Vyzváněcí tón pro příchozí hovory Časová lhůta Časová lhůta po kterou bude Conversations v tichém režimu při zaznamenání aktivity na jiném přístroji Rozšířené Neodesílat detaily o pádu aplikace + Zasíláním detailů o důvodu selhání pomůžete dalšímu vývoji Potvrzovat zprávy Nechat kontaky vědět kdy jste dostali a přečetli jejich zprávy UI + Chyba OpenKeychain. + Chybný klíč pro šifrování. Přijmout Došlo k chybě Chyba @@ -121,8 +152,10 @@ Vyfotit obrázek Aktivně povolovat vyžádání změn stavu Vybraný soubor není obrázek + Nebylo možné převést obrázek Soubor nenalezen Obecná I/O chyba. Že by již nebylo volné místo? + Aplikace, kterou jste použil(a) k výběru obrázku, neposkytla dostatečná oprávnění ke čtení souboru.\n\nPoužijte jiného správce souborů k výběru obrázku. Neznámý Dočasně vypnuto Online @@ -134,9 +167,13 @@ Registrace selhala Uživatelské jméno se již používá Registrace dokončena + Registrace není podporována serverem + Chybný registrační token + Vyjednávání TLS selhalo Porušení podmínek Nekompatibilní server Chyba přenosu + Chyba při otevírání proudu Nešifrováno OTR OpenPGP @@ -145,13 +182,19 @@ Dočasně vypnout Zveřejnit avatar Zveřejnit OpenPGP klíč + Odstranit veřejný klíč OpenPGP + 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. OpenPGP veřejný klíč zveřejněn. Povolit účet Jste si jisti? + Smazáním Vašeho účtu dojde k vymazání celé Vaší historie konverzací. Nahrát hlas Adresa XMPP + Blokovat XMPP adresu jmeno@server.cz Heslo + Toto není platná XMPP adresa + Nedostatek paměti. Obrázek je příliš velký Chcete přidat %s do svého adresáře? Údaje serveru XEP-0313: MAM @@ -178,9 +221,11 @@ OpenPGP ID klíče OMEMO otisk v\\OMEMO otisk + OMEMO otisk (původce zprávy) + v\\OMEMO otisk (původce zprávy) Ostatní přístroje Věřit OMEMO otiskům - Získávání klíčů... + Získávání klíčů… Hotovo Dešifrovat Záložky @@ -198,56 +243,99 @@ kanál@konference.server.cz Uložit jako záložku Smazat záložku + Zrušit skupinový chat Zrušit kanál + Skutečně chcete zrušit skupinový chat?\n\nVarování: Skupinový chat bude zcela odstraněn ze serveru. + Skutečně chcete zrušit veřejný kanál?\n\nVarování: Kanál bude zcela odstraněn ze serveru. + Nebylo možné zrušit skupinový chat + Nebylo možné zrušit kanál + Upravit předmět skupinového chatu + Téma + Připojuji se ke skupinovému chatu… Odejít Kontakt přidán do seznamu Opět přidat %s dočetl(a) až sem + %s dočetli až sem + %1$s +%2$d ostatní(ch) dočetli až sem + Všichni dočetli až sem Zveřejnit + Ťuknutím na avatar vyberete obrázek z galerie Zveřejňuji… Server odmítl toto zveřejnění + Nebylo možné převést Váš obrázek Nepodařilo se uložit avatar na disk (Stisknout dlouze pro obnovení výchozího stavu) + Váš server nepodporuje zveřejňování avataru šeptem pro %s Zaslat soukromou zprávu pro %s Připojit Tento účet již existuje Další + Sezení vytvořeno Přeskočit Vypnout upozornění Povolit + Požadováno heslo ke skupinovému chatu Vložit heslo + Nejdříve, prosím, od kontaktu vyžádejte zasílání informací o změně stavu.\n\nTo bude využito k identifikaci aplikace, kterou kontakt používá. Ihned vyžádat Ignorovat + Varování: Odeslání bez povolení vzájemného informování o změně stavu může způsobit nečekané potíže.\n\nJděte do \"Detaily kontaktu\" a ověřte nastavení aktualizace stavu. Zabezpečení Povolit opravu zpráv Povolí kontaktům zpětné upravování jejich zpráv Expertní nastavení S tímto zacházejte velmi opatrně + O %s Tichý režim Odkdy Dokdy Povolit tichý režim Upozornění budou během tichého režimu ztlumena Další + Synchronizovat se záložkami + Automaticky se připojovat ke skupinovým chatům, pokud jsou nastaveny v záložkách + OMEMO otisk zkopírován do schránky + Byl(a) jste blokován(a) v této skupině + Tento skupinový chat je pouze pro registrované členy + Omezení zdrojů + Byl(a) jste vyloučen(a) z této skupiny + Skupinový chat byl ukončen + Již nejste členem tohoto skupinového chatu za použití účtu %s + hostován na %s Ověřuji %s na HTTP hostiteli Bez připojení. Zkus znovu později Ověřit %s velikost Kontrola %1$s velikosti na %2$s Možnosti zpráv + Citovat + Vložit jako citaci Kopírovat originální URL Poslat znovu URL souboru + Adresa URL zkopírována do schránky + Adresa XMPP zkopírována do schránky + Chybové hlášení zkopírováno do schránky + webová adresa Skenovat 2D kód Zobrazit 2D kód Zobrazit seznam blokovaných Detaily účtu Potvrdit Zkusit znovu + Služba na popředí Zamezit operačnímu systému v ukončení připojení Vytvořit zálohu + Soubory zálohy budou uloženy do %s + Vytvářím soubory zálohy + Záloha byla vytvořena + Soubory zálohy byly uloženy do %s + Obnovuji zálohu + Záloha obnovena + Nezapomeňte povolit účet Vybrat soubor Přijímám %1$s (%2$d%% dokončeno) Stáhnout %s @@ -255,22 +343,37 @@ soubor Otevřít %s odesílám (%1$d%% přeneseno) + Připravuji sdílení souboru %s nabídnuto ke stažení Zrušit přenos + nebylo možné sdílet soubor + přenos souboru byl zrušen + Soubor byl smazán + Nebyla nalezena aplikace umožňující otevření souboru + Nebyla nalezena aplikace umožňující otevření odkazu + Nebyla nalezena aplikace umožňující zobrazení kontaktu + Dynamické tagy Zobrazit tagy pro čtení pod kontakty Povolit upozornění + Žádný server pro skupinový chat nebyl nalezen + Nebylo možné vytvořit skupinový chat Avatar účtu Zkopírovat OMEMO otisk do schránky Znovu vytvořit OMEMO klíč Smazat přístroje + 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. + 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? + Pro tento kontakt nejsou dostupné žádné klíče.\nUjistěte se, že oba máte zapnuté zasílání informací o změně stavu. + Něco se pokazilo Načíst historii ze serveru Na serveru není žádná další historie - Aktualizuji... + Aktualizuji… Heslo změněno! Nelze změnit heslo Změnit heslo Současné heslo Nové heslo + Heslo nesmí být prázdné Povolit všechny účty Vypnout všechny účty Provést akci s @@ -279,17 +382,35 @@ Vyloučený Člen Pokročilý mód + Udělit oprávnění člena + Odebrat oprávnění člena Povolit administrátorská oprávnění Odebrat administrátorská oprávnění + Udělit práva vlastníka + Odebrat práva vlastníka + Odebrat ze skupinového chatu + Odebrat z kanálu Nelze změnit připojení uživatele %s + Blokovat ve skupinovém chatu + Blokovat v kanálu + 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. Vypovědět Nelze změnit roli uživatele %s + Nastavení soukromých skupinových chatů + Nastavení veřejných kanálů Soukromé, pouze pro členy + Ukázat XMPP adresy všem + Nastavit kanál jako moderovaný Neúčastníte se + Nastavení skupinového chatu změněno! + Nebylo možné změnit nastavení skupinového chatu Nikdy Než opět změním + Odpovědět + Označit jako přečtené Vstup Enter odesílá + Odeslat klávesou Enter. Vždy můžete zprávy odeslat pomocí Ctrl+Enter, i když tato možnost není povolena. Zobrazit klávesu enter Změnit klávesu emotikon na klávesu enter audio @@ -302,12 +423,15 @@ Odesílám %s Nabízím %s Skrýt offline - %s píše... + %s píše… %s přestal(a) psát + %s píší… + %s přestali psát Upozornění při psaní Nechat kontaky vědět když jim píšete zprávu Poslat pozici Zobrazit pozici + Nebyla nalezena aplikace pro zobrazení pozice Pozice Conversation zavřena Nedůvěřovat systémovým CA @@ -324,6 +448,7 @@ %d certifikátů smazáno %d certifikátů smazáno + Nahradit tlačítko odeslání rychlou akcí Rychlá akce Žádná Naposledy použitá @@ -331,6 +456,7 @@ Prohledat kontakty Prohledat záložky Poslat soukromou zprávu + %1$s opustil(a) skupinový chat Uživatelské jméno Uživatelské jméno Toto není platné uživatelské jméno @@ -341,14 +467,25 @@ Tor síť není dostupná Bind chyba Rozbité + Dostupnost + Pryč při uzamčení zařízení + Při uzamčeném zařízení nastaví váš stav na \"pryč\" + Nedostupný při vypnutém zvuku + Při ztišeném vyzvánění označí váš stav jako \"nedostupný\" Vibrační mód brát stejně jako tichý + Při nastavení pouze na vibrace označí váš stav jako \"nedostupný\" Rozšířená nastavení připojení Zobrazovat nastavení hostname a port při vytváření účtu xmpp.server.cz + Přihlásit se pomocí certifikátu + Nelze analyzovat certifikát Nastavení archivace Nastavení archivace na serveru - Získávání nastavení archivace. Chvíli strpení... + Získávání nastavení archivace. Chvíli strpení… + Nelze získat nastavení archivace + Vyžadována CAPTCHA Zadejte text z obrázku výše + Adresa XMPP nesouhlasí s certifikátem Obnovit certifikát Chyba získání OMEMO klíče! OMEMO klíč ověřen certifikátem! @@ -368,23 +505,45 @@ %d zpráv Načíst více zpráv + Soubor sdílen s %s + Obrázek sdílen s %s + Obrázky sdíleny s %s + Text sdílen s %s + Povolit %1$s přístup k externímu úložišti + Povolit %1$s přístup ke kameře Synchronizovat s kontakty + %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. +
Tyto kontaktní údaje nebudeme kopírovat a ukládat.\n\nVíce informací najdete v našich zásadách pro ochranu osobních údajů.

Nyní budete požádáni o udělení přístupu k Vašim kontaktům.]]>
Upozorňovat na všechny zprávy + Upozornit pouze, když mě někdo zmíní Upozornění vypnuta Upozornění pozastavena + Komprese obrázků + Tip: Pokud použijete \"Vybrat soubor\" místo \"Vybrat obrázek\", můžete poslat nekomprimovaný obrázek bez ohledu na toto nastavení. Vždy + Pouze pro velké obrázky Povolena optimalizace využití baterie + 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. + 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í. Vypnout Vybraný obsah je příliš dlouhý (Žádné aktivované účty) Toto pole je vyžadováno Opravit zprávu Odeslat opravenou zprávu + Tento osobní otisk byl již bezpečně ověřen. Ťuknutím na \"Hotovo\" pouze potvrzujete, že %s je členem tohoto skupinového chatu. Tento účet byl vypnut - Sdílet URI s... + Nebyla nalezena aplikace umožňující sdílení URI + Sdílet URI s… +
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.

Přihlášením se do služby potvrzujete souhlas s našimi zásadami pro ochranu osobních údajů.]]>
+ Souhlasit a pokračovat + 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. + Vaše celá XMPP adresa: %s Vytvořit účet Použít vlastního provozovatele Zadejte své uživatelské jméno + Spravovat viditelnost ručně + Nastavit viditelnost při úpravě statusové zprávy Stavová zpráva Volný pro chat Online @@ -396,16 +555,23 @@ Registrace selhala: Zkuste znovu později Registrace selhala: Příliš slabé heslo Vybrat účastníky + Vytvářím skupinový chat… Pozvat znovu Vypnout Krátký Střední Dlouhý + Informovat o používání + Tato možnost dává vědět Vašim kontaktům, kdy používáte Conversations Soukromí Vzhled Vybrat paletu barev + Automaticky + Světlý vzhled + Tmavý vzhled Zelené pozadí Použít zelené pozadí pro přijaté zprávy + Nelze se spojit s OpenKeychain Tento přístoj již není používán Počítač Mobil @@ -413,38 +579,389 @@ Prohlížeč Konzole Vyžadována platba + Udělte povolení pro přístup na Internet Kontakt žádá informace o změnách stavu Povolit Chybí oprávnění přistupovat k %s Vzdálený server nebyl nalezen + Vypršel čas spojení se vzdáleným serverem + Nelze aktualizovat účet + Nahlásit tuto XMPP adresu kvůli odesílání spamu. Smazat OMEMO identity + Znovu vygenerovat OMEMO klíče. Vyžaduje potvrzení od všech vašich kontaktů. Použijte pouze jako poslední řešení. Smazat vybrané klíče Pro zveřejnění svého avatara musíte být online. Zobrazit chybovou zprávu Chybová zpráva Zapnuta úspora dat + 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. + Tento přístroj nepodporuje vypnutí úspory dat pro aplikaci %1$s. + Nebylo možné vytvořit dočasný soubor Tento přístroj byl ověřen Kopírovat identifikátor + Oveřil(a) jste všechny OMEMO klíče, které vlastníte. + Kód neobsahuje otisk pro tuto konverzaci. + Ověřené otisky + Naskenovat kód kontaktu pomocí fotoaparátu + Prosím, počkejte na získání klíčů Sdílet jako čárový kód Sdílet jako XMPP URI Sdílet jako HTTP odkaz + Slepě důvěřovat před ověřením + 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ů. + Nedůvěryhodný Neplatný 2D kód + Vyčistit složku dočasných souborů (užitých aplikací fotoaparátu) + Vyčistit dočasné soubory + Vyčistit soukromé úložiště + Vyčistit úložiště souborů (Mohou být znovu staženy ze serveru) + Tento odkaz pochází z důvěryhodného zdroje + 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. + Ověřit OMEMO klíče + Zobrazit neaktivní + Skrýt neaktivní + Odebrat z důvěryhodných + 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é\". + + %d vteřina + %d vteřiny + %d vteřin + %d vteřin + + + %d minuta + %d minut + %d minut + %d minut + + + %d hodina + %d hodiny + %d hodin + %d hodin + + + %d den + %d dny + %d dnů + %d dnů + + + %d týden + %d týdny + %d týdnů + %d týdnů + + + %d měsíc + %d měsíce + %d měsíců + %d měsíců + + Automatické mazání zpráv + Automaticky z tohoto zařízení mazat zprávy, které jsou starší, než je nastaveno. + Šifruji zprávu + Komprimuji video + Odpovídající konverzace uzavřena. + Kontakt zablokován. + Upozornění od neznámých + Upozornit na zprávy a hovory od neznámých kontaktů. + Přijata zpráva od neznámého kontaktu + Zablokovat neznámý kontakt + Zablokovat celou doménu + právě teď online + Zkusit znovu dešifrovat + Chyba sezení + Degradovaný SASL mechanismus + Server požaduje registraci přes webovou stránku + Otevřít webovou stránku + Nebyla nalezena aplikace umožňující otevření webové stránky + Heads-up upozornění + Zobrazit heads-up upozornění + Dnes + Včera + Ověřit název hostitele pomocí DNSSEC + Certifikáty serverů obsahující ověřený název hostitele jsou považovány za ověřené + Certifikát neobsahuje XMPP adresu + částečný + Nahrát video + Kopírovat do schránky Zpráva zkopírována do schránky + Zpráva + Soukromé zprávy jsou zakázány + Chráněné aplikace + Abyste mohli dostávat upozornění i při vypnuté obrazovce, musíte přidat Conversations mezi chráněné aplikace. + Přijmout neznámý certifikát? + Certifikát není podepsaný žádnou známou certifikační autoritou. + Přijmout nesouhlasící jméno serveru? + Server se nemohl prokázat jako \"%s\". Certifikát je platný pouze pro: + Chcete se přesto připojit? + Detaily certifikátu: + Jednou + Skener kódů QR potřebuje přístup k fotoaparátu + Posunout na konec + Posunout na konec po odeslání zprávy Upravit stavovou zprávu Upravit stavovou zprávu + Zakázat šifrování + %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í. + Nelze získat seznam zařízení + Nelze získat šifrovací klíče + Tip: V některých případech může být řešení vzájemné přidání kontaktů do seznamu kontaktů. + 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ů. + Vypnout hned + Koncept: + OMEMO šifrování + OMEMO bude vždy použito k šifrování zpráv v jednotlivých konverzacích i v soukromých skupinách. + OMEMO bude použito jako výchozí pro nové konverzace. + OMEMO bude nutné zapnout ručně pro každou každou novou konverzaci. + Vytvořit zástupce + Velikost písma + Relativní velikost písma v aplikaci + Zapnuto jako výchozí + Vypnuto jako výchozí + Malé Střední + Velké + Zpráva nebyla pro toto zařízení zašifrována. + Chyba při dešifrování OMEMO zprávy. + zpět + Sdílení polohy je vypnuto + Kopírovat pozici + Sdílet pozici + Pokyny + Sdílet pozici Zobrazit pozici + Sdílet + Nebylo možné zahájit nahrávání + Chvíli strpení… + Povolit %1$s přístup k mikrofonu Prohledat zprávy + GIF + Zobrazit konverzaci + Plugin pro sdílení pozice + Použít Plugin pro sdílení pozice namísto interní mapy + Kopírovat webovou adresu + Kopírovat XMPP adresu + Přímé vyhledávání + Na úvodní obrazovce otevřít klávesnici a umístit kurzor do vyhledávacího pole + Avatar skupinového chatu + Hostitel nepodporuje avatary pro skupinový chat + Pouze vlastník může změnit avatar skupinového chatu + Jméno kontaktu + Přezdívka + Jméno + Poskytnutí jména je nepovinné Jméno skupinového chatu + Tento skupinový chat byl zrušen + Nebylo možné uložit nahrávku + Služba na popředí + Tato kategorie upozornění zobrazuje stálou notifikaci, že aplikace %1$s je spuštěná. + Informace o stavu + Problémy s připojením + Tato kategorie upozornění zobrazuje notifikaci v případě problémů s připojením k účtu. + Zprávy + Hovory + Zprávy + Příchozí hovory + Probíhající hovory + Tiché zprávy + Kategorie upozornění, která nejsou doprovázena žádným zvukem. Například když jste aktivní na jiném zařízení (ochranná doba). + Neúspěšné přenosy + Nastavení upozornění na zprávy + Nastavení upozornění na příchozí hovory + Důležitost, Zvuk, Vibrace + Komprese videa + Zobrazit média + Účastníci + Prohlížeč médií + Soubor byl vynechán kvůli porušení bezpečnosti. + Kvalita videa + Nižší kvalita znamená menší soubory + Střední (360p) + Vysoká (720p) + zrušeno + Již máte rozepsaný koncept zprávy. + Funkce není implemetována + Neplatný kód země + Vyberte zemi + telefonní číslo + Ověřte své telefonní číslo + 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: +
%s

. Je číslo v pořádku, nebo ho chete upravit?]]>
+ %s není platné telefonní číslo. + Prosíme, zadejte své telefonní číslo. + Hledat zemi + Ověřit %s + %s.]]> + Poslali jsme Vám další SMS se 6místným kódem. + Prosím, vložte 6místný pin. + Poslat SMS znovu + Poslat SMS znovu (%s) + Chvíli strpení (%s) + zpět + Automaticky vložen pravděpodobný pin ze schránky. + Prosím, vložte svůj 6místný pin. + Opravdu si přejete přerušit registraci? + Ano + Ne + Ověřuji… + Pin, který jste zadali, je nesprávný. + Pin, který jsme Vám poslali, vypršel. + Neznámá chyba sítě. + Neznámá odpověď serveru. + Nebylo možné se připojit k serveru. + Nebylo možné navázat zabezpečené spojení. + Server nenalezen. + Něco se pokazilo při zpracovávání Vašeho požadavku. + Dočasně nedostupné. Zkuste to později. + Žádné připojení k síti. + Prosíme, zkuste to znovu za %s + Příliš mnoho pokusů + Používáte zastaralou verzi této aplikace. + Aktualizovat + Toto telefonní číslo je již přihlášeno z jiného zařízení. + Prosíme, vložte své jméno, aby ostatní, kteří Vás nemají v seznamu kontaktů, věděli, kdo jste. Vaše jméno + Vložte své jméno + Pro nastavení jména klepněte na Upravit. + Odmítnout žádost + Instalovat Orbot + Spustit Orbot + Není nainstalován žádný správce aplikací. + Tento kanál zveřejní Vaši XMPP adresu + Originální (nekomprimováno) + Otevřít pomocí… + Nastavit profilový obrázek + Vybrat účet + Obnovit ze zálohy + Obnovit + Pro obnovení ze zálohy zadejte heslo k účtu %s. + 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í. + Nebylo možné obnovit zálohu. + Nebylo možné dešifrovat zálohu. Zadal(a) jste správné heslo? + Záloha & Obnova + Zadejte XMPP adresu Vytvořit skupinový chat + Připojit se k veřejnému kanálu Vytvořit soukromý skupinový chat Vytvořit veřejný kanál Jméno kanálu Adresa XMPP + Prosím, zadejte název kanálu + Zadejte XMPP adresu + Toto je XMPP adresa. Prosím, zadejte jméno. + Vytváření veřejného kanálu… + Tento kanál již existuje + Připojil(a) jste se k existujícímu kanálu + Nebylo možné uložit nastavení kanálu + Povolit komukoli změnit téma + Povolit komukoli pozvat další účastníky + Kdokoli může změnit téma. + Vlastníci mohou měnit téma. + Správci mohou měnit téma. + Vlastníci mohou pozvat další účastníky. + Kdokoli může pozvat další účastníky. + Správci mohou vidět XMPP adresy. + Kdokoli může vidět XMPP adresy. + 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. + Tento soukromý skupinový chat nemá žádné účastníky. + Spravovat oprávnění + Hledat účastníky + Soubor je příliš velký + Přiložit Najít kanály + Prohledat kanály Možné porušení soukromí search.jabber.network.

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 Zásady ochrany osobních údajů.]]>
+ Již mám účet + Přidat existující účet + Vytvořit nový účet + Toto vypadá jako adresa domény + Přesto přidat + Toto vypadá jako adresa kanálu + Sdílet soubory zálohy + Záloha Conversations + Otevřít zálohu + Soubor, který jste zvolili, není soubor zálohy Conversations + Tento účet byl již nastaven + Prosím, zadejte heslo k tomuto účtu + Nebylo možné vykonat tuto akci + Připojit se k veřejnému kanálu… + Sdílející aplikace neudělila dostatečná oprávnění pro přístup k souboru. + + Místní server + 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. + Metoda objevování kanálů + Záloha + O + Prosíme, povolte účet + Volat + Příchozí hovor + Příchozí videohovor + Připojuji + Připojeno + Přijímám hovor + Ukončuji hovor + Přijmout + Odmítnout + Vyhledávám zařízení + Vyzvánění Zaneprázdněný - + Hovor nebylo možné spojit + Spojení ztraceno + Chyba aplikace + Zavěsit + Probíhající hovor + Probíhající videohovor + Zakázat hovory přes Tor + Příchozí hovor + Příchozí hovor · %s + Zmeškané volání · %s + Odchozí hovor + Odchozí hovor · %s + Zmeškané volání + Hovor + Videohovor + Nápověda + Přepnout na konverzaci + Váš mikrofon je nedostupný + V jednu chvíli může probíhat pouze jeden hovor. + Návrat k probíhajícímu hovoru + Nebylo možné přepnout kameru + Připnout nahoru + Odepnout shora + Nebylo možné opravit zprávu + Všechny konverzace + Tato konverzace + Váš avatar + Avatar uživatele %s + Šifrováno pomocí OMEMO + Šifrováno pomocí OpenPGP + Nešifrováno + Ukončit + Nahrát hlasovou zprávu + Přehrát audio + Pozastavit audio + Přidat kontakt, vytvořit nebo se připojit ke skupinovému chatu nebo vyhledat kanály + + Ukázat %1$d účastníka + Ukázat %1$d účastníky + Ukázat %1$d účastníků + Ukázat %1$d účastníků + + + Zpráva nemohla být doručena + Několik zpráv nemohlo být doručeny + Některé zprávy nemohly být doručeny + Některé zprávy nemohly být doručeny + + Neúspěšné přenosy + Více možností + Nenalezena žádná aplikace + Pozvat do Conversations + Server nepodporuje vytváření pozvánek + Žádný z aktivních účtů tuto funkci nepodporuje + Zálohování zahájeno. Budete upozorněni, jakmile bude záloha hotova. + diff --git a/src/main/res/values-da-rDK/strings.xml b/src/main/res/values-da-rDK/strings.xml index 73dc7022e..9bbe133b5 100644 --- a/src/main/res/values-da-rDK/strings.xml +++ b/src/main/res/values-da-rDK/strings.xml @@ -465,8 +465,8 @@ Serveren er ikke ansvarlig for dette domæne Brudt Tilgængelighed - Væk når enhed er låst - Vis som Væk når enheden er låst + Ude når enhed er låst + Vis som Ude når enheden er låst Optaget i lydløs tilstand Vis som Optaget når enhed er i lydløs tilstand Behandl vibration som lydløs tilstand @@ -486,7 +486,7 @@ XMPP-adresse matcher ikke certifikatet Forny certifikat Fejl ved hentning af OMEMO-nøgle! - Bekræftet OMEMO-nøgler med Certifikat! + Bekræftet OMEMO-nøgler med certifikat! Din enhed understøtter ikke valget af klientcertifikater! Forbindelse Forbind via TOR @@ -545,7 +545,7 @@ Statusbesked Gratis for Chat Online - Væk + Ude Ikke tilgængelig Optaget Der er genereret en sikker adgangskode @@ -959,4 +959,4 @@ Kunne ikke analysere invitation Server understøtter ikke generering af invitationer Ingen aktive konti understøtter denne funktion - + diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index 4c47948a2..eaf26c614 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -363,7 +363,7 @@ Lade Chatverlauf… Keine weiteren Nachrichten vorhanden Aktualisieren… - Passwort geändert. + Passwort geändert! Passwort konnte nicht geändert werden Passwort ändern Aktuelles Passwort @@ -959,4 +959,5 @@ Einladung kann nicht gelesen werden Server unterstützt keine Generierung von Einladungen Keine aktiven Konten unterstützen diese Funktion + Das Backup wurde gestartet. Du bekommst eine Benachrichtigung sobald es fertig ist. diff --git a/src/main/res/values-el/strings.xml b/src/main/res/values-el/strings.xml index 388b9a646..121304792 100644 --- a/src/main/res/values-el/strings.xml +++ b/src/main/res/values-el/strings.xml @@ -956,4 +956,4 @@ Αδυναμία ανάγνωσης πρόσκλησης Ο διακομιστής δεν υποστηρίζει την δημιουργία προσκλήσεων Κανένας από τους ενεργούς λογαριασμούς δεν υποστηρίζει αυτό το χαρακτηριστικό - + diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index ffff341d0..a8a304843 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -954,4 +954,4 @@ No se ha podido leer la invitación El servidor no soporta la creación de invitaciones Ninguna cuenta activa soporta esta característica - + diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index 40f6ae6cd..50b54d900 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -959,4 +959,4 @@ Non se puido enviar o convite O servidor non soporta a creación de convites Ningunha conta activa soporta esta función - + diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml index 2d9955009..e0f5d5125 100644 --- a/src/main/res/values-it/strings.xml +++ b/src/main/res/values-it/strings.xml @@ -959,4 +959,4 @@ Impossibile analizzare l\'invito Il server non supporta la generazione di inviti Nessun account attivo supporta questa funzione - + diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index 2285c60fb..7a7ba0b50 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -986,4 +986,5 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Nie można przetworzyć zaproszenia Serwer nie wspiera tworzenia zaproszeń Nie ma aktywnych kont wspierających tę funkcję + Tworzenie kopii zapasowej się rozpoczęło. Dostaniesz powiadomienie kiedy się zakończy. diff --git a/src/main/res/values-pt-rBR/strings.xml b/src/main/res/values-pt-rBR/strings.xml index f1430f4a5..6c71b6bbb 100644 --- a/src/main/res/values-pt-rBR/strings.xml +++ b/src/main/res/values-pt-rBR/strings.xml @@ -959,4 +959,4 @@ Não foi possível processar o convite O servidor não suporta a criação de convites Nenhuma conta ativa suporta esse recurso - + diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index 694e0d5cf..5b2f051be 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -972,4 +972,5 @@ Nu s-a putut procesa invitația Serverul nu suportă generarea de invitații Nici un cont activ nu suporta această caracteristică + Se creează copia de siguranță. Veți primi o notificare când acesta este completă. diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml index 2ff631f71..629c45ac7 100644 --- a/src/main/res/values-ru/strings.xml +++ b/src/main/res/values-ru/strings.xml @@ -985,4 +985,4 @@ Невозможно разобрать приглашение Сервер не поддерживает создание приглашений Ни один активный аккаунт не поддерживает эту функцию - + diff --git a/src/main/res/values-tr-rTR/strings.xml b/src/main/res/values-tr-rTR/strings.xml index 3ca8404af..3460ee293 100644 --- a/src/main/res/values-tr-rTR/strings.xml +++ b/src/main/res/values-tr-rTR/strings.xml @@ -124,6 +124,7 @@ Zil sesi Bildirim sesi Yeni mesajlar için bildirim sesi + Gelen çağrılar için zil sesi Mühlet Cihazlarınızın birinde faaliyet tespit edilmesinden sonra zaman hatırlatmalarının susturulma uzunluğu. Gelişmiş @@ -464,6 +465,8 @@ Sunucu bu alan adı için sorumlu değil Bozuk Mevcudiyet + Telefon kilitliyken uzakta + Telefon kilitliyken Uzakta göster Müsait değilken sessiz kipte olur Telefonunuz sessizdeyken, durum bildiriminizi müsait değil olarak gösterir. Titreşim kipini sessiz kip olarak değerlendir @@ -956,4 +959,4 @@ Davet iletilemedi Sunucu, davet oluşturulmasını desteklemiyor Bu özelliği destekleyen aktif bir hesap yok - + diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index b45877393..764daf0ea 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -946,4 +946,5 @@ 无法解析邀请 服务器不支持生成邀请 没有活跃帐户支持此功能 + 已启动备份。一旦完成,你会收到通知。 diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index cdc616f64..6b5774639 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -148,6 +148,7 @@ File not found General I/O error. Maybe you ran out of storage space? The app you used to select this image did not provide enough permissions to read the file.\n\nUse a different file manager to choose an image. + The app you used to share this file did not provide enough permissions. Unknown Temporarily disabled Online @@ -959,4 +960,6 @@ Unable to parse invite Server does not support generating invites No active accounts support this feature + The backup has been started. You’ll get a notification once it has been completed. + Unable to enable video.