Compare commits

...

52 Commits

Author SHA1 Message Date
ChaosKid42 af898b3bc4 LMC: find replacedMessages based on bare JID (#3548) 2019-10-05 20:18:42 +00:00
Daniel Gultsch 6155938623 show reason in error message 2019-10-05 22:14:38 +02:00
Daniel Gultsch 01fc78d2a5 disable context menu on failed jingle files. fixes #3556 2019-10-05 22:05:03 +02:00
Daniel Gultsch d6835101b9 fixes for previous commit 2019-10-05 21:58:21 +02:00
Daniel Gultsch 7ce7a505a0 mark cancelled jingle ft as such on both sides
fixes #3554
2019-10-05 21:30:15 +02:00
Daniel Gultsch 6776603b90 fully read port in socks connection
incoming direct connections in receive mode wouldn’t clear the entire
destination from the input stream; thus adding a leading 0x00 to the file

fixes #3557
2019-10-03 20:47:31 +02:00
Daniel Gultsch 3d332f6f35 set shorter timeouts when using direct candidates 2019-10-03 09:37:01 +02:00
Marcin Mielniczuk c7bbfaf061 Use a more meaningful name for old status (#3552) 2019-10-01 19:42:05 +00:00
Wiktor Kwapisiewicz 7c631c493a Use dark navigation bar in QR scanner activity (#3551)
* Use dark navigation bar on dark theme

This approach uses `tools:targetApi` instead of separate theme file and
avoids lint errors.

* Use dark navigation bar in QR scanner activity

This is consistent with the black background that is already used in
that activity.
2019-10-01 14:44:53 +00:00
Daniel Gultsch 322352ccbf use new jabber.search.network endpoint 2019-10-01 12:42:12 +02:00
Daniel Gultsch 37b87e18ee fix NPE when using channel search and DOMAIN_LOCK closes #3458 2019-10-01 12:24:57 +02:00
Daniel Gultsch f8bd4284a5 report not-acceptable on jingle errors 2019-10-01 11:31:15 +02:00
Daniel Gultsch d2d9bbe3da improved jingle debugging 2019-09-29 13:32:45 +02:00
Daniel Gultsch aef394c9f0 ability to open files from media preview. fixes #3521 2019-09-29 02:00:06 +02:00
Daniel Gultsch 618d892ae7 account deletion: only attempt to delete omemo id when connected 2019-09-29 01:40:40 +02:00
Daniel Gultsch 7b160a358e do not add 'quote' when sharing own msgs 2019-09-28 23:56:29 +02:00
Daniel Gultsch 863ac7f2e5 show resource prompt when sending uncompressed video 2019-09-28 23:56:02 +02:00
Daniel Gultsch 9276eff1db delete omemo keys when deleting account 2019-09-28 21:52:07 +02:00
Daniel Gultsch 175d9f539e version bump to 2.5.11 + changelog 2019-09-28 11:04:08 +02:00
Daniel Gultsch 705f31518c pulled translations from transifex 2019-09-28 10:37:21 +02:00
Daniel Gultsch a2d521568d demote okhttp to 3.12 to work with old android 2019-09-28 10:35:36 +02:00
Daniel Gultsch 0fc41d8c82 version bump to 2.5.10 + changelog 2019-09-27 20:29:22 +02:00
Daniel Gultsch a3227caa37 pulled translations from transifex 2019-09-27 16:30:33 +02:00
Daniel Gultsch 11736ce48c make list selection manager work with app compat 2019-09-26 23:47:55 +02:00
Daniel Gultsch f9e1e856d2
Merge pull request #3546 from marcellippmann/master
Correct a small typo in Romanian translation
2019-09-25 19:19:33 +00:00
Daniel Gultsch 4dd4886758 pulled translations from transifex 2019-09-25 21:19:09 +02:00
Daniel Gultsch c749aa1cae bumped okhttp 2019-09-25 21:12:12 +02:00
Marcel Lippmann e2886098fe Correct a small typo in Romanian translation 2019-09-25 18:15:36 +02:00
Daniel Gultsch 6c5b71440f channel search result long press to show join dialog 2019-09-24 17:29:34 +02:00
Daniel Gultsch 92e27a3649 pulled translations from transifex 2019-09-24 12:12:20 +02:00
Daniel Gultsch 3733148ac8 version bump to 2.5.9 + changelog 2019-09-24 12:11:51 +02:00
Daniel Gultsch bea94d565b use http link for join 2019-09-22 11:45:10 +02:00
Daniel Gultsch 98384314cd clarify transifex procedure 2019-09-22 11:43:44 +02:00
Daniel Gultsch 75d35c357e print emoji only status messages larger 2019-09-22 11:39:34 +02:00
Daniel Gultsch 426090c301 do not parse invites from type=groupchat 2019-09-22 10:00:09 +02:00
Daniel Gultsch d2ef0728a3 pulled translations from transifex 2019-09-21 10:52:34 +02:00
Daniel Gultsch 3820950408 show context menu in channel search to share uri 2019-09-21 10:51:05 +02:00
Daniel Gultsch ed46d12115 close correct socket after faulty jingle socks connection 2019-09-20 10:00:57 +02:00
Daniel Gultsch 5e1d2a92a6 uploaded doap file to new syntax 2019-09-19 15:33:21 +02:00
Daniel Gultsch d0d87cb8a6 added doap file 2019-09-19 15:19:09 +02:00
Daniel Gultsch abe01f18f2 improved logging for messages waiting for join 2019-09-19 10:00:50 +02:00
Daniel Gultsch af74c3604c fixed R8 weirdness 2019-09-18 11:04:36 +02:00
Daniel Gultsch 02351dc0fb fixed direct invites after adhoc 2019-09-18 09:55:18 +02:00
Daniel Gultsch 4c92d1b755 cancel spinning wheel on muclumbus error 2019-09-18 09:17:47 +02:00
Daniel Gultsch 2ec1d0cc09 warn when using _only_ ambiguous cyrillic 2019-09-16 15:13:53 +02:00
Daniel Gultsch d963d95e30 fixed some minor NPE 2019-09-16 14:20:15 +02:00
Daniel Gultsch ab57c59838 clear notifications when deleting account 2019-09-15 12:23:56 +02:00
Daniel Gultsch 4cd652884c do not finish or repair sessions for untrusted senders
finishing (sending a key transport message in response to pre key message) as
well as reparing sessions will leak resource and availability and might in
certain situations in group chat leak the Jabber ID.

Therefor we disable that. Leaking resource might not be considered harmful by
a lot of people however we have always doing similar things with receipts.
2019-09-15 11:49:58 +02:00
Daniel Gultsch be4953b1e4 parse LMC 1.1 2019-09-13 16:38:15 +02:00
Daniel Gultsch e395da18bf when parsing omemo messages ensure we only find one element 2019-09-12 12:43:11 +02:00
Daniel Gultsch a7c47a33fa inherit language from parent message when finding localized body 2019-09-12 12:42:42 +02:00
Daniel Gultsch 9bf5fb98ac show language in message bubble if multiple language variants were received
XML and by inheritence XMPP has the feature of transmitting multiple language
variants for the same content. This can be really useful if, for example, you
are talking to an automated system. A chat bot could greet you in your own
language.

On the wire this will usually look like this:

```xml
<message to="you">
  <body>Good morning</body>
  <body xml:lang="de">Guten Morgen</body>
</message>
```

However receiving such a message in a group chat can be very confusing and
potentially dangerous if the sender puts conflicting information in there and
different people get shown different strings.

Disabeling support for localization entirely isn’t an ideal solution as on
principle it is still a good feature; and other clients might still show a
localization even if Conversations would always show the default language.

So instead Conversations now shows the displayed language in a corner of the
message bubble if more than one translation has been received.

If multiple languages are received Conversations will attempt to find one in
the language the operating system is set to. If no such translation can be
found it will attempt to display the English string.

If English can not be found either (for example a message that only has ru and
fr on a phone that is set to de) it will display what ever language came first.

Furthermore Conversations will discard (not show at all) messages with with
multiple bodies of the same language. (This is considered an invalid message)

The lanuage tag will not be shown if Conversations received a single body in
a language not understood by the user. (For example operating system set to
'de' and message received with one body in 'ru' will just display that body as
usual.)

As a guide line to the user: If you are reading a message where it is important
that this message is not interpreted differently by different people (like a
vote (+1 / -1) in a chat room) make sure it has *no* language tag.
2019-09-12 10:12:51 +02:00
62 changed files with 1822 additions and 736 deletions

View File

@ -1,6 +1,16 @@
# Changelog
## Version 2.5.8
### Version 2.5.11
* Fixed crash on Android <5.0
### Version 2.5.10
* Fixed crash on Xiaomi devices running Android 8.0 + 8.1
### Version 2.5.9
* fixed minor security issues
* Share XMPP uri from channel search by long pressing a result
### Version 2.5.8
* fixed connection issues over Tor
* P2P file transfer (Jingle) now offers direct candidates
* Support XEP-0396: Jingle Encrypted Transports - OMEMO

View File

@ -285,7 +285,11 @@ Conversations is trying to get rid of old behaviours and set an example for
other clients.
#### Translations
Translations are managed on [Transifex](https://www.transifex.com/projects/p/conversations/)
Translations are managed on [Transifex](https://www.transifex.com/projects/p/conversations/).
If you want to become a translator Please register on transifex, apply to join
the translation team and then step by our group chat on
[conversations@conference.siacs.eu](https://conversations.im/j/conversations@conference.siacs.eu)
and introduce yourself to `iNPUTmice` so he can approve your join request.
#### How do I backup / move Conversations to a new device?
On the one hand Conversations supports Message Archive Management to keep a server side history of your messages so when migrating to a new device that device can display your entire history. However that does not work if you enable OMEMO due to its forward secrecy. (Read [The State of Mobile XMPP in 2016](https://gultsch.de/xmpp_2016.html) especially the section on encryption.)

View File

@ -63,13 +63,14 @@ dependencies {
implementation project(':libs:xmpp-addr')
implementation 'org.osmdroid:osmdroid-android:6.1.0'
implementation 'org.hsluv:hsluv:0.2'
implementation 'org.conscrypt:conscrypt-android:2.1.0'
implementation 'org.conscrypt:conscrypt-android:2.2.1'
implementation 'me.drakeet.support:toastcompat:1.1.0'
implementation "com.leinardi.android:speed-dial:2.0.1"
implementation 'com.squareup.retrofit2:retrofit:2.6.0'
implementation 'com.squareup.retrofit2:converter-gson:2.6.0'
implementation 'com.squareup.retrofit2:retrofit:2.6.1'
implementation 'com.squareup.retrofit2:converter-gson:2.6.1'
implementation 'com.squareup.okhttp3:okhttp:3.12.5'
implementation 'com.google.guava:guava:27.1-android'
quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.10.1'
quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.10.16'
}
ext {
@ -83,8 +84,8 @@ android {
defaultConfig {
minSdkVersion 16
targetSdkVersion 28
versionCode 338
versionName "2.5.8"
versionCode 341
versionName "2.5.11"
archivesBaseName += "-$versionName"
applicationId "eu.siacs.conversations"
resValue "string", "applicationId", applicationId

340
doap.rdf Normal file
View File

@ -0,0 +1,340 @@
<?xml version="1.0"?>
<?xml-stylesheet href="../style.xsl" type="text/xsl"?>
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<Project xmlns="http://usefulinc.com/ns/doap#" xmlns:foaf="http://xmlns.com/foaf/0.1/" xmlns:xmpp="https://linkmauve.fr/ns/xmpp-doap#">
<name>Conversations</name>
<created>2014-01-14</created>
<shortdesc xml:lang="en">Android XMPP Client</shortdesc>
<description xml:lang="en">Conversations is an open source XMPP/Jabber client for the Android platform</description>
<homepage rdf:resource="https://conversations.im/"/>
<download-page rdf:resource="https://play.google.com/store/apps/details?id=eu.siacs.conversations"/>
<bug-database rdf:resource="https://github.com/siacs/Conversations/issues"/>
<!-- See https://github.com/ewilderj/doap/issues/53 -->
<developer-forum rdf:resource="xmpp:conversations@siacs.conference.eu?join"/>
<support-forum rdf:resource="xmpp:conversations@siacs.conference.eu?join"/>
<license rdf:resource="https://github.com/siacs/Conversations/blob/master/LICENSE"/>
<!-- See https://github.com/ewilderj/doap/issues/49 -->
<language>en</language>
<logo rdf:resource="https://raw.githubusercontent.com/siacs/Conversations/master/doap.rdf"/>
<programming-language>Java</programming-language>
<os>Android</os>
<!-- TODO: Categories are URIs, find a better location for them. -->
<category rdf:resource="https://linkmauve.fr/ns/xmpp-doap#category-xmpp"/>
<category rdf:resource="https://linkmauve.fr/ns/xmpp-doap#category-jabber"/>
<category rdf:resource="https://linkmauve.fr/ns/xmpp-doap#category-client"/>
<maintainer>
<foaf:Person>
<foaf:name>Daniel Gultsch</foaf:name>
<foaf:homepage rdf:resource="https://gultsch.de/"/>
</foaf:Person>
</maintainer>
<repository>
<GitRepository>
<browse rdf:resource="https://github.com/siacs/Conversations"/>
<location rdf:resource="https://github.com/siacs/Conversations.git"/>
</GitRepository>
</repository>
<implements rdf:resource="https://xmpp.org/rfcs/rfc6120.html"/>
<implements rdf:resource="https://xmpp.org/rfcs/rfc6121.html"/>
<implements rdf:resource="https://xmpp.org/rfcs/rfc6122.html"/>
<implements rdf:resource="https://xmpp.org/rfcs/rfc7590.html"/>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0027.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.4</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0030.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>2.5rc3</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0045.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.32.0</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0048.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.1</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0084.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.1.3</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0085.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>2.1</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0092.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.1</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0115.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.5.1</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0163.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.2.1</xmpp:version>
<xmpp:note>Avatar, Nick, OMEMO</xmpp:note>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0166.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.1.2</xmpp:version>
<xmpp:note>File transfer only</xmpp:note>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0172.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.1</xmpp:version>
<xmpp:note>read only</xmpp:note>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0184.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.4.0</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0191.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.3</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0198.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.6</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0199.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>2.0.1</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0234.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.19.1</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0237.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.3</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0245.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.0</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0249.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.2</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0260.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.0.3</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0261.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.0</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0280.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.13.1</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0308.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.0</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0313.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.6.3</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0319.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.0.2</xmpp:version>
<xmpp:note>opt-in</xmpp:note>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0333.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.3</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0352.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.3.0</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0357.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.4.0</xmpp:version>
<xmpp:note>Only available in the version distributed over Google Play</xmpp:note>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0368.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.1.0</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0377.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.2</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0384.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.3.0</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0391.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.1.2</xmpp:version>
<xmpp:since>2.5.8</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0392.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.6.0</xmpp:version>
<xmpp:since>2.3.1</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0393.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.1.4</xmpp:version>
<xmpp:since>1.22.0</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0396.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.1</xmpp:version>
<xmpp:since>2.5.8</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0398.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.2.1</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0410.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.0.1</xmpp:version>
<xmpp:since>2.5.4</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0411.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.2.0</xmpp:version>
</xmpp:SupportedXep>
</implements>
<release>
<Version>
<revision>2.5.8</revision>
<created>2019-09-12</created>
<file-release rdf:resource="https://github.com/siacs/Conversations/archive/2.5.8.tar.gz"/>
</Version>
</release>
</Project>
</rdf:RDF>

3
proguard-rules.pro vendored
View File

@ -21,6 +21,9 @@
-dontwarn java.lang.**
-dontwarn javax.lang.**
-keepclassmembers class eu.siacs.conversations.http.services.** {
!transient <fields>;
}
# Retrofit does reflection on generic parameters. InnerClasses is required to use Signature and
# EnclosingMethod is required to use InnerClasses.

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="pick_a_server">اختر مزود خدمة XMPP الخاص بك</string>
<string name="use_conversations.im">استخدِم conversations.im</string>
<string name="create_new_account">أنشئ حسابًا جديدًا</string>
</resources>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="use_conversations.im">Använd conversations.im</string>
<string name="create_new_account">Skapa nytt konto</string>
</resources>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="pick_a_server">选择您的XMPP提供者</string>
<string name="use_conversations.im">使用 conversations.im</string>
<string name="create_new_account">创建新账户</string>
<string name="do_you_have_an_account">您已经拥有一个XMPP账户了吗如果您之前使用过其他的XMPP客户端的话那么您已经拥有这种账户了。如果没有账户的话您可以现在创建一个。\n提示有些电子邮件服务也提供XMPP账户。</string>
</resources>

View File

@ -41,7 +41,7 @@ public final class Config {
public static final String MAGIC_CREATE_DOMAIN = "conversations.im";
public static final String QUICKSY_DOMAIN = "quicksy.im";
public static final String CHANNEL_DISCOVERY = "https://search.jabbercat.org";
public static final String CHANNEL_DISCOVERY = "https://search.jabber.network";
public static final boolean DISALLOW_REGISTRATION_IN_UI = false; //hide the register checkbox
@ -116,6 +116,8 @@ public final class Config {
public static final boolean IGNORE_ID_REWRITE_IN_MUC = true;
public static final boolean MUC_LEAVE_BEFORE_JOIN = true;
public static final boolean USE_LMC_VERSION_1_1 = false;
public static final long MAM_MAX_CATCHUP = MILLISECONDS_IN_DAY * 5;
public static final int MAM_MAX_MESSAGES = 750;

View File

@ -48,7 +48,6 @@ import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded;
import eu.siacs.conversations.xmpp.OnIqPacketReceived;
import eu.siacs.conversations.xmpp.pep.PublishOptions;
@ -67,8 +66,8 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
public static final String LOGPREFIX = "AxolotlService";
public static final int NUM_KEYS_TO_PUBLISH = 100;
public static final int publishTriesThreshold = 3;
private static final int NUM_KEYS_TO_PUBLISH = 100;
private static final int publishTriesThreshold = 3;
private final Account account;
private final XmppConnectionService mXmppConnectionService;
@ -840,6 +839,13 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
});
}
public void deleteOmemoIdentity() {
final String node = AxolotlService.PEP_BUNDLES + ":" + getOwnDeviceId();
final IqPacket deleteBundleNode = mXmppConnectionService.getIqGenerator().deleteNode(node);
mXmppConnectionService.sendIqPacket(account, deleteBundleNode, null);
publishDeviceIdsAndRefineAccessModel(getOwnDeviceIds());
}
public List<Jid> getCryptoTargets(Conversation conversation) {
final List<Jid> jids;
if (conversation.getMode() == Conversation.MODE_SINGLE) {
@ -1469,7 +1475,9 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
} else {
Log.d(Config.LOGTAG,account.getJid().asBareJid()+": nothing to flush. Not republishing key");
}
completeSession(session);
if (trustedOrPreviouslyResponded(session)) {
completeSession(session);
}
}
}
@ -1479,23 +1487,44 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
publishBundlesIfNeeded(false, false);
}
}
Iterator<XmppAxolotlSession> iterator = postponedSessions.iterator();
final Iterator<XmppAxolotlSession> iterator = postponedSessions.iterator();
while (iterator.hasNext()) {
completeSession(iterator.next());
final XmppAxolotlSession session = iterator.next();
if (trustedOrPreviouslyResponded(session)) {
completeSession(session);
}
iterator.remove();
}
Iterator<SignalProtocolAddress> postponedHealingAttemptsIterator = postponedHealing.iterator();
final Iterator<SignalProtocolAddress> postponedHealingAttemptsIterator = postponedHealing.iterator();
while (postponedHealingAttemptsIterator.hasNext()) {
notifyRequiresHealing(postponedHealingAttemptsIterator.next());
postponedHealingAttemptsIterator.remove();
}
}
private boolean trustedOrPreviouslyResponded(XmppAxolotlSession session) {
try {
return trustedOrPreviouslyResponded(Jid.of(session.getRemoteAddress().getName()));
} catch (IllegalArgumentException e) {
return false;
}
}
public boolean trustedOrPreviouslyResponded(Jid jid) {
final Contact contact = account.getRoster().getContact(jid);
if (contact.showInRoster() || contact.isSelf()) {
return true;
}
final Conversation conversation = mXmppConnectionService.find(account, jid);
return conversation != null && conversation.sentMessagesCount() > 0;
}
private void completeSession(XmppAxolotlSession session) {
final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId());
axolotlMessage.addDevice(session, true);
try {
Jid jid = Jid.of(session.getRemoteAddress().getName());
final Jid jid = Jid.of(session.getRemoteAddress().getName());
MessagePacket packet = mXmppConnectionService.getMessageGenerator().generateKeyTransportMessage(jid, axolotlMessage);
mXmppConnectionService.sendMessagePacket(account, packet);
} catch (IllegalArgumentException e) {
@ -1505,9 +1534,8 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
public XmppAxolotlMessage.XmppAxolotlKeyTransportMessage processReceivingKeyTransportMessage(XmppAxolotlMessage message, final boolean postponePreKeyMessageHandling) {
XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage;
XmppAxolotlSession session = getReceivingSession(message);
final XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage;
final XmppAxolotlSession session = getReceivingSession(message);
try {
keyTransportMessage = message.getParameters(session, getOwnDeviceId());
Integer preKeyId = session.getPreKeyIdAndReset();
@ -1516,7 +1544,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
}
} catch (CryptoFailedException e) {
Log.d(Config.LOGTAG, "could not decrypt keyTransport message " + e.getMessage());
keyTransportMessage = null;
return null;
}
if (session.isFresh() && keyTransportMessage != null) {
@ -1527,7 +1555,6 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
}
private void putFreshSession(XmppAxolotlSession session) {
Log.d(Config.LOGTAG, "put fresh session");
sessions.put(session);
if (Config.X509_VERIFICATION) {
if (session.getIdentityKey() != null) {

View File

@ -135,7 +135,7 @@ public class XmppAxolotlMessage {
break;
}
}
Element payloadElement = axolotlMessage.findChild(PAYLOAD);
final Element payloadElement = axolotlMessage.findChildEnsureSingle(PAYLOAD, AxolotlService.PEP_PREFIX);
if (payloadElement != null) {
ciphertext = Base64.decode(payloadElement.getContent().trim(), Base64.DEFAULT);
}

View File

@ -2,7 +2,6 @@ package eu.siacs.conversations.entities;
import android.content.ContentValues;
import android.database.Cursor;
import android.graphics.Color;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
@ -13,23 +12,19 @@ import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicBoolean;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.OmemoSetting;
import eu.siacs.conversations.crypto.PgpDecryptionService;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.persistance.DatabaseBackend;
import eu.siacs.conversations.services.AvatarService;
import eu.siacs.conversations.services.QuickConversationsService;
import eu.siacs.conversations.utils.JidHelper;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xmpp.InvalidJid;
import eu.siacs.conversations.xmpp.chatstate.ChatState;
import eu.siacs.conversations.xmpp.mam.MamReference;
import rocks.xmpp.addr.Jid;
@ -311,11 +306,14 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
public Message findMessageWithRemoteIdAndCounterpart(String id, Jid counterpart, boolean received, boolean carbon) {
synchronized (this.messages) {
for (int i = this.messages.size() - 1; i >= 0; --i) {
Message message = messages.get(i);
if (counterpart.equals(message.getCounterpart())
&& ((message.getStatus() == Message.STATUS_RECEIVED) == received)
final Message message = messages.get(i);
final boolean counterpartMatch = mode == MODE_SINGLE ?
counterpart.asBareJid().equals(message.getCounterpart().asBareJid()) :
counterpart.equals(message.getCounterpart());
if (counterpartMatch && ((message.getStatus() == Message.STATUS_RECEIVED) == received)
&& (carbon == message.isCarbon() || received)) {
if (id.equals(message.getRemoteMsgId()) && !message.isFileOrImage() && !message.treatAsDownloadable()) {
final boolean idMatch = id.equals(message.getRemoteMsgId()) || message.remoteMsgIdMatchInEdit(id);
if (idMatch && !message.isFileOrImage() && !message.treatAsDownloadable()) {
return message;
} else {
return null;

View File

@ -43,8 +43,8 @@ public class IndividualMessage extends Message {
super(conversation);
}
private IndividualMessage(Conversational conversation, String uuid, String conversationUUid, Jid counterpart, Jid trueCounterpart, String body, long timeSent, int encryption, int status, int type, boolean carbon, String remoteMsgId, String relativeFilePath, String serverMsgId, String fingerprint, boolean read, String edited, boolean oob, String errorMessage, Set<ReadByMarker> readByMarkers, boolean markable, boolean deleted) {
super(conversation, uuid, conversationUUid, counterpart, trueCounterpart, body, timeSent, encryption, status, type, carbon, remoteMsgId, relativeFilePath, serverMsgId, fingerprint, read, edited, oob, errorMessage, readByMarkers, markable, deleted);
private IndividualMessage(Conversational conversation, String uuid, String conversationUUid, Jid counterpart, Jid trueCounterpart, String body, long timeSent, int encryption, int status, int type, boolean carbon, String remoteMsgId, String relativeFilePath, String serverMsgId, String fingerprint, boolean read, String edited, boolean oob, String errorMessage, Set<ReadByMarker> readByMarkers, boolean markable, boolean deleted, String bodyLanguage) {
super(conversation, uuid, conversationUUid, counterpart, trueCounterpart, body, timeSent, encryption, status, type, carbon, remoteMsgId, relativeFilePath, serverMsgId, fingerprint, read, edited, oob, errorMessage, readByMarkers, markable, deleted, bodyLanguage);
}
@Override
@ -116,6 +116,8 @@ public class IndividualMessage extends Message {
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.getInt(cursor.getColumnIndex(DELETED)) > 0,
cursor.getString(cursor.getColumnIndex(BODY_LANGUAGE))
);
}
}

View File

@ -62,6 +62,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
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";
@ -100,6 +101,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
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;
@ -145,7 +147,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
null,
null,
false,
false);
false,
null);
}
protected Message(final Conversational conversation, final String uuid, final String conversationUUid, final Jid counterpart,
@ -154,7 +157,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
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<ReadByMarker> readByMarkers,
final boolean markable, final boolean deleted) {
final boolean markable, final boolean deleted, final String bodyLanguage) {
this.conversation = conversation;
this.uuid = uuid;
this.conversationUuid = conversationUUid;
@ -177,6 +180,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
this.readByMarkers = readByMarkers == null ? new HashSet<>() : readByMarkers;
this.markable = markable;
this.deleted = deleted;
this.bodyLanguage = bodyLanguage;
}
public static Message fromCursor(Cursor cursor, Conversation conversation) {
@ -201,7 +205,9 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
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.getInt(cursor.getColumnIndex(DELETED)) > 0,
cursor.getString(cursor.getColumnIndex(BODY_LANGUAGE))
);
}
private static Jid fromString(String value) {
@ -266,6 +272,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
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;
}
@ -430,6 +437,23 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
this.edits.add(new Edited(edited, serverMsgId));
}
public boolean remoteMsgIdMatchInEdit(String id) {
for(Edited 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;
}
@ -717,6 +741,14 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
}
}
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;
}

View File

@ -15,6 +15,7 @@ public interface Transferable {
int STATUS_DOWNLOADING = 0x204;
int STATUS_OFFER_CHECK_FILESIZE = 0x206;
int STATUS_UPLOADING = 0x207;
int STATUS_CANCELLED = 0x208;
boolean start();

View File

@ -58,7 +58,7 @@ public class MessageGenerator extends AbstractGenerator {
packet.setId(message.getUuid());
packet.addChild("origin-id", Namespace.STANZA_IDS).setAttribute("id", message.getUuid());
if (message.edited()) {
packet.addChild("replace", "urn:xmpp:message-correct:0").setAttribute("id", message.getEditedId());
packet.addChild("replace", "urn:xmpp:message-correct:0").setAttribute("id", message.getEditedIdWireFormat());
}
return packet;
}
@ -193,6 +193,10 @@ public class MessageGenerator extends AbstractGenerator {
if (password != null) {
x.setAttribute("password", password);
}
if (contact.isFullJid()) {
packet.addChild("no-store", "urn:xmpp:hints");
packet.addChild("no-copy", "urn:xmpp:hints");
}
return packet;
}

View File

@ -4,7 +4,6 @@ import com.google.common.base.Objects;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import eu.siacs.conversations.services.AvatarService;
@ -83,7 +82,7 @@ public interface MuclumbusService {
class SearchRequest {
public Set<String> keywords;
public final Set<String> keywords;
public SearchRequest(String keyword) {
this.keywords = Collections.singleton(keyword);

View File

@ -33,6 +33,7 @@ import eu.siacs.conversations.services.MessageArchiveService;
import eu.siacs.conversations.services.QuickConversationsService;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.xml.LocalizedContent;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.InvalidJid;
@ -124,8 +125,13 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
plaintextMessage = service.processReceivingPayloadMessage(xmppAxolotlMessage, postpone);
} catch (BrokenSessionException e) {
if (checkedForDuplicates) {
service.reportBrokenSessionException(e, postpone);
return new Message(conversation, "", Message.ENCRYPTION_AXOLOTL_FAILED, status);
if (service.trustedOrPreviouslyResponded(from.asBareJid())) {
service.reportBrokenSessionException(e, postpone);
return new Message(conversation, "", Message.ENCRYPTION_AXOLOTL_FAILED, status);
} else {
Log.d(Config.LOGTAG, "ignoring broken session exception because contact was not trusted");
return new Message(conversation, "", Message.ENCRYPTION_AXOLOTL_FAILED, status);
}
} else {
Log.d(Config.LOGTAG,"ignoring broken session exception because checkForDuplicates failed");
//TODO should be still emit a failed message?
@ -147,31 +153,28 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
return null;
}
private Invite extractInvite(Account account, Element message) {
Element x = message.findChild("x", "http://jabber.org/protocol/muc#user");
if (x != null) {
Element invite = x.findChild("invite");
private Invite extractInvite(Element message) {
final Element mucUser = message.findChild("x", Namespace.MUC_USER);
if (mucUser != null) {
Element invite = mucUser.findChild("invite");
if (invite != null) {
String password = x.findChildContent("password");
String password = mucUser.findChildContent("password");
Jid from = InvalidJid.getNullForInvalid(invite.getAttributeAsJid("from"));
Contact contact = from == null ? null : account.getRoster().getContact(from);
Jid room = InvalidJid.getNullForInvalid(message.getAttributeAsJid("from"));
if (room == null) {
return null;
}
return new Invite(room, password, contact);
return new Invite(room, password, false, from);
}
} else {
x = message.findChild("x", "jabber:x:conference");
if (x != null) {
Jid from = InvalidJid.getNullForInvalid(message.getAttributeAsJid("from"));
Contact contact = from == null ? null : account.getRoster().getContact(from);
Jid room = InvalidJid.getNullForInvalid(x.getAttributeAsJid("jid"));
if (room == null) {
return null;
}
return new Invite(room, x.getAttribute("password"), contact);
}
final Element conference = message.findChild("x", "jabber:x:conference");
if (conference != null) {
Jid from = InvalidJid.getNullForInvalid(message.getAttributeAsJid("from"));
Jid room = InvalidJid.getNullForInvalid(conference.getAttributeAsJid("jid"));
if (room == null) {
return null;
}
return new Invite(room, conference.getAttribute("password"), true, from);
}
return null;
}
@ -328,8 +331,8 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
if (timestamp == null) {
timestamp = AbstractParser.parseTimestamp(original, AbstractParser.parseTimestamp(packet));
}
final String body = packet.getBody();
final Element mucUserElement = packet.findChild("x", "http://jabber.org/protocol/muc#user");
final LocalizedContent body = packet.getBody();
final Element mucUserElement = packet.findChild("x", Namespace.MUC_USER);
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);
@ -337,7 +340,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
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.findChild(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX);
final Element axolotlEncrypted = packet.findChildEnsureSingle(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX);
int status;
final Jid counterpart;
final Jid to = packet.getTo();
@ -377,9 +380,16 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
selfAddressed = false;
}
Invite invite = extractInvite(account, packet);
if (invite != null && invite.execute(account)) {
return;
final Invite invite = extractInvite(packet);
if (invite != null) {
if (isTypeGroupChat) {
Log.d(Config.LOGTAG,account.getJid().asBareJid()+": ignoring invite to "+invite.jid+" because type=groupchat");
} else if (invite.direct && (mucUserElement != null || invite.inviter == null || mXmppConnectionService.isMuc(account, invite.inviter))) {
Log.d(Config.LOGTAG, account.getJid().asBareJid()+": ignoring direct invite to "+invite.jid+" because it was received in MUC");
} else {
invite.execute(account);
return;
}
}
if ((body != null || pgpEncrypted != null || (axolotlEncrypted != null && axolotlEncrypted.hasChild("payload")) || oobUrl != null || xP1S3 != null) && !isMucStatusMessage) {
@ -409,10 +419,13 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
if (mXmppConnectionService.markMessage(conversation, remoteMsgId, status, serverMsgId)) {
return;
} else if (remoteMsgId == null || Config.IGNORE_ID_REWRITE_IN_MUC) {
Message message = conversation.findSentMessageWithBody(packet.getBody());
if (message != null) {
mXmppConnectionService.markMessage(message, status);
return;
LocalizedContent localizedBody = packet.getBody();
if (localizedBody != null) {
Message message = conversation.findSentMessageWithBody(localizedBody.content);
if (message != null) {
mXmppConnectionService.markMessage(message, status);
return;
}
}
}
} else {
@ -491,7 +504,10 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
message.setEncryption(Message.ENCRYPTION_DECRYPTED);
}
} else {
message = new Message(conversation, body, Message.ENCRYPTION_NONE, status);
message = new Message(conversation, body.content, Message.ENCRYPTION_NONE, status);
if (body.count > 1) {
message.setBodyLanguage(body.language);
}
}
message.setCounterpart(counterpart);
@ -499,7 +515,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
message.setServerMsgId(serverMsgId);
message.setCarbon(isCarbon);
message.setTime(timestamp);
if (body != null && body.equals(oobUrl)) {
if (body != null && body.content != null && body.content.equals(oobUrl)) {
message.setOob(true);
if (CryptoHelper.isPgpEncryptedUrl(oobUrl)) {
message.setEncryption(Message.ENCRYPTION_DECRYPTED);
@ -702,11 +718,11 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
}
if (isTypeGroupChat) {
if (packet.hasChild("subject")) {
if (packet.hasChild("subject")) { //TODO usually we would want to check for lack of body; however some servers do set a body :(
if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) {
conversation.setHasMessagesLeftOnServer(conversation.countMessages() > 0);
String subject = packet.findInternationalizedChildContent("subject");
if (conversation.getMucOptions().setSubject(subject)) {
final LocalizedContent subject = packet.findInternationalizedChildContentInDefaultNamespace("subject");
if (subject != null && conversation.getMucOptions().setSubject(subject.content)) {
mXmppConnectionService.updateConversation(conversation);
}
mXmppConnectionService.updateConversationUi();
@ -791,9 +807,8 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
mXmppConnectionService.markRead(conversation);
}
} else if (!counterpart.isBareJid() && trueJid != null) {
ReadByMarker readByMarker = ReadByMarker.from(counterpart, trueJid);
final ReadByMarker readByMarker = ReadByMarker.from(counterpart, trueJid);
if (message.addReadByMarker(readByMarker)) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": added read by (" + readByMarker.getRealJid() + ") to message '" + message.getBody() + "'");
mXmppConnectionService.updateMessage(message, false);
}
}
@ -873,11 +888,13 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
private class Invite {
final Jid jid;
final String password;
final Contact inviter;
final boolean direct;
final Jid inviter;
Invite(Jid jid, String password, Contact inviter) {
Invite(Jid jid, String password, boolean direct, Jid inviter) {
this.jid = jid;
this.password = password;
this.direct = direct;
this.inviter = inviter;
}
@ -890,7 +907,8 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
} else {
conversation.getMucOptions().setPassword(password);
mXmppConnectionService.databaseBackend.updateConversation(conversation);
mXmppConnectionService.joinMuc(conversation, inviter != null && inviter.mutualPresenceSubscription());
final Contact contact = inviter != null ? account.getRoster().getContactFromContactList(inviter) : null;
mXmppConnectionService.joinMuc(conversation, contact != null && contact.mutualPresenceSubscription());
mXmppConnectionService.updateConversationUi();
}
return true;

View File

@ -60,7 +60,7 @@ public class PresenceParser extends AbstractParser implements
final Jid from = packet.getFrom();
if (!from.isBareJid()) {
final String type = packet.getAttribute("type");
final Element x = packet.findChild("x", "http://jabber.org/protocol/muc#user");
final Element x = packet.findChild("x", Namespace.MUC_USER);
Avatar avatar = Avatar.parsePresence(packet.findChild("x", "vcard-temp:x:update"));
final List<String> codes = getStatusCodes(x);
if (type == null) {
@ -364,7 +364,7 @@ public class PresenceParser extends AbstractParser implements
@Override
public void onPresencePacketReceived(Account account, PresencePacket packet) {
if (packet.hasChild("x", "http://jabber.org/protocol/muc#user")) {
if (packet.hasChild("x", Namespace.MUC_USER)) {
this.parseConferencePresence(packet, account);
} else if (packet.hasChild("x", "http://jabber.org/protocol/muc")) {
this.parseConferencePresence(packet, account);

View File

@ -62,7 +62,7 @@ import rocks.xmpp.addr.Jid;
public class DatabaseBackend extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "history";
private static final int DATABASE_VERSION = 44;
private static final int DATABASE_VERSION = 45;
private static DatabaseBackend instance = null;
private static String CREATE_CONTATCS_STATEMENT = "create table "
+ Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, "
@ -239,6 +239,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
+ Message.READ_BY_MARKERS + " TEXT,"
+ Message.MARKABLE + " NUMBER DEFAULT 0,"
+ Message.DELETED + " NUMBER DEFAULT 0,"
+ Message.BODY_LANGUAGE + " TEXT,"
+ Message.REMOTE_MSG_ID + " TEXT, FOREIGN KEY("
+ Message.CONVERSATION + ") REFERENCES "
+ Conversation.TABLENAME + "(" + Conversation.UUID
@ -540,6 +541,10 @@ public class DatabaseBackend extends SQLiteOpenHelper {
db.execSQL(CREATE_MESSAGE_RELATIVE_FILE_PATH_INDEX);
db.execSQL(CREATE_MESSAGE_TYPE_INDEX);
}
if (oldVersion < 45 && newVersion >= 45) {
db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.BODY_LANGUAGE);
}
}
private void canonicalizeJids(SQLiteDatabase db) {

View File

@ -56,6 +56,7 @@ import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.services.AttachFileToConversationRunnable;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.RecordingActivity;
import eu.siacs.conversations.ui.util.Attachment;
@ -111,6 +112,7 @@ public class FileBackend {
}
public static boolean allFilesUnderSize(Context context, List<Attachment> attachments, long max) {
final boolean compressVideo = !AttachFileToConversationRunnable.getVideoCompression(context).equals("uncompressed");
if (max <= 0) {
Log.d(Config.LOGTAG, "server did not report max file size for http upload");
return true; //exception to be compatible with HTTP Upload < v0.2
@ -120,7 +122,7 @@ public class FileBackend {
continue;
}
String mime = attachment.getMime();
if (mime != null && mime.startsWith("video/")) {
if (mime != null && mime.startsWith("video/") && compressVideo) {
try {
Dimensions dimensions = FileBackend.getVideoDimensions(context, attachment.getUri());
if (dimensions.getMin() > 720) {
@ -191,6 +193,14 @@ public class FileBackend {
return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) + "/Camera/";
}
public static Uri getUriForUri(Context context, Uri uri) {
if ("file".equals(uri.getScheme())) {
return getUriForFile(context, new File(uri.getPath()));
} else {
return uri;
}
}
public static Uri getUriForFile(Context context, File file) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N || Config.ONLY_INTERNAL_STORAGE) {
try {

View File

@ -1,5 +1,6 @@
package eu.siacs.conversations.services;
import android.content.Context;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Build;
@ -177,7 +178,11 @@ public class AttachFileToConversationRunnable implements Runnable, MediaTranscod
}
private String getVideoCompression() {
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService);
return preferences.getString("video_compression", mXmppConnectionService.getResources().getString(R.string.video_compression));
return getVideoCompression(mXmppConnectionService);
}
public static String getVideoCompression(final Context context) {
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
return preferences.getString("video_compression", context.getResources().getString(R.string.video_compression));
}
}

View File

@ -1,11 +1,10 @@
package eu.siacs.conversations.services;
import android.support.annotation.NonNull;
import android.util.Log;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import java.io.IOException;
import java.util.Collections;
@ -17,6 +16,7 @@ import eu.siacs.conversations.Config;
import eu.siacs.conversations.http.HttpConnectionManager;
import eu.siacs.conversations.http.services.MuclumbusService;
import okhttp3.OkHttpClient;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
@ -32,13 +32,14 @@ public class ChannelDiscoveryService {
private final Cache<String, List<MuclumbusService.Room>> cache;
public ChannelDiscoveryService(XmppConnectionService service) {
ChannelDiscoveryService(XmppConnectionService service) {
this.service = service;
this.cache = CacheBuilder.newBuilder().expireAfterWrite(5, TimeUnit.MINUTES).build();
}
public void initializeMuclumbusService() {
OkHttpClient.Builder builder = new OkHttpClient.Builder();
void initializeMuclumbusService() {
final OkHttpClient.Builder builder = new OkHttpClient.Builder();
if (service.useTorToConnect()) {
try {
builder.proxy(HttpConnectionManager.getProxy());
@ -55,9 +56,8 @@ public class ChannelDiscoveryService {
this.muclumbusService = retrofit.create(MuclumbusService.class);
}
public void discover(String query, OnChannelSearchResultsFound onChannelSearchResultsFound) {
void discover(String query, OnChannelSearchResultsFound onChannelSearchResultsFound) {
final boolean all = query == null || query.trim().isEmpty();
Log.d(Config.LOGTAG, "discover channels. query=" + query);
List<MuclumbusService.Room> result = cache.getIfPresent(all ? "" : query);
if (result != null) {
onChannelSearchResultsFound.onChannelSearchResultsFound(result);
@ -75,9 +75,11 @@ public class ChannelDiscoveryService {
try {
call.enqueue(new Callback<MuclumbusService.Rooms>() {
@Override
public void onResponse(Call<MuclumbusService.Rooms> call, Response<MuclumbusService.Rooms> response) {
public void onResponse(@NonNull Call<MuclumbusService.Rooms> call, @NonNull Response<MuclumbusService.Rooms> response) {
final MuclumbusService.Rooms body = response.body();
if (body == null) {
listener.onChannelSearchResultsFound(Collections.emptyList());
logError(response);
return;
}
cache.put("", body.items);
@ -85,8 +87,8 @@ public class ChannelDiscoveryService {
}
@Override
public void onFailure(Call<MuclumbusService.Rooms> call, Throwable throwable) {
Log.d(Config.LOGTAG, "Unable to query muclumbus on "+Config.CHANNEL_DISCOVERY, throwable);
public void onFailure(@NonNull Call<MuclumbusService.Rooms> call, @NonNull Throwable throwable) {
Log.d(Config.LOGTAG, "Unable to query muclumbus on " + Config.CHANNEL_DISCOVERY, throwable);
listener.onChannelSearchResultsFound(Collections.emptyList());
}
});
@ -96,14 +98,16 @@ public class ChannelDiscoveryService {
}
private void discoverChannels(final String query, OnChannelSearchResultsFound listener) {
Call<MuclumbusService.SearchResult> searchResultCall = muclumbusService.search(new MuclumbusService.SearchRequest(query));
MuclumbusService.SearchRequest searchRequest = new MuclumbusService.SearchRequest(query);
Call<MuclumbusService.SearchResult> searchResultCall = muclumbusService.search(searchRequest);
searchResultCall.enqueue(new Callback<MuclumbusService.SearchResult>() {
@Override
public void onResponse(Call<MuclumbusService.SearchResult> call, Response<MuclumbusService.SearchResult> response) {
System.out.println(response.message());
MuclumbusService.SearchResult body = response.body();
public void onResponse(@NonNull Call<MuclumbusService.SearchResult> call, @NonNull Response<MuclumbusService.SearchResult> response) {
final MuclumbusService.SearchResult body = response.body();
if (body == null) {
listener.onChannelSearchResultsFound(Collections.emptyList());
logError(response);
return;
}
cache.put(query, body.result.items);
@ -111,13 +115,26 @@ public class ChannelDiscoveryService {
}
@Override
public void onFailure(Call<MuclumbusService.SearchResult> call, Throwable throwable) {
Log.d(Config.LOGTAG, "Unable to query muclumbus on "+Config.CHANNEL_DISCOVERY, throwable);
public void onFailure(@NonNull Call<MuclumbusService.SearchResult> call, @NonNull Throwable throwable) {
Log.d(Config.LOGTAG, "Unable to query muclumbus on " + Config.CHANNEL_DISCOVERY, throwable);
listener.onChannelSearchResultsFound(Collections.emptyList());
}
});
}
private static void logError(final Response response) {
final ResponseBody errorBody = response.errorBody();
Log.d(Config.LOGTAG, "code from muclumbus=" + response.code());
if (errorBody == null) {
return;
}
try {
Log.d(Config.LOGTAG,"error body="+errorBody.string());
} catch (IOException e) {
//ignored
}
}
public interface OnChannelSearchResultsFound {
void onChannelSearchResultsFound(List<MuclumbusService.Room> results);
}

View File

@ -1383,10 +1383,8 @@ public class XmppConnectionService extends Service {
}
}
final boolean inProgressJoin;
synchronized (account.inProgressConferenceJoins) {
inProgressJoin = conversation.getMode() == Conversational.MODE_MULTI && (account.inProgressConferenceJoins.contains(conversation) || account.pendingConferenceJoins.contains(conversation));
}
final boolean inProgressJoin = isJoinInProgress(conversation);
if (account.isOnlineAndConnected() && !inProgressJoin) {
switch (message.getEncryption()) {
@ -1516,6 +1514,23 @@ public class XmppConnectionService extends Service {
}
}
private boolean isJoinInProgress(final Conversation conversation) {
final Account account = conversation.getAccount();
synchronized (account.inProgressConferenceJoins) {
if (conversation.getMode() == Conversational.MODE_MULTI) {
final boolean inProgress = account.inProgressConferenceJoins.contains(conversation);
final boolean pending = account.pendingConferenceJoins.contains(conversation);
final boolean inProgressJoin = inProgress || pending;
if (inProgressJoin) {
Log.d(Config.LOGTAG,account.getJid().asBareJid()+": holding back message to group. inProgress="+inProgress+", pending="+pending);
}
return inProgressJoin;
} else {
return false;
}
}
}
private void sendUnsentMessages(final Conversation conversation) {
conversation.findWaitingMessages(message -> resendMessage(message, true));
}
@ -2183,17 +2198,24 @@ public class XmppConnectionService extends Service {
}
public void deleteAccount(final Account account) {
final boolean connected = account.getStatus() == Account.State.ONLINE;
synchronized (this.conversations) {
for (final Conversation conversation : conversations) {
if (conversation.getAccount() == account) {
if (conversation.getMode() == Conversation.MODE_MULTI) {
leaveMuc(conversation);
}
conversations.remove(conversation);
}
}
if (connected) {
account.getAxolotlService().deleteOmemoIdentity();
}
for (final Conversation conversation : conversations) {
if (conversation.getAccount() == account) {
if (conversation.getMode() == Conversation.MODE_MULTI) {
if (connected) {
leaveMuc(conversation);
}
}
conversations.remove(conversation);
mNotificationService.clear(conversation);
}
}
if (account.getXmppConnection() != null) {
new Thread(() -> disconnect(account, true)).start();
new Thread(() -> disconnect(account, !connected)).start();
}
final Runnable runnable = () -> {
if (!databaseBackend.deleteAccount(account)) {
@ -2204,7 +2226,7 @@ public class XmppConnectionService extends Service {
this.accounts.remove(account);
this.mRosterSyncTaskManager.clear(account);
updateAccountUi();
getNotificationService().updateErrorNotification();
mNotificationService.updateErrorNotification();
syncEnabledAccountSetting();
toggleForegroundService();
}
@ -2550,6 +2572,9 @@ public class XmppConnectionService extends Service {
final MucOptions mucOptions = conversation.getMucOptions();
if (mucOptions.nonanonymous() && !mucOptions.membersOnly() && !conversation.getBooleanAttribute("accept_non_anonymous", false)) {
synchronized (account.inProgressConferenceJoins) {
account.inProgressConferenceJoins.remove(conversation);
}
mucOptions.setError(MucOptions.Error.NON_ANONYMOUS);
updateConversationUi();
if (onConferenceJoined != null) {
@ -2943,9 +2968,11 @@ public class XmppConnectionService extends Service {
for (Jid invite : jids) {
invite(conversation, invite);
}
if (account.countPresences() > 1) {
directInvite(conversation, account.getJid().asBareJid());
}
for(String resource : account.getSelfContact().getPresences().toResourceArray()) {
Jid other = account.getJid().withResource(resource);
Log.d(Config.LOGTAG,account.getJid().asBareJid()+": sending direct invite to "+other);
directInvite(conversation, other);
}
saveConversationAsBookmark(conversation, name);
if (callback != null) {
callback.success(conversation);
@ -2989,31 +3016,31 @@ public class XmppConnectionService extends Service {
@Override
public void onIqPacketReceived(Account account, IqPacket packet) {
if (packet.getType() == IqPacket.TYPE.RESULT) {
final MucOptions mucOptions = conversation.getMucOptions();
final Bookmark bookmark = conversation.getBookmark();
final boolean sameBefore = StringUtils.equals(bookmark == null ? null : bookmark.getBookmarkName(), mucOptions.getName());
final MucOptions mucOptions = conversation.getMucOptions();
final Bookmark bookmark = conversation.getBookmark();
final boolean sameBefore = StringUtils.equals(bookmark == null ? null : bookmark.getBookmarkName(), mucOptions.getName());
if (mucOptions.updateConfiguration(new ServiceDiscoveryResult(packet))) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": muc configuration changed for " + conversation.getJid().asBareJid());
updateConversation(conversation);
}
if (mucOptions.updateConfiguration(new ServiceDiscoveryResult(packet))) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": muc configuration changed for " + conversation.getJid().asBareJid());
updateConversation(conversation);
}
if (bookmark != null && (sameBefore || bookmark.getBookmarkName() == null)) {
if (bookmark.setBookmarkName(StringUtils.nullOnEmpty(mucOptions.getName()))) {
pushBookmarks(account);
}
}
if (bookmark != null && (sameBefore || bookmark.getBookmarkName() == null)) {
if (bookmark.setBookmarkName(StringUtils.nullOnEmpty(mucOptions.getName()))) {
pushBookmarks(account);
}
}
if (callback != null) {
callback.onConferenceConfigurationFetched(conversation);
}
if (callback != null) {
callback.onConferenceConfigurationFetched(conversation);
}
updateConversationUi();
} else if (packet.getType() == IqPacket.TYPE.ERROR) {
updateConversationUi();
} else if (packet.getType() == IqPacket.TYPE.TIMEOUT) {
Log.d(Config.LOGTAG,account.getJid().asBareJid()+": received timeout waiting for conference configuration fetch");
} else {
if (callback != null) {
callback.onFetchFailed(conversation, packet.getError());
}
@ -3073,7 +3100,6 @@ public class XmppConnectionService extends Service {
if (packet.getType() == IqPacket.TYPE.RESULT) {
Data data = Data.parse(packet.query().findChild("x", Namespace.DATA));
data.submit(options);
Log.d(Config.LOGTAG,data.toString());
IqPacket set = new IqPacket(IqPacket.TYPE.SET);
set.setTo(conversation.getJid().asBareJid());
set.query("http://jabber.org/protocol/muc#owner").addChild(data);
@ -3750,11 +3776,11 @@ public class XmppConnectionService extends Service {
public void markMessage(Message message, int status, String errorMessage) {
final int c = message.getStatus();
if (status == Message.STATUS_SEND_FAILED && (c == Message.STATUS_SEND_RECEIVED || c == Message.STATUS_SEND_DISPLAYED)) {
final int oldStatus = message.getStatus();
if (status == Message.STATUS_SEND_FAILED && (oldStatus == Message.STATUS_SEND_RECEIVED || oldStatus == Message.STATUS_SEND_DISPLAYED)) {
return;
}
if (status == Message.STATUS_SEND_RECEIVED && c == Message.STATUS_SEND_DISPLAYED) {
if (status == Message.STATUS_SEND_RECEIVED && oldStatus == Message.STATUS_SEND_DISPLAYED) {
return;
}
message.setErrorMessage(errorMessage);

View File

@ -2,8 +2,10 @@ package eu.siacs.conversations.ui;
import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.databinding.DataBindingUtil;
import android.net.Uri;
import android.os.Bundle;
import android.support.v7.widget.Toolbar;
import android.text.Html;
@ -19,6 +21,7 @@ import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.ActivityChannelDiscoveryBinding;
import eu.siacs.conversations.entities.Account;
@ -37,11 +40,8 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O
private static final String CHANNEL_DISCOVERY_OPT_IN = "channel_discovery_opt_in";
private final ChannelSearchResultAdapter adapter = new ChannelSearchResultAdapter();
private ActivityChannelDiscoveryBinding binding;
private final PendingItem<String> mInitialSearchValue = new PendingItem<>();
private ActivityChannelDiscoveryBinding binding;
private MenuItem mMenuSearchView;
private EditText mSearchEditText;
@ -198,18 +198,39 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O
}
public void joinChannelSearchResult(String accountJid, MuclumbusService.Room result) {
final boolean syncAutojoin = getBooleanPreference("autojoin", R.bool.autojoin);
Account account = xmppConnectionService.findAccountByJid(Jid.of(accountJid));
@Override
public boolean onContextItemSelected(MenuItem item) {
final MuclumbusService.Room room = adapter.getCurrent();
if (room != null) {
switch (item.getItemId()) {
case R.id.share_with:
StartConversationActivity.shareAsChannel(this, room.address);
return true;
case R.id.open_join_dialog:
final Intent intent = new Intent(this, StartConversationActivity.class);
intent.setAction(Intent.ACTION_VIEW);
intent.putExtra("force_dialog", true);
intent.setData(Uri.parse(String.format("xmpp:%s?join", room.address)));
startActivity(intent);
return true;
}
}
return false;
}
public void joinChannelSearchResult(String selectedAccount, MuclumbusService.Room result) {
final Jid jid = Config.DOMAIN_LOCK == null ? Jid.of(selectedAccount) : Jid.of(selectedAccount, Config.DOMAIN_LOCK, null);
final boolean syncAutoJoin = getBooleanPreference("autojoin", R.bool.autojoin);
final Account account = xmppConnectionService.findAccountByJid(jid);
final Conversation conversation = xmppConnectionService.findOrCreateConversation(account, result.getRoom(), true, true, true);
if (conversation.getBookmark() != null) {
if (!conversation.getBookmark().autojoin() && syncAutojoin) {
if (!conversation.getBookmark().autojoin() && syncAutoJoin) {
conversation.getBookmark().setAutojoin(true);
xmppConnectionService.pushBookmarks(account);
}
} else {
final Bookmark bookmark = new Bookmark(account, conversation.getJid().asBareJid());
bookmark.setAutojoin(syncAutojoin);
bookmark.setAutojoin(syncAutoJoin);
account.getBookmarks().add(bookmark);
xmppConnectionService.pushBookmarks(account);
}

View File

@ -13,6 +13,9 @@ import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Intents;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.Toolbar;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.style.RelativeSizeSpan;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
@ -49,6 +52,7 @@ import eu.siacs.conversations.ui.util.JidDialog;
import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
import eu.siacs.conversations.utils.AccountUtils;
import eu.siacs.conversations.utils.Compatibility;
import eu.siacs.conversations.utils.Emoticons;
import eu.siacs.conversations.utils.IrregularUnicodeDetector;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.utils.XmppUri;
@ -328,14 +332,19 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
List<String> statusMessages = contact.getPresences().getStatusMessages();
if (statusMessages.size() == 0) {
binding.statusMessage.setVisibility(View.GONE);
} else if (statusMessages.size() == 1) {
final String message = statusMessages.get(0);
binding.statusMessage.setVisibility(View.VISIBLE);
final Spannable span = new SpannableString(message);
if (Emoticons.isOnlyEmoji(message)) {
span.setSpan(new RelativeSizeSpan(2.0f), 0, message.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
binding.statusMessage.setText(span);
} else {
StringBuilder builder = new StringBuilder();
binding.statusMessage.setVisibility(View.VISIBLE);
int s = statusMessages.size();
for (int i = 0; i < s; ++i) {
if (s > 1) {
builder.append("");
}
builder.append(statusMessages.get(i));
if (i < s - 1) {
builder.append("\n");

View File

@ -1044,6 +1044,10 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
return;
}
if (m.getStatus() == Message.STATUS_RECEIVED && t != null && (t.getStatus() == Transferable.STATUS_CANCELLED || t.getStatus() == Transferable.STATUS_FAILED)) {
return;
}
final boolean deleted = m.isDeleted();
final boolean encrypted = m.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED
|| m.getEncryption() == Message.ENCRYPTION_PGP;
@ -1552,7 +1556,8 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
intent = GeoHelper.getFetchIntent(activity);
break;
}
if (intent.resolveActivity(getActivity().getPackageManager()) != null) {
final Context context = getActivity();
if (context != null && intent.resolveActivity(context.getPackageManager()) != null) {
if (chooser) {
startActivityForResult(
Intent.createChooser(intent, getString(R.string.perform_action_with)),

View File

@ -4,6 +4,7 @@ import android.app.Activity;
import android.app.Dialog;
import android.databinding.DataBindingUtil;
import android.support.annotation.NonNull;
import android.support.design.widget.TextInputLayout;
import android.support.v4.app.DialogFragment;
import android.content.Context;
import android.content.DialogInterface;
@ -65,9 +66,9 @@ public class JoinConferenceDialog extends DialogFragment implements OnBackendCon
builder.setNegativeButton(R.string.cancel, null);
AlertDialog dialog = builder.create();
dialog.show();
dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(view -> mListener.onJoinDialogPositiveClick(dialog, binding.account, binding.jid, binding.bookmark.isChecked()));
dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(view -> mListener.onJoinDialogPositiveClick(dialog, binding.account, binding.accountJidLayout, binding.jid, binding.bookmark.isChecked()));
binding.jid.setOnEditorActionListener((v, actionId, event) -> {
mListener.onJoinDialogPositiveClick(dialog, binding.account, binding.jid, binding.bookmark.isChecked());
mListener.onJoinDialogPositiveClick(dialog, binding.account, binding.accountJidLayout, binding.jid, binding.bookmark.isChecked());
return true;
});
return dialog;
@ -116,6 +117,6 @@ public class JoinConferenceDialog extends DialogFragment implements OnBackendCon
}
public interface JoinConferenceDialogListener {
void onJoinDialogPositiveClick(Dialog dialog, Spinner spinner, AutoCompleteTextView jid, boolean isBookmarkChecked);
void onJoinDialogPositiveClick(Dialog dialog, Spinner spinner, TextInputLayout jidLayout, AutoCompleteTextView jid, boolean isBookmarkChecked);
}
}

View File

@ -15,6 +15,7 @@ import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.TextInputLayout;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
@ -405,14 +406,18 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
protected void shareBookmarkUri(int position) {
Bookmark bookmark = (Bookmark) conferences.get(position);
shareAsChannel(this, bookmark.getJid().asBareJid().toEscapedString());
}
public static void shareAsChannel(final Context context, final String address) {
Intent shareIntent = new Intent();
shareIntent.setAction(Intent.ACTION_SEND);
shareIntent.putExtra(Intent.EXTRA_TEXT, "xmpp:" + bookmark.getJid().asBareJid().toEscapedString() + "?join");
shareIntent.putExtra(Intent.EXTRA_TEXT, "xmpp:" + address + "?join");
shareIntent.setType("text/plain");
try {
startActivity(Intent.createChooser(shareIntent, getText(R.string.share_uri_with)));
context.startActivity(Intent.createChooser(shareIntent, context.getText(R.string.share_uri_with)));
} catch (ActivityNotFoundException e) {
Toast.makeText(this, R.string.no_application_to_share_uri, Toast.LENGTH_SHORT).show();
Toast.makeText(context, R.string.no_application_to_share_uri, Toast.LENGTH_SHORT).show();
}
}
@ -833,6 +838,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
if (uri != null) {
Invite invite = new Invite(intent.getData(), intent.getBooleanExtra("scanned", false));
invite.account = intent.getStringExtra("account");
invite.forceDialog = intent.getBooleanExtra("force_dialog", false);
return invite.invite();
} else {
return false;
@ -845,7 +851,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
List<Contact> contacts = xmppConnectionService.findContacts(invite.getJid(), invite.account);
if (invite.isAction(XmppUri.ACTION_JOIN)) {
Conversation muc = xmppConnectionService.findFirstMuc(invite.getJid());
if (muc != null) {
if (muc != null && !invite.forceDialog) {
switchToConversationDoNotAppend(muc, invite.getBody());
return true;
} else {
@ -1000,7 +1006,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
}
@Override
public void onJoinDialogPositiveClick(Dialog dialog, Spinner spinner, AutoCompleteTextView jid, boolean isBookmarkChecked) {
public void onJoinDialogPositiveClick(Dialog dialog, Spinner spinner, TextInputLayout layout, AutoCompleteTextView jid, boolean isBookmarkChecked) {
if (!xmppConnectionServiceBound) {
return;
}
@ -1008,17 +1014,26 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
if (account == null) {
return;
}
final Jid conferenceJid;
final String input = jid.getText().toString();
Jid conferenceJid;
try {
conferenceJid = Jid.of(jid.getText().toString());
conferenceJid = Jid.of(input);
} catch (final IllegalArgumentException e) {
jid.setError(getString(R.string.invalid_jid));
return;
final XmppUri xmppUri = new XmppUri(input);
if (xmppUri.isJidValid() && xmppUri.isAction(XmppUri.ACTION_JOIN)) {
final Editable editable = jid.getEditableText();
editable.clear();
editable.append(xmppUri.getJid().toEscapedString());
conferenceJid = xmppUri.getJid();
} else {
layout.setError(getString(R.string.invalid_jid));
return;
}
}
if (isBookmarkChecked) {
if (account.hasBookmarkFor(conferenceJid)) {
jid.setError(getString(R.string.bookmark_already_exists));
layout.setError(getString(R.string.bookmark_already_exists));
} else {
final Bookmark bookmark = new Bookmark(account, conferenceJid.asBareJid());
bookmark.setAutojoin(getBooleanPreference("autojoin", R.bool.autojoin));
@ -1274,6 +1289,8 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
public String account;
public boolean forceDialog = false;
public Invite(final Uri uri) {
super(uri);
}

View File

@ -1,11 +1,13 @@
package eu.siacs.conversations.ui.adapter;
import android.app.Activity;
import android.databinding.DataBindingUtil;
import android.support.annotation.NonNull;
import android.support.v7.recyclerview.extensions.ListAdapter;
import android.support.v7.util.DiffUtil;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -15,11 +17,11 @@ import java.util.Locale;
import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.SearchResultItemBinding;
import eu.siacs.conversations.http.services.MuclumbusService;
import eu.siacs.conversations.ui.XmppActivity;
import eu.siacs.conversations.ui.util.AvatarWorkerTask;
import rocks.xmpp.addr.Jid;
public class ChannelSearchResultAdapter extends ListAdapter<MuclumbusService.Room, ChannelSearchResultAdapter.ViewHolder> {
private OnChannelSearchResultSelected listener;
public class ChannelSearchResultAdapter extends ListAdapter<MuclumbusService.Room, ChannelSearchResultAdapter.ViewHolder> implements View.OnCreateContextMenuListener {
private static final DiffUtil.ItemCallback<MuclumbusService.Room> DIFF = new DiffUtil.ItemCallback<MuclumbusService.Room>() {
@Override
@ -32,6 +34,8 @@ public class ChannelSearchResultAdapter extends ListAdapter<MuclumbusService.Roo
return a.equals(b);
}
};
private OnChannelSearchResultSelected listener;
private MuclumbusService.Room current;
public ChannelSearchResultAdapter() {
super(DIFF);
@ -61,15 +65,37 @@ public class ChannelSearchResultAdapter extends ListAdapter<MuclumbusService.Roo
viewHolder.binding.language.setText(language.toUpperCase(Locale.ENGLISH));
viewHolder.binding.language.setVisibility(View.VISIBLE);
}
viewHolder.binding.room.setText(searchResult.getRoom().asBareJid().toString());
final Jid room = searchResult.getRoom();
viewHolder.binding.room.setText(room != null ? room.asBareJid().toString() : "");
AvatarWorkerTask.loadAvatar(searchResult, viewHolder.binding.avatar, R.dimen.avatar);
viewHolder.binding.getRoot().setOnClickListener(v -> listener.onChannelSearchResult(searchResult));
final View root = viewHolder.binding.getRoot();
root.setTag(searchResult);
root.setOnClickListener(v -> listener.onChannelSearchResult(searchResult));
root.setOnCreateContextMenuListener(this);
}
public void setOnChannelSearchResultSelectedListener(OnChannelSearchResultSelected listener) {
this.listener = listener;
}
public MuclumbusService.Room getCurrent() {
return this.current;
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
final Activity activity = XmppActivity.find(v);
final Object tag = v.getTag();
if (activity != null && tag instanceof MuclumbusService.Room) {
activity.getMenuInflater().inflate(R.menu.channel_item_context, menu);
this.current = (MuclumbusService.Room) tag;
}
}
public interface OnChannelSearchResultSelected {
void onChannelSearchResult(MuclumbusService.Room result);
}
public static class ViewHolder extends RecyclerView.ViewHolder {
@ -80,8 +106,4 @@ public class ChannelSearchResultAdapter extends ListAdapter<MuclumbusService.Roo
this.binding = binding;
}
}
public interface OnChannelSearchResultSelected {
void onChannelSearchResult(MuclumbusService.Room result);
}
}

View File

@ -1,17 +1,21 @@
package eu.siacs.conversations.ui.adapter;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.databinding.DataBindingUtil;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.Toast;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
@ -20,6 +24,7 @@ import java.util.concurrent.RejectedExecutionException;
import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.MediaPreviewBinding;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.ui.ConversationFragment;
import eu.siacs.conversations.ui.XmppActivity;
import eu.siacs.conversations.ui.util.Attachment;
@ -54,11 +59,24 @@ public class MediaPreviewAdapter extends RecyclerView.Adapter<MediaPreviewAdapte
MediaAdapter.renderPreview(context, attachment, holder.binding.mediaPreview);
}
holder.binding.deleteButton.setOnClickListener(v -> {
int pos = mediaPreviews.indexOf(attachment);
final int pos = mediaPreviews.indexOf(attachment);
mediaPreviews.remove(pos);
notifyItemRemoved(pos);
conversationFragment.toggleInputMethod();
});
holder.binding.mediaPreview.setOnClickListener(v -> view(context, attachment));
}
private static void view(final Context context, Attachment attachment) {
final Intent view = new Intent(Intent.ACTION_VIEW);
final Uri uri = FileBackend.getUriForUri(context, attachment.getUri());
view.setDataAndType(uri, attachment.getMime());
view.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
try {
context.startActivity(view);
} catch (ActivityNotFoundException e) {
Toast.makeText(context, R.string.no_application_found_to_open_file, Toast.LENGTH_SHORT).show();
}
}
public void addMediaPreviews(List<Attachment> attachments) {

View File

@ -36,6 +36,7 @@ import com.google.common.base.Strings;
import java.net.URL;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -187,7 +188,7 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
if (message.isFileOrImage() || transferable != null) {
FileParams params = message.getFileParams();
filesize = params.size > 0 ? UIHelper.filesizeToString(params.size) : null;
if (transferable != null && transferable.getStatus() == Transferable.STATUS_FAILED) {
if (transferable != null && (transferable.getStatus() == Transferable.STATUS_FAILED || transferable.getStatus() == Transferable.STATUS_CANCELLED)) {
error = true;
}
}
@ -206,10 +207,6 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
info = getContext().getString(R.string.offering);
break;
case Message.STATUS_SEND_RECEIVED:
if (mIndicateReceived) {
viewHolder.indicatorReceived.setVisibility(View.VISIBLE);
}
break;
case Message.STATUS_SEND_DISPLAYED:
if (mIndicateReceived) {
viewHolder.indicatorReceived.setVisibility(View.VISIBLE);
@ -283,30 +280,32 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
viewHolder.indicator.setVisibility(View.VISIBLE);
}
String formatedTime = UIHelper.readableTimeDifferenceFull(getContext(), message.getMergedTimeSent());
if (message.getStatus() <= Message.STATUS_RECEIVED) {
final String formattedTime = UIHelper.readableTimeDifferenceFull(getContext(), message.getMergedTimeSent());
final String bodyLanguage = message.getBodyLanguage();
final String bodyLanguageInfo = bodyLanguage == null ? "" : String.format(" \u00B7 %s", bodyLanguage.toUpperCase(Locale.US));
if (message.getStatus() <= Message.STATUS_RECEIVED) { ;
if ((filesize != null) && (info != null)) {
viewHolder.time.setText(formatedTime + " \u00B7 " + filesize + " \u00B7 " + info);
viewHolder.time.setText(formattedTime + " \u00B7 " + filesize + " \u00B7 " + info + bodyLanguageInfo);
} else if ((filesize == null) && (info != null)) {
viewHolder.time.setText(formatedTime + " \u00B7 " + info);
viewHolder.time.setText(formattedTime + " \u00B7 " + info + bodyLanguageInfo);
} else if ((filesize != null) && (info == null)) {
viewHolder.time.setText(formatedTime + " \u00B7 " + filesize);
viewHolder.time.setText(formattedTime + " \u00B7 " + filesize + bodyLanguageInfo);
} else {
viewHolder.time.setText(formatedTime);
viewHolder.time.setText(formattedTime+bodyLanguageInfo);
}
} else {
if ((filesize != null) && (info != null)) {
viewHolder.time.setText(filesize + " \u00B7 " + info);
viewHolder.time.setText(filesize + " \u00B7 " + info + bodyLanguageInfo);
} else if ((filesize == null) && (info != null)) {
if (error) {
viewHolder.time.setText(info + " \u00B7 " + formatedTime);
viewHolder.time.setText(info + " \u00B7 " + formattedTime + bodyLanguageInfo);
} else {
viewHolder.time.setText(info);
}
} else if ((filesize != null) && (info == null)) {
viewHolder.time.setText(filesize + " \u00B7 " + formatedTime);
viewHolder.time.setText(filesize + " \u00B7 " + formattedTime + bodyLanguageInfo);
} else {
viewHolder.time.setText(formatedTime);
viewHolder.time.setText(formattedTime+bodyLanguageInfo);
}
}
}

View File

@ -147,6 +147,8 @@ public final class MucDetailsContextMenuHelper {
activity.xmppConnectionService.changeAffiliationInConference(conversation, jid, MucOptions.Affiliation.ADMIN, onAffiliationChanged);
return true;
case R.id.give_membership:
case R.id.remove_admin_privileges:
case R.id.revoke_owner_privileges:
activity.xmppConnectionService.changeAffiliationInConference(conversation, jid, MucOptions.Affiliation.MEMBER, onAffiliationChanged);
return true;
case R.id.give_owner_privileges:
@ -155,10 +157,6 @@ public final class MucDetailsContextMenuHelper {
case R.id.remove_membership:
activity.xmppConnectionService.changeAffiliationInConference(conversation, jid, MucOptions.Affiliation.NONE, onAffiliationChanged);
return true;
case R.id.remove_admin_privileges:
case R.id.revoke_owner_privileges:
activity.xmppConnectionService.changeAffiliationInConference(conversation, jid, MucOptions.Affiliation.MEMBER, onAffiliationChanged);
return true;
case R.id.remove_from_room:
removeFromRoom(user, activity, onAffiliationChanged);
return true;
@ -180,7 +178,7 @@ public final class MucDetailsContextMenuHelper {
return true;
case R.id.invite:
if (user.getAffiliation().ranks(MucOptions.Affiliation.MEMBER)) {
activity.xmppConnectionService.directInvite(conversation, jid);
activity.xmppConnectionService.directInvite(conversation, jid.asBareJid());
} else {
activity.xmppConnectionService.invite(conversation, jid);
}

View File

@ -53,11 +53,10 @@ public class ShareUtil {
if (message.isGeoUri()) {
shareIntent.putExtra(Intent.EXTRA_TEXT, message.getBody());
shareIntent.setType("text/plain");
shareIntent.putExtra(ConversationsActivity.EXTRA_AS_QUOTE, true);
} else if (!message.isFileOrImage()) {
shareIntent.putExtra(Intent.EXTRA_TEXT, message.getMergedBody().toString());
shareIntent.setType("text/plain");
shareIntent.putExtra(ConversationsActivity.EXTRA_AS_QUOTE, true);
shareIntent.putExtra(ConversationsActivity.EXTRA_AS_QUOTE, message.getStatus() == Message.STATUS_RECEIVED);
} else {
final DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message);
try {

View File

@ -1,8 +1,5 @@
package eu.siacs.conversations.ui.widget;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
@ -13,199 +10,202 @@ import android.view.Menu;
import android.view.MenuItem;
import android.widget.TextView;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class ListSelectionManager {
private static final int MESSAGE_SEND_RESET = 1;
private static final int MESSAGE_RESET = 2;
private static final int MESSAGE_START_SELECTION = 3;
private static final int MESSAGE_SEND_RESET = 1;
private static final int MESSAGE_RESET = 2;
private static final int MESSAGE_START_SELECTION = 3;
private static final Field FIELD_EDITOR;
private static final Method METHOD_START_SELECTION;
private static final boolean SUPPORTED;
private static final Handler HANDLER = new Handler(Looper.getMainLooper(), new Handler.Callback() {
private static final Handler HANDLER = new Handler(Looper.getMainLooper(), new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE_SEND_RESET: {
// Skip one more message queue loop
HANDLER.obtainMessage(MESSAGE_RESET, msg.obj).sendToTarget();
return true;
}
case MESSAGE_RESET: {
final ListSelectionManager listSelectionManager = (ListSelectionManager) msg.obj;
listSelectionManager.futureSelectionIdentifier = null;
return true;
}
case MESSAGE_START_SELECTION: {
final StartSelectionHolder holder = (StartSelectionHolder) msg.obj;
holder.listSelectionManager.futureSelectionIdentifier = null;
startSelection(holder.textView, holder.start, holder.end);
return true;
}
}
return false;
}
});
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE_SEND_RESET: {
// Skip one more message queue loop
HANDLER.obtainMessage(MESSAGE_RESET, msg.obj).sendToTarget();
return true;
}
case MESSAGE_RESET: {
final ListSelectionManager listSelectionManager = (ListSelectionManager) msg.obj;
listSelectionManager.futureSelectionIdentifier = null;
return true;
}
case MESSAGE_START_SELECTION: {
final StartSelectionHolder holder = (StartSelectionHolder) msg.obj;
holder.listSelectionManager.futureSelectionIdentifier = null;
startSelection(holder.textView, holder.start, holder.end);
return true;
}
}
return false;
}
});
static {
Field editor;
try {
editor = TextView.class.getDeclaredField("mEditor");
editor.setAccessible(true);
} catch (Exception e) {
editor = null;
}
FIELD_EDITOR = editor;
Method startSelection = null;
if (editor != null) {
String[] startSelectionNames = {"startSelectionActionMode", "startSelectionActionModeWithSelection"};
for (String startSelectionName : startSelectionNames) {
try {
startSelection = editor.getType().getDeclaredMethod(startSelectionName);
startSelection.setAccessible(true);
break;
} catch (Exception e) {
startSelection = null;
}
}
}
METHOD_START_SELECTION = startSelection;
SUPPORTED = FIELD_EDITOR != null && METHOD_START_SELECTION != null;
}
private static class StartSelectionHolder {
private ActionMode selectionActionMode;
private Object selectionIdentifier;
private TextView selectionTextView;
private Object futureSelectionIdentifier;
private int futureSelectionStart;
private int futureSelectionEnd;
public final ListSelectionManager listSelectionManager;
public final TextView textView;
public final int start;
public final int end;
public static boolean isSupported() {
return SUPPORTED;
}
public StartSelectionHolder(ListSelectionManager listSelectionManager, TextView textView,
int start, int end) {
this.listSelectionManager = listSelectionManager;
this.textView = textView;
this.start = start;
this.end = end;
}
}
private static void startSelection(TextView textView, int start, int end) {
final CharSequence text = textView.getText();
if (SUPPORTED && start >= 0 && end > start && textView.isTextSelectable() && text instanceof Spannable) {
final Spannable spannable = (Spannable) text;
start = Math.min(start, spannable.length());
end = Math.min(end, spannable.length());
Selection.setSelection(spannable, start, end);
try {
final Object editor = FIELD_EDITOR != null ? FIELD_EDITOR.get(textView) : textView;
METHOD_START_SELECTION.invoke(editor);
} catch (Exception e) {
}
}
}
private ActionMode selectionActionMode;
private Object selectionIdentifier;
private TextView selectionTextView;
public void onCreate(TextView textView, ActionMode.Callback additionalCallback) {
final CustomCallback callback = new CustomCallback(textView, additionalCallback);
textView.setCustomSelectionActionModeCallback(callback);
}
private Object futureSelectionIdentifier;
private int futureSelectionStart;
private int futureSelectionEnd;
public void onUpdate(TextView textView, Object identifier) {
if (SUPPORTED) {
final ActionMode.Callback callback = textView.getCustomSelectionActionModeCallback();
if (callback instanceof CustomCallback) {
final CustomCallback customCallback = (CustomCallback) textView.getCustomSelectionActionModeCallback();
customCallback.identifier = identifier;
if (futureSelectionIdentifier == identifier) {
HANDLER.obtainMessage(MESSAGE_START_SELECTION, new StartSelectionHolder(this,
textView, futureSelectionStart, futureSelectionEnd)).sendToTarget();
}
}
}
}
public void onCreate(TextView textView, ActionMode.Callback additionalCallback) {
final CustomCallback callback = new CustomCallback(textView, additionalCallback);
textView.setCustomSelectionActionModeCallback(callback);
}
public void onBeforeNotifyDataSetChanged() {
if (SUPPORTED) {
HANDLER.removeMessages(MESSAGE_SEND_RESET);
HANDLER.removeMessages(MESSAGE_RESET);
HANDLER.removeMessages(MESSAGE_START_SELECTION);
if (selectionActionMode != null) {
final CharSequence text = selectionTextView.getText();
futureSelectionIdentifier = selectionIdentifier;
futureSelectionStart = Selection.getSelectionStart(text);
futureSelectionEnd = Selection.getSelectionEnd(text);
selectionActionMode.finish();
selectionActionMode = null;
selectionIdentifier = null;
selectionTextView = null;
}
}
}
public void onUpdate(TextView textView, Object identifier) {
if (SUPPORTED) {
CustomCallback callback = (CustomCallback) textView.getCustomSelectionActionModeCallback();
callback.identifier = identifier;
if (futureSelectionIdentifier == identifier) {
HANDLER.obtainMessage(MESSAGE_START_SELECTION, new StartSelectionHolder(this,
textView, futureSelectionStart, futureSelectionEnd)).sendToTarget();
}
}
}
public void onAfterNotifyDataSetChanged() {
if (SUPPORTED && futureSelectionIdentifier != null) {
HANDLER.obtainMessage(MESSAGE_SEND_RESET, this).sendToTarget();
}
}
public void onBeforeNotifyDataSetChanged() {
if (SUPPORTED) {
HANDLER.removeMessages(MESSAGE_SEND_RESET);
HANDLER.removeMessages(MESSAGE_RESET);
HANDLER.removeMessages(MESSAGE_START_SELECTION);
if (selectionActionMode != null) {
final CharSequence text = selectionTextView.getText();
futureSelectionIdentifier = selectionIdentifier;
futureSelectionStart = Selection.getSelectionStart(text);
futureSelectionEnd = Selection.getSelectionEnd(text);
selectionActionMode.finish();
selectionActionMode = null;
selectionIdentifier = null;
selectionTextView = null;
}
}
}
private static class StartSelectionHolder {
public void onAfterNotifyDataSetChanged() {
if (SUPPORTED && futureSelectionIdentifier != null) {
HANDLER.obtainMessage(MESSAGE_SEND_RESET, this).sendToTarget();
}
}
final ListSelectionManager listSelectionManager;
final TextView textView;
public final int start;
public final int end;
private class CustomCallback implements ActionMode.Callback {
StartSelectionHolder(ListSelectionManager listSelectionManager, TextView textView,
int start, int end) {
this.listSelectionManager = listSelectionManager;
this.textView = textView;
this.start = start;
this.end = end;
}
}
private final TextView textView;
private final ActionMode.Callback additionalCallback;
public Object identifier;
private class CustomCallback implements ActionMode.Callback {
public CustomCallback(TextView textView, ActionMode.Callback additionalCallback) {
this.textView = textView;
this.additionalCallback = additionalCallback;
}
private final TextView textView;
private final ActionMode.Callback additionalCallback;
Object identifier;
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
selectionActionMode = mode;
selectionIdentifier = identifier;
selectionTextView = textView;
if (additionalCallback != null) {
additionalCallback.onCreateActionMode(mode, menu);
}
return true;
}
CustomCallback(TextView textView, ActionMode.Callback additionalCallback) {
this.textView = textView;
this.additionalCallback = additionalCallback;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
if (additionalCallback != null) {
additionalCallback.onPrepareActionMode(mode, menu);
}
return true;
}
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
selectionActionMode = mode;
selectionIdentifier = identifier;
selectionTextView = textView;
if (additionalCallback != null) {
additionalCallback.onCreateActionMode(mode, menu);
}
return true;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
if (additionalCallback != null && additionalCallback.onActionItemClicked(mode, item)) {
return true;
}
return false;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
if (additionalCallback != null) {
additionalCallback.onPrepareActionMode(mode, menu);
}
return true;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
if (additionalCallback != null) {
additionalCallback.onDestroyActionMode(mode);
}
if (selectionActionMode == mode) {
selectionActionMode = null;
selectionIdentifier = null;
selectionTextView = null;
}
}
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
if (additionalCallback != null && additionalCallback.onActionItemClicked(mode, item)) {
return true;
}
return false;
}
private static final Field FIELD_EDITOR;
private static final Method METHOD_START_SELECTION;
private static final boolean SUPPORTED;
static {
Field editor;
try {
editor = TextView.class.getDeclaredField("mEditor");
editor.setAccessible(true);
} catch (Exception e) {
editor = null;
}
FIELD_EDITOR = editor;
Method startSelection = null;
if (editor != null) {
String[] startSelectionNames = {"startSelectionActionMode", "startSelectionActionModeWithSelection"};
for (String startSelectionName : startSelectionNames) {
try {
startSelection = editor.getType().getDeclaredMethod(startSelectionName);
startSelection.setAccessible(true);
break;
} catch (Exception e) {
startSelection = null;
}
}
}
METHOD_START_SELECTION = startSelection;
SUPPORTED = FIELD_EDITOR != null && METHOD_START_SELECTION != null;
}
public static boolean isSupported() {
return SUPPORTED;
}
public static void startSelection(TextView textView, int start, int end) {
final CharSequence text = textView.getText();
if (SUPPORTED && start >= 0 && end > start && textView.isTextSelectable() && text instanceof Spannable) {
final Spannable spannable = (Spannable) text;
start = Math.min(start, spannable.length());
end = Math.min(end, spannable.length());
Selection.setSelection(spannable, start, end);
try {
final Object editor = FIELD_EDITOR != null ? FIELD_EDITOR.get(textView) : textView;
METHOD_START_SELECTION.invoke(editor);
} catch (Exception e) {
}
}
}
@Override
public void onDestroyActionMode(ActionMode mode) {
if (additionalCallback != null) {
additionalCallback.onDestroyActionMode(mode);
}
if (selectionActionMode == mode) {
selectionActionMode = null;
selectionIdentifier = null;
selectionTextView = null;
}
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2018, Daniel Gultsch All rights reserved.
* Copyright (c) 2018-2019, 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:
@ -40,6 +40,8 @@ import android.text.style.ForegroundColorSpan;
import android.util.LruCache;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
@ -57,6 +59,7 @@ public class IrregularUnicodeDetector {
private static final Map<Character.UnicodeBlock, Character.UnicodeBlock> NORMALIZATION_MAP;
private static final LruCache<Jid, PatternTuple> CACHE = new LruCache<>(4096);
private static final List<String> AMBIGUOUS_CYRILLIC = Arrays.asList("а","г","е","ѕ","і","q","о","р","с","у");
static {
Map<Character.UnicodeBlock, Character.UnicodeBlock> temp = new HashMap<>();
@ -185,13 +188,41 @@ public class IrregularUnicodeDetector {
private static Set<String> findIrregularCodePoints(String word) {
Set<String> codePoints;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
codePoints = eliminateFirstAndGetCodePointsCompat(mapCompat(word));
final Map<Character.UnicodeBlock, List<String>> map = mapCompat(word);
final Set<String> set = asSet(map);
if (containsOnlyAmbiguousCyrillic(set)) {
return set;
}
codePoints = eliminateFirstAndGetCodePointsCompat(map);
} else {
codePoints = eliminateFirstAndGetCodePoints(map(word));
final Map<Character.UnicodeScript, List<String>> map = map(word);
final Set<String> set = asSet(map);
if (containsOnlyAmbiguousCyrillic(set)) {
return set;
}
codePoints = eliminateFirstAndGetCodePoints(map);
}
return codePoints;
}
private static Set<String> asSet(Map<?, List<String>> map) {
final Set<String> flat = new HashSet<>();
for(List<String> value : map.values()) {
flat.addAll(value);
}
return flat;
}
private static boolean containsOnlyAmbiguousCyrillic(Collection<String> codePoints) {
for (String codePoint : codePoints) {
if (!AMBIGUOUS_CYRILLIC.contains(codePoint)) {
return false;
}
}
return true;
}
private static PatternTuple find(Jid jid) {
synchronized (CACHE) {
PatternTuple pattern = CACHE.get(jid);

View File

@ -15,8 +15,8 @@ public class SocksSocketFactory {
private static final byte[] LOCALHOST = new byte[]{127,0,0,1};
public static void createSocksConnection(Socket socket, String destination, int port) throws IOException {
InputStream proxyIs = socket.getInputStream();
OutputStream proxyOs = socket.getOutputStream();
final InputStream proxyIs = socket.getInputStream();
final OutputStream proxyOs = socket.getOutputStream();
proxyOs.write(new byte[]{0x05, 0x01, 0x00});
byte[] response = new byte[2];
proxyIs.read(response);
@ -24,7 +24,7 @@ public class SocksSocketFactory {
throw new SocksConnectionException("Socks 5 handshake failed");
}
byte[] dest = destination.getBytes();
ByteBuffer request = ByteBuffer.allocate(7 + dest.length);
final ByteBuffer request = ByteBuffer.allocate(7 + dest.length);
request.put(new byte[]{0x05, 0x01, 0x00, 0x03});
request.put((byte) dest.length);
request.put(dest);
@ -52,7 +52,7 @@ public class SocksSocketFactory {
return false;
}
public static Socket createSocket(InetSocketAddress address, String destination, int port) throws IOException {
private static Socket createSocket(InetSocketAddress address, String destination, int port) throws IOException {
Socket socket = new Socket();
try {
socket.connect(address, Config.CONNECT_TIMEOUT * 1000);

View File

@ -274,6 +274,8 @@ public class UIHelper {
getFileDescriptionString(context, message)), true);
case Transferable.STATUS_FAILED:
return new Pair<>(context.getString(R.string.file_transmission_failed), true);
case Transferable.STATUS_CANCELLED:
return new Pair<>(context.getString(R.string.file_transmission_cancelled), true);
case Transferable.STATUS_UPLOADING:
if (message.getStatus() == Message.STATUS_OFFERED) {
return new Pair<>(context.getString(R.string.offering_x_file,

View File

@ -1,15 +1,11 @@
package eu.siacs.conversations.xml;
import android.support.annotation.NonNull;
import android.util.Log;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Locale;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.utils.XmlHelper;
import eu.siacs.conversations.xmpp.InvalidJid;
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
@ -71,31 +67,8 @@ public class Element {
return element == null ? null : element.getContent();
}
public String findInternationalizedChildContent(String name) {
return findInternationalizedChildContent(name, Locale.getDefault().getLanguage());
}
private String findInternationalizedChildContent(String name, @NonNull String language) {
final HashMap<String,String> contents = new HashMap<>();
for(Element child : this.children) {
if (name.equals(child.getName())) {
String lang = child.getAttribute("xml:lang");
String content = child.getContent();
if (content != null) {
if (language.equals(lang)) {
return content;
} else {
contents.put(lang, content);
}
}
}
}
final String value = contents.get(null);
if (value != null) {
return value;
}
final String[] values = contents.values().toArray(new String[0]);
return values.length == 0 ? null : values[0];
public LocalizedContent findInternationalizedChildContentInDefaultNamespace(String name) {
return LocalizedContent.get(this, name);
}
public Element findChild(String name, String xmlns) {
@ -107,6 +80,19 @@ public class Element {
return null;
}
public Element findChildEnsureSingle(String name, String xmlns) {
final List<Element> results = new ArrayList<>();
for (Element child : this.children) {
if (name.equals(child.getName()) && xmlns.equals(child.getAttribute("xmlns"))) {
results.add(child);
}
}
if (results.size() == 1) {
return results.get(0);
}
return null;
}
public String findChildContent(String name, String xmlns) {
Element element = findChild(name,xmlns);
return element == null ? null : element.getContent();

View File

@ -0,0 +1,59 @@
package eu.siacs.conversations.xml;
import com.google.common.collect.Iterables;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
public class LocalizedContent {
public static final String STREAM_LANGUAGE = "en";
public final String content;
public final String language;
public final int count;
private LocalizedContent(String content, String language, int count) {
this.content = content;
this.language = language;
this.count = count;
}
public static LocalizedContent get(final Element element, String name) {
final HashMap<String, String> contents = new HashMap<>();
final String parentLanguage = element.getAttribute("xml:lang");
for(Element child : element.children) {
if (name.equals(child.getName())) {
final String namespace = child.getNamespace();
final String childLanguage = child.getAttribute("xml:lang");
final String lang = childLanguage == null ? parentLanguage : childLanguage;
final String content = child.getContent();
if (content != null && (namespace == null || "jabber:client".equals(namespace))) {
if (contents.put(lang, content) != null) {
//anything that has multiple contents for the same language is invalid
return null;
}
}
}
}
if (contents.size() == 0) {
return null;
}
final String userLanguage = Locale.getDefault().getLanguage();
final String localized = contents.get(userLanguage);
if (localized != null) {
return new LocalizedContent(localized, userLanguage, contents.size());
}
final String defaultLanguageContent = contents.get(null);
if (defaultLanguageContent != null) {
return new LocalizedContent(defaultLanguageContent, STREAM_LANGUAGE, contents.size());
}
final String streamLanguageContent = contents.get(STREAM_LANGUAGE);
if (streamLanguageContent != null) {
return new LocalizedContent(streamLanguageContent, STREAM_LANGUAGE, contents.size());
}
final Map.Entry<String, String> first = Iterables.get(contents.entrySet(), 0);
return new LocalizedContent(first.getValue(), first.getKey(), contents.size());
}
}

View File

@ -32,4 +32,5 @@ public final class Namespace {
public static final String COMMANDS = "http://jabber.org/protocol/commands";
public static final String JINGLE_ENCRYPTED_TRANSPORT = "urn:xmpp:jingle:jet:0";
public static final String JINGLE_ENCRYPTED_TRANSPORT_OMEMO = "urn:xmpp:jingle:jet-omemo:0";
public static final String MUC_USER = "http://jabber.org/protocol/muc#user";
}

View File

@ -80,6 +80,7 @@ import eu.siacs.conversations.utils.SSLSocketHelper;
import eu.siacs.conversations.utils.SocksSocketFactory;
import eu.siacs.conversations.utils.XmlHelper;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.LocalizedContent;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xml.Tag;
import eu.siacs.conversations.xml.TagWriter;
@ -1334,7 +1335,7 @@ public class XmppConnection implements Runnable {
final Tag stream = Tag.start("stream:stream");
stream.setAttribute("to", account.getServer());
stream.setAttribute("version", "1.0");
stream.setAttribute("xml:lang", "en");
stream.setAttribute("xml:lang", LocalizedContent.STREAM_LANGUAGE);
stream.setAttribute("xmlns", "jabber:client");
stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams");
tagWriter.writeTag(stream);

View File

@ -142,7 +142,7 @@ public class JingleConnection implements Transferable {
@Override
public void onFileTransferAborted() {
JingleConnection.this.sendCancel();
JingleConnection.this.sendSessionTerminate("connectivity-error");
JingleConnection.this.fail();
}
};
@ -222,27 +222,32 @@ public class JingleConnection implements Transferable {
return this.message.getCounterpart();
}
public void deliverPacket(JinglePacket packet) {
boolean returnResult = true;
void deliverPacket(JinglePacket packet) {
if (packet.isAction("session-terminate")) {
Reason reason = packet.getReason();
if (reason != null) {
if (reason.hasChild("cancel")) {
this.cancelled = true;
this.fail();
} else if (reason.hasChild("success")) {
this.receiveSuccess();
} else {
this.fail();
final List<Element> children = reason.getChildren();
if (children.size() == 1) {
this.fail(children.get(0).getName());
} else {
this.fail();
}
}
} else {
this.fail();
}
} else if (packet.isAction("session-accept")) {
returnResult = receiveAccept(packet);
receiveAccept(packet);
} else if (packet.isAction("session-info")) {
Element checksum = packet.getChecksum();
Element file = checksum == null ? null : checksum.findChild("file");
Element hash = file == null ? null : file.findChild("hash", "urn:xmpp:hashes:2");
final Element checksum = packet.getChecksum();
final Element file = checksum == null ? null : checksum.findChild("file");
final Element hash = file == null ? null : file.findChild("hash", "urn:xmpp:hashes:2");
if (hash != null && "sha-1".equalsIgnoreCase(hash.getAttribute("algo"))) {
try {
this.expectedHash = Base64.decode(hash.getContent(), Base64.DEFAULT);
@ -250,33 +255,44 @@ public class JingleConnection implements Transferable {
this.expectedHash = new byte[0];
}
}
respondToIq(packet, true);
} else if (packet.isAction("transport-info")) {
returnResult = receiveTransportInfo(packet);
receiveTransportInfo(packet);
} else if (packet.isAction("transport-replace")) {
if (packet.getJingleContent().hasIbbTransport()) {
returnResult = this.receiveFallbackToIbb(packet);
receiveFallbackToIbb(packet);
} else {
returnResult = false;
Log.d(Config.LOGTAG, "trying to fallback to something unknown"
+ packet.toString());
Log.d(Config.LOGTAG, "trying to fallback to something unknown" + packet.toString());
respondToIq(packet, false);
}
} else if (packet.isAction("transport-accept")) {
returnResult = this.receiveTransportAccept(packet);
receiveTransportAccept(packet);
} else {
Log.d(Config.LOGTAG, "packet arrived in connection. action was "
+ packet.getAction());
returnResult = false;
Log.d(Config.LOGTAG, "packet arrived in connection. action was " + packet.getAction());
respondToIq(packet, false);
}
IqPacket response;
if (returnResult) {
response = packet.generateResponse(IqPacket.TYPE.RESULT);
}
private void respondToIq(final IqPacket packet, final boolean result) {
final IqPacket response;
if (result) {
response = packet.generateResponse(IqPacket.TYPE.RESULT);
} else {
response = packet.generateResponse(IqPacket.TYPE.ERROR);
final Element error = response.addChild("error").setAttribute("type", "cancel");
error.addChild("not-acceptable", "urn:ietf:params:xml:ns:xmpp-stanzas");
}
mXmppConnectionService.sendIqPacket(account, response, null);
}
private void respondToIqWithOutOfOrder(final IqPacket packet) {
final IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR);
final Element error = response.addChild("error").setAttribute("type", "wait");
error.addChild("unexpected-request", "urn:ietf:params:xml:ns:xmpp-stanzas");
error.addChild("out-of-order", "urn:xmpp:jingle:errors:1");
mXmppConnectionService.sendIqPacket(account, response, null);
}
public void init(final Message message) {
if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
Conversation conversation = (Conversation) message.getConversation();
@ -320,7 +336,7 @@ public class JingleConnection implements Transferable {
@Override
public void failed() {
Log.d(Config.LOGTAG, "connection to our own proxy65 candidate failed");
Log.d(Config.LOGTAG, String.format("connection to our own proxy65 candidate failed (%s:%d)", candidate.getHost(), candidate.getPort()));
sendInitRequest();
}
@ -400,7 +416,6 @@ public class JingleConnection implements Transferable {
this.contentName = content.getAttribute("name");
this.transportId = content.getTransportId();
mXmppConnectionService.sendIqPacket(account, packet.generateResponse(IqPacket.TYPE.RESULT), null);
if (this.initialTransport == Transport.SOCKS) {
this.mergeCandidates(JingleCandidate.parse(content.socks5transport().getChildren()));
@ -411,20 +426,20 @@ public class JingleConnection implements Transferable {
this.ibbBlockSize = Math.min(Integer.parseInt(receivedBlockSize), this.ibbBlockSize);
} catch (NumberFormatException e) {
Log.d(Config.LOGTAG, "number format exception " + e.getMessage());
this.sendCancel();
respondToIq(packet, false);
this.fail();
return;
}
} else {
Log.d(Config.LOGTAG, "received block size was null");
this.sendCancel();
respondToIq(packet, false);
this.fail();
return;
}
}
this.ftVersion = content.getVersion();
if (ftVersion == null) {
this.sendCancel();
respondToIq(packet, false);
this.fail();
return;
}
@ -486,6 +501,9 @@ public class JingleConnection implements Transferable {
//JET reports the plain text size. however lower levels of our receiving code still
//expect the cipher text size. so we just + 16 bytes (auth tag size) here
this.file.setExpectedSize(size + (remoteIsUsingJet ? 16 : 0));
respondToIq(packet, true);
if (mJingleConnectionManager.hasStoragePermission()
&& size < this.mJingleConnectionManager.getAutoAcceptFileSize()
&& mXmppConnectionService.isDataSaverDisabled()) {
@ -503,13 +521,9 @@ public class JingleConnection implements Transferable {
this.mXmppConnectionService.getNotificationService().push(message);
}
Log.d(Config.LOGTAG, "receiving file: expecting size of " + this.file.getExpectedSize());
} else {
this.sendCancel();
this.fail();
return;
}
} else {
this.sendCancel();
this.fail();
respondToIq(packet, false);
}
}
@ -557,14 +571,17 @@ public class JingleConnection implements Transferable {
try {
this.mFileInputStream = new FileInputStream(file);
} catch (FileNotFoundException e) {
abort();
fail(e.getMessage());
return;
}
content.setTransportId(this.transportId);
if (this.initialTransport == Transport.IBB) {
content.ibbTransport().setAttribute("block-size", Integer.toString(this.ibbBlockSize));
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending IBB offer");
} else {
content.socks5transport().setChildren(getCandidatesAsElements());
final List<Element> candidates = getCandidatesAsElements();
Log.d(Config.LOGTAG, String.format("%s: sending S5B offer with %d candidates", account.getJid().asBareJid(), candidates.size()));
content.socks5transport().setChildren(candidates);
}
packet.setContent(content);
this.sendJinglePacket(packet, (account, response) -> {
@ -682,18 +699,19 @@ public class JingleConnection implements Transferable {
mXmppConnectionService.sendIqPacket(account, packet, callback);
}
private boolean receiveAccept(JinglePacket packet) {
private void receiveAccept(JinglePacket packet) {
if (this.mJingleStatus != JINGLE_STATUS_INITIATED) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received out of order session-accept");
return false;
respondToIqWithOutOfOrder(packet);
return;
}
this.mJingleStatus = JINGLE_STATUS_ACCEPTED;
mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND);
Content content = packet.getJingleContent();
if (content.hasSocks5Transport()) {
respondToIq(packet, true);
mergeCandidates(JingleCandidate.parse(content.socks5transport().getChildren()));
this.connectNextCandidate();
return true;
} else if (content.hasIbbTransport()) {
String receivedBlockSize = packet.getJingleContent().ibbTransport().getAttribute("block-size");
if (receivedBlockSize != null) {
@ -706,18 +724,19 @@ public class JingleConnection implements Transferable {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to parse block size in session-accept");
}
}
respondToIq(packet, true);
this.transport = new JingleInbandTransport(this, this.transportId, this.ibbBlockSize);
this.transport.connect(onIbbTransportConnected);
return true;
} else {
return false;
respondToIq(packet, false);
}
}
private boolean receiveTransportInfo(JinglePacket packet) {
Content content = packet.getJingleContent();
private void receiveTransportInfo(JinglePacket packet) {
final Content content = packet.getJingleContent();
if (content.hasSocks5Transport()) {
if (content.socks5transport().hasChild("activated")) {
respondToIq(packet, true);
if ((this.transport != null) && (this.transport instanceof JingleSocks5Transport)) {
onProxyActivated.success();
} else {
@ -729,21 +748,20 @@ public class JingleConnection implements Transferable {
connection.setActivated(true);
} else {
Log.d(Config.LOGTAG, "activated connection not found");
this.sendCancel();
sendSessionTerminate("failed-transport");
this.fail();
}
}
return true;
} else if (content.socks5transport().hasChild("proxy-error")) {
respondToIq(packet, true);
onProxyActivated.failed();
return true;
} else if (content.socks5transport().hasChild("candidate-error")) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received candidate error");
respondToIq(packet, true);
this.receivedCandidate = true;
if (mJingleStatus == JINGLE_STATUS_ACCEPTED && this.sentCandidate) {
this.connect();
}
return true;
} else if (content.socks5transport().hasChild("candidate-used")) {
String cid = content.socks5transport().findChild("candidate-used").getAttribute("cid");
if (cid != null) {
@ -751,8 +769,10 @@ public class JingleConnection implements Transferable {
JingleCandidate candidate = getCandidate(cid);
if (candidate == null) {
Log.d(Config.LOGTAG, "could not find candidate with cid=" + cid);
return false;
respondToIq(packet, false);
return;
}
respondToIq(packet, true);
candidate.flagAsUsedByCounterpart();
this.receivedCandidate = true;
if (mJingleStatus == JINGLE_STATUS_ACCEPTED && this.sentCandidate) {
@ -760,15 +780,14 @@ public class JingleConnection implements Transferable {
} else {
Log.d(Config.LOGTAG, "ignoring because file is already in transmission or we haven't sent our candidate yet status=" + mJingleStatus + " sentCandidate=" + sentCandidate);
}
return true;
} else {
return false;
respondToIq(packet, false);
}
} else {
return false;
respondToIq(packet, false);
}
} else {
return true;
respondToIq(packet, true);
}
}
@ -867,11 +886,7 @@ public class JingleConnection implements Transferable {
}
private void sendSuccess() {
JinglePacket packet = bootstrapPacket("session-terminate");
Reason reason = new Reason();
reason.addChild("success");
packet.setReason(reason);
this.sendJinglePacket(packet);
sendSessionTerminate("success");
this.disconnectSocks5Connections();
this.mJingleStatus = JINGLE_STATUS_FINISHED;
this.message.setStatus(Message.STATUS_RECEIVED);
@ -893,7 +908,7 @@ public class JingleConnection implements Transferable {
}
private boolean receiveFallbackToIbb(JinglePacket packet) {
private void receiveFallbackToIbb(JinglePacket packet) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": receiving fallback to ibb");
final String receivedBlockSize = packet.getJingleContent().ibbTransport().getAttribute("block-size");
if (receivedBlockSize != null) {
@ -916,6 +931,7 @@ public class JingleConnection implements Transferable {
content.ibbTransport().setAttribute("sid", this.transportId);
answer.setContent(content);
respondToIq(packet, true);
if (initiating()) {
this.sendJinglePacket(answer, (account, response) -> {
@ -928,13 +944,13 @@ public class JingleConnection implements Transferable {
this.transport.receive(file, onFileTransmissionStatusChanged);
this.sendJinglePacket(answer);
}
return true;
}
private boolean receiveTransportAccept(JinglePacket packet) {
private void receiveTransportAccept(JinglePacket packet) {
if (packet.getJingleContent().hasIbbTransport()) {
String receivedBlockSize = packet.getJingleContent().ibbTransport()
.getAttribute("block-size");
final Element ibbTransport = packet.getJingleContent().ibbTransport();
final String receivedBlockSize = ibbTransport.getAttribute("block-size");
final String sid = ibbTransport.getAttribute("sid");
if (receivedBlockSize != null) {
try {
int bs = Integer.parseInt(receivedBlockSize);
@ -947,15 +963,19 @@ public class JingleConnection implements Transferable {
}
this.transport = new JingleInbandTransport(this, this.transportId, this.ibbBlockSize);
if (sid == null || !sid.equals(this.transportId)) {
Log.w(Config.LOGTAG, String.format("%s: sid in transport-accept (%s) did not match our sid (%s) ", account.getJid().asBareJid(), sid, transportId));
}
respondToIq(packet, true);
//might be receive instead if we are not initiating
if (initiating()) {
this.transport.connect(onIbbTransportConnected);
} else {
this.transport.receive(file, onFileTransmissionStatusChanged);
}
return true;
} else {
return false;
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received invalid transport-accept");
respondToIq(packet, false);
}
}
@ -977,18 +997,18 @@ public class JingleConnection implements Transferable {
@Override
public void cancel() {
this.cancelled = true;
abort();
abort("cancel");
}
public void abort() {
void abort(final String reason) {
this.disconnectSocks5Connections();
if (this.transport instanceof JingleInbandTransport) {
this.transport.disconnect();
}
this.sendCancel();
sendSessionTerminate(reason);
this.mJingleConnectionManager.finishConnection(this);
if (responding()) {
this.message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_FAILED));
this.message.setTransferable(new TransferablePlaceholder(cancelled ? Transferable.STATUS_CANCELLED : Transferable.STATUS_FAILED));
if (this.file != null) {
file.delete();
}
@ -1013,7 +1033,7 @@ public class JingleConnection implements Transferable {
FileBackend.close(mFileOutputStream);
if (this.message != null) {
if (responding()) {
this.message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_FAILED));
this.message.setTransferable(new TransferablePlaceholder(cancelled ? Transferable.STATUS_CANCELLED : Transferable.STATUS_FAILED));
if (this.file != null) {
file.delete();
}
@ -1028,11 +1048,11 @@ public class JingleConnection implements Transferable {
this.mJingleConnectionManager.finishConnection(this);
}
private void sendCancel() {
JinglePacket packet = bootstrapPacket("session-terminate");
Reason reason = new Reason();
reason.addChild("cancel");
packet.setReason(reason);
private void sendSessionTerminate(String reason) {
final JinglePacket packet = bootstrapPacket("session-terminate");
final Reason r = new Reason();
r.addChild(reason);
packet.setReason(r);
this.sendJinglePacket(packet);
}

View File

@ -106,7 +106,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
candidate.setPort(Integer.parseInt(port));
candidate.setType(JingleCandidate.TYPE_PROXY);
candidate.setJid(proxy);
candidate.setPriority(655360 + (initiator ? 10 : 20));
candidate.setPriority(655360 + (initiator ? 30 : 0));
primaryCandidates.put(account.getJid().asBareJid(),candidate);
listener.onPrimaryCandidateFound(true,candidate);
} catch (final NumberFormatException e) {
@ -166,7 +166,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
public void cancelInTransmission() {
for (JingleConnection connection : this.connections) {
if (connection.getJingleStatus() == JingleConnection.JINGLE_STATUS_TRANSMITTING) {
connection.abort();
connection.abort("connectivity-error");
}
}
}

View File

@ -23,223 +23,222 @@ import rocks.xmpp.addr.Jid;
public class JingleInbandTransport extends JingleTransport {
private Account account;
private Jid counterpart;
private int blockSize;
private int seq = 0;
private String sessionId;
private Account account;
private Jid counterpart;
private int blockSize;
private int seq = 0;
private String sessionId;
private boolean established = false;
private boolean established = false;
private boolean connected = true;
private boolean connected = true;
private DownloadableFile file;
private JingleConnection connection;
private DownloadableFile file;
private JingleConnection connection;
private InputStream fileInputStream = null;
private InputStream innerInputStream = null;
private OutputStream fileOutputStream = null;
private long remainingSize = 0;
private long fileSize = 0;
private MessageDigest digest;
private InputStream fileInputStream = null;
private InputStream innerInputStream = null;
private OutputStream fileOutputStream = null;
private long remainingSize = 0;
private long fileSize = 0;
private MessageDigest digest;
private OnFileTransmissionStatusChanged onFileTransmissionStatusChanged;
private OnFileTransmissionStatusChanged onFileTransmissionStatusChanged;
private OnIqPacketReceived onAckReceived = new OnIqPacketReceived() {
@Override
public void onIqPacketReceived(Account account, IqPacket packet) {
if (connected && packet.getType() == IqPacket.TYPE.RESULT) {
if (remainingSize > 0) {
sendNextBlock();
}
}
}
};
private OnIqPacketReceived onAckReceived = new OnIqPacketReceived() {
@Override
public void onIqPacketReceived(Account account, IqPacket packet) {
if (!connected) {
return;
}
if (packet.getType() == IqPacket.TYPE.RESULT) {
if (remainingSize > 0) {
sendNextBlock();
}
} else if (packet.getType() == IqPacket.TYPE.ERROR) {
onFileTransmissionStatusChanged.onFileTransferAborted();
}
}
};
public JingleInbandTransport(final JingleConnection connection, final String sid, final int blocksize) {
this.connection = connection;
this.account = connection.getAccount();
this.counterpart = connection.getCounterPart();
this.blockSize = blocksize;
this.sessionId = sid;
}
private void sendClose() {
IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
iq.setTo(this.counterpart);
Element close = iq.addChild("close", "http://jabber.org/protocol/ibb");
close.setAttribute("sid", this.sessionId);
this.account.getXmppConnection().sendIqPacket(iq, null);
}
public void connect(final OnTransportConnected callback) {
IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
iq.setTo(this.counterpart);
Element open = iq.addChild("open", "http://jabber.org/protocol/ibb");
open.setAttribute("sid", this.sessionId);
open.setAttribute("stanza", "iq");
open.setAttribute("block-size", Integer.toString(this.blockSize));
this.connected = true;
this.account.getXmppConnection().sendIqPacket(iq,
new OnIqPacketReceived() {
@Override
public void onIqPacketReceived(Account account,
IqPacket packet) {
if (packet.getType() != IqPacket.TYPE.RESULT) {
callback.failed();
} else {
callback.established();
}
}
});
}
@Override
public void receive(DownloadableFile file, OnFileTransmissionStatusChanged callback) {
this.onFileTransmissionStatusChanged = callback;
this.file = file;
try {
this.digest = MessageDigest.getInstance("SHA-1");
digest.reset();
this.fileOutputStream = connection.getFileOutputStream();
if (this.fileOutputStream == null) {
Log.d(Config.LOGTAG,account.getJid().asBareJid()+": could not create output stream");
callback.onFileTransferAborted();
return;
}
this.remainingSize = this.fileSize = file.getExpectedSize();
} catch (final NoSuchAlgorithmException | IOException e) {
Log.d(Config.LOGTAG,account.getJid().asBareJid()+" "+e.getMessage());
callback.onFileTransferAborted();
}
public JingleInbandTransport(final JingleConnection connection, final String sid, final int blocksize) {
this.connection = connection;
this.account = connection.getAccount();
this.counterpart = connection.getCounterPart();
this.blockSize = blocksize;
this.sessionId = sid;
}
@Override
public void send(DownloadableFile file, OnFileTransmissionStatusChanged callback) {
this.onFileTransmissionStatusChanged = callback;
this.file = file;
try {
this.remainingSize = this.file.getExpectedSize();
this.fileSize = this.remainingSize;
this.digest = MessageDigest.getInstance("SHA-1");
this.digest.reset();
fileInputStream = connection.getFileInputStream();
if (fileInputStream == null) {
Log.d(Config.LOGTAG,account.getJid().asBareJid()+": could no create input stream");
callback.onFileTransferAborted();
return;
}
innerInputStream = AbstractConnectionManager.upgrade(file, fileInputStream);
if (this.connected) {
this.sendNextBlock();
}
} catch (Exception e) {
callback.onFileTransferAborted();
Log.d(Config.LOGTAG,account.getJid().asBareJid()+": "+e.getMessage());
}
}
private void sendClose() {
IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
iq.setTo(this.counterpart);
Element close = iq.addChild("close", "http://jabber.org/protocol/ibb");
close.setAttribute("sid", this.sessionId);
this.account.getXmppConnection().sendIqPacket(iq, null);
}
@Override
public void disconnect() {
this.connected = false;
FileBackend.close(fileOutputStream);
FileBackend.close(fileInputStream);
}
public void connect(final OnTransportConnected callback) {
IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
iq.setTo(this.counterpart);
Element open = iq.addChild("open", "http://jabber.org/protocol/ibb");
open.setAttribute("sid", this.sessionId);
open.setAttribute("stanza", "iq");
open.setAttribute("block-size", Integer.toString(this.blockSize));
this.connected = true;
this.account.getXmppConnection().sendIqPacket(iq, (account, packet) -> {
if (packet.getType() != IqPacket.TYPE.RESULT) {
callback.failed();
} else {
callback.established();
}
});
}
private void sendNextBlock() {
byte[] buffer = new byte[this.blockSize];
try {
int count = innerInputStream.read(buffer);
if (count == -1) {
sendClose();
file.setSha1Sum(digest.digest());
Log.d(Config.LOGTAG,account.getJid().asBareJid()+": sendNextBlock() count was -1");
this.onFileTransmissionStatusChanged.onFileTransmitted(file);
fileInputStream.close();
return;
} else if (count != buffer.length) {
int rem = innerInputStream.read(buffer,count,buffer.length-count);
if (rem > 0) {
count += rem;
}
}
this.remainingSize -= count;
this.digest.update(buffer,0,count);
String base64 = Base64.encodeToString(buffer,0,count, Base64.NO_WRAP);
IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
iq.setTo(this.counterpart);
Element data = iq.addChild("data", "http://jabber.org/protocol/ibb");
data.setAttribute("seq", Integer.toString(this.seq));
data.setAttribute("block-size", Integer.toString(this.blockSize));
data.setAttribute("sid", this.sessionId);
data.setContent(base64);
this.account.getXmppConnection().sendIqPacket(iq, this.onAckReceived);
this.account.getXmppConnection().r(); //don't fill up stanza queue too much
this.seq++;
connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100));
if (this.remainingSize <= 0) {
sendClose();
file.setSha1Sum(digest.digest());
this.onFileTransmissionStatusChanged.onFileTransmitted(file);
fileInputStream.close();
}
} catch (IOException e) {
Log.d(Config.LOGTAG,account.getJid().asBareJid()+": io exception during sendNextBlock() "+e.getMessage());
FileBackend.close(fileInputStream);
this.onFileTransmissionStatusChanged.onFileTransferAborted();
}
}
@Override
public void receive(DownloadableFile file, OnFileTransmissionStatusChanged callback) {
this.onFileTransmissionStatusChanged = callback;
this.file = file;
try {
this.digest = MessageDigest.getInstance("SHA-1");
digest.reset();
this.fileOutputStream = connection.getFileOutputStream();
if (this.fileOutputStream == null) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could not create output stream");
callback.onFileTransferAborted();
return;
}
this.remainingSize = this.fileSize = file.getExpectedSize();
} catch (final NoSuchAlgorithmException | IOException e) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + " " + e.getMessage());
callback.onFileTransferAborted();
}
}
private void receiveNextBlock(String data) {
try {
byte[] buffer = Base64.decode(data, Base64.NO_WRAP);
if (this.remainingSize < buffer.length) {
buffer = Arrays.copyOfRange(buffer, 0, (int) this.remainingSize);
}
this.remainingSize -= buffer.length;
this.fileOutputStream.write(buffer);
this.digest.update(buffer);
if (this.remainingSize <= 0) {
file.setSha1Sum(digest.digest());
fileOutputStream.flush();
fileOutputStream.close();
Log.d(Config.LOGTAG,account.getJid().asBareJid()+": receive next block nothing remaining");
this.onFileTransmissionStatusChanged.onFileTransmitted(file);
} else {
connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100));
}
} catch (Exception e) {
Log.d(Config.LOGTAG,account.getJid().asBareJid()+": "+e.getMessage());
FileBackend.close(fileOutputStream);
this.onFileTransmissionStatusChanged.onFileTransferAborted();
}
}
@Override
public void send(DownloadableFile file, OnFileTransmissionStatusChanged callback) {
this.onFileTransmissionStatusChanged = callback;
this.file = file;
try {
this.remainingSize = this.file.getExpectedSize();
this.fileSize = this.remainingSize;
this.digest = MessageDigest.getInstance("SHA-1");
this.digest.reset();
fileInputStream = connection.getFileInputStream();
if (fileInputStream == null) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could no create input stream");
callback.onFileTransferAborted();
return;
}
innerInputStream = AbstractConnectionManager.upgrade(file, fileInputStream);
if (this.connected) {
this.sendNextBlock();
}
} catch (Exception e) {
callback.onFileTransferAborted();
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + e.getMessage());
}
}
public void deliverPayload(IqPacket packet, Element payload) {
if (payload.getName().equals("open")) {
if (!established) {
established = true;
connected = true;
this.receiveNextBlock("");
this.account.getXmppConnection().sendIqPacket(
packet.generateResponse(IqPacket.TYPE.RESULT), null);
} else {
this.account.getXmppConnection().sendIqPacket(
packet.generateResponse(IqPacket.TYPE.ERROR), null);
}
} else if (connected && payload.getName().equals("data")) {
this.receiveNextBlock(payload.getContent());
this.account.getXmppConnection().sendIqPacket(
packet.generateResponse(IqPacket.TYPE.RESULT), null);
} else if (connected && payload.getName().equals("close")) {
this.connected = false;
this.account.getXmppConnection().sendIqPacket(
packet.generateResponse(IqPacket.TYPE.RESULT), null);
Log.d(Config.LOGTAG,account.getJid().asBareJid()+": received ibb close");
} else {
Log.d(Config.LOGTAG,payload.toString());
// TODO some sort of exception
}
}
@Override
public void disconnect() {
this.connected = false;
FileBackend.close(fileOutputStream);
FileBackend.close(fileInputStream);
}
private void sendNextBlock() {
byte[] buffer = new byte[this.blockSize];
try {
int count = innerInputStream.read(buffer);
if (count == -1) {
sendClose();
file.setSha1Sum(digest.digest());
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sendNextBlock() count was -1");
this.onFileTransmissionStatusChanged.onFileTransmitted(file);
fileInputStream.close();
return;
} else if (count != buffer.length) {
int rem = innerInputStream.read(buffer, count, buffer.length - count);
if (rem > 0) {
count += rem;
}
}
this.remainingSize -= count;
this.digest.update(buffer, 0, count);
String base64 = Base64.encodeToString(buffer, 0, count, Base64.NO_WRAP);
IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
iq.setTo(this.counterpart);
Element data = iq.addChild("data", "http://jabber.org/protocol/ibb");
data.setAttribute("seq", Integer.toString(this.seq));
data.setAttribute("block-size", Integer.toString(this.blockSize));
data.setAttribute("sid", this.sessionId);
data.setContent(base64);
this.account.getXmppConnection().sendIqPacket(iq, this.onAckReceived);
this.account.getXmppConnection().r(); //don't fill up stanza queue too much
this.seq++;
connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100));
if (this.remainingSize <= 0) {
sendClose();
file.setSha1Sum(digest.digest());
this.onFileTransmissionStatusChanged.onFileTransmitted(file);
fileInputStream.close();
}
} catch (IOException e) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": io exception during sendNextBlock() " + e.getMessage());
FileBackend.close(fileInputStream);
this.onFileTransmissionStatusChanged.onFileTransferAborted();
}
}
private void receiveNextBlock(String data) {
try {
byte[] buffer = Base64.decode(data, Base64.NO_WRAP);
if (this.remainingSize < buffer.length) {
buffer = Arrays.copyOfRange(buffer, 0, (int) this.remainingSize);
}
this.remainingSize -= buffer.length;
this.fileOutputStream.write(buffer);
this.digest.update(buffer);
if (this.remainingSize <= 0) {
file.setSha1Sum(digest.digest());
fileOutputStream.flush();
fileOutputStream.close();
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": receive next block nothing remaining");
this.onFileTransmissionStatusChanged.onFileTransmitted(file);
} else {
connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100));
}
} catch (Exception e) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + e.getMessage());
FileBackend.close(fileOutputStream);
this.onFileTransmissionStatusChanged.onFileTransferAborted();
}
}
public void deliverPayload(IqPacket packet, Element payload) {
if (payload.getName().equals("open")) {
if (!established) {
established = true;
connected = true;
this.receiveNextBlock("");
this.account.getXmppConnection().sendIqPacket(
packet.generateResponse(IqPacket.TYPE.RESULT), null);
} else {
this.account.getXmppConnection().sendIqPacket(
packet.generateResponse(IqPacket.TYPE.ERROR), null);
}
} else if (connected && payload.getName().equals("data")) {
this.receiveNextBlock(payload.getContent());
this.account.getXmppConnection().sendIqPacket(
packet.generateResponse(IqPacket.TYPE.RESULT), null);
} else if (connected && payload.getName().equals("close")) {
this.connected = false;
this.account.getXmppConnection().sendIqPacket(
packet.generateResponse(IqPacket.TYPE.RESULT), null);
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received ibb close");
} else {
Log.d(Config.LOGTAG, payload.toString());
// TODO some sort of exception
}
}
}

View File

@ -26,6 +26,10 @@ import eu.siacs.conversations.utils.WakeLockHelper;
import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
public class JingleSocks5Transport extends JingleTransport {
private static final int SOCKET_TIMEOUT_DIRECT = 3000;
private static final int SOCKET_TIMEOUT_PROXY = 5000;
private final JingleCandidate candidate;
private final JingleConnection connection;
private final String destination;
@ -92,8 +96,9 @@ public class JingleSocks5Transport extends JingleTransport {
}
}
private void acceptIncomingSocketConnection(Socket socket) throws IOException {
private void acceptIncomingSocketConnection(final Socket socket) throws IOException {
Log.d(Config.LOGTAG, "accepted connection from " + socket.getInetAddress().getHostAddress());
socket.setSoTimeout(SOCKET_TIMEOUT_DIRECT);
final byte[] authBegin = new byte[2];
final InputStream inputStream = socket.getInputStream();
final OutputStream outputStream = socket.getOutputStream();
@ -115,7 +120,8 @@ public class JingleSocks5Transport extends JingleTransport {
int destinationCount = inputStream.read();
final byte[] destination = new byte[destinationCount];
inputStream.read(destination);
final int port = inputStream.read();
final byte[] port = new byte[2];
inputStream.read(port);
final String receivedDestination = new String(destination);
final ByteBuffer response = ByteBuffer.allocate(7 + destination.length);
final byte[] responseHeader;
@ -131,18 +137,19 @@ public class JingleSocks5Transport extends JingleTransport {
response.put(responseHeader);
response.put((byte) destination.length);
response.put(destination);
response.putShort((short) port);
response.put(port);
outputStream.write(response.array());
outputStream.flush();
if (success) {
Log.d(Config.LOGTAG,connection.getAccount().getJid().asBareJid()+": successfully processed connection to candidate "+candidate.getHost()+":"+candidate.getPort());
socket.setSoTimeout(0);
this.socket = socket;
this.inputStream = inputStream;
this.outputStream = outputStream;
this.isEstablished = true;
FileBackend.close(serverSocket);
} else {
this.socket.close();
FileBackend.close(socket);
}
} else {
socket.close();
@ -151,6 +158,7 @@ public class JingleSocks5Transport extends JingleTransport {
public void connect(final OnTransportConnected callback) {
new Thread(() -> {
final int timeout = candidate.getType() == JingleCandidate.TYPE_DIRECT ? SOCKET_TIMEOUT_DIRECT : SOCKET_TIMEOUT_PROXY;
try {
final boolean useTor = connection.getAccount().isOnion() || connection.getConnectionManager().getXmppConnectionService().useTorToConnect();
if (useTor) {
@ -158,11 +166,11 @@ public class JingleSocks5Transport extends JingleTransport {
} else {
socket = new Socket();
SocketAddress address = new InetSocketAddress(candidate.getHost(), candidate.getPort());
socket.connect(address, 5000);
socket.connect(address, timeout);
}
inputStream = socket.getInputStream();
outputStream = socket.getOutputStream();
socket.setSoTimeout(5000);
socket.setSoTimeout(timeout);
SocksSocketFactory.createSocksConnection(socket, destination, 0);
socket.setSoTimeout(0);
isEstablished = true;

View File

@ -4,6 +4,7 @@ import android.util.Pair;
import eu.siacs.conversations.parser.AbstractParser;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.LocalizedContent;
public class MessagePacket extends AbstractAcknowledgeableStanza {
public static final int TYPE_CHAT = 0;
@ -16,8 +17,8 @@ public class MessagePacket extends AbstractAcknowledgeableStanza {
super("message");
}
public String getBody() {
return findChildContent("body");
public LocalizedContent getBody() {
return findInternationalizedChildContentInDefaultNamespace("body");
}
public void setBody(String text) {

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/share_with"
android:title="@string/share_uri_with" />
<item
android:id="@+id/open_join_dialog"
android:title="@string/open_join_dialog"/>
</menu>

View File

@ -7,6 +7,7 @@
<string name="action_end_conversation">أغلق هذه المحادثة</string>
<string name="action_contact_details">بيانات جهة الإتصال</string>
<string name="action_muc_details">تفاصيل مجموعة المحادثة</string>
<string name="channel_details">تفاصيل القناة</string>
<string name="action_secure">تشفير المحادثة</string>
<string name="action_add_account">إضافة حساب</string>
<string name="action_edit_contact">تعديل الإسم</string>
@ -16,6 +17,8 @@
<string name="action_unblock_contact">إنهاء حجب جهة اتصال</string>
<string name="action_block_domain">حجب دومين</string>
<string name="action_unblock_domain">إنهاء حجب دومين</string>
<string name="action_block_participant">احجب المشارِك</string>
<string name="action_unblock_participant">إلغاء حجب المشارِك</string>
<string name="title_activity_manage_accounts">إدارة الحسابات</string>
<string name="title_activity_settings">إعدادات</string>
<string name="title_activity_sharewith">مشاركة مع محادثة</string>
@ -50,6 +53,7 @@
<string name="share_with">مشاركة مع</string>
<string name="start_conversation">إبداء المحادثة</string>
<string name="invite_contact">دعوة جهة إتصال</string>
<string name="invite">دعوة</string>
<string name="contacts">جهات الإتصال</string>
<string name="contact">جهة إتصال</string>
<string name="cancel">الغاء</string>
@ -143,6 +147,8 @@
<string name="account_status_tls_error">فشلت عملية التفاوض عبر TLS</string>
<string name="account_status_policy_violation">خرق للقواعد</string>
<string name="account_status_incompatible_server">لا يتوافق مع السيرفر</string>
<string name="account_status_stream_error">خطأ في التدفق</string>
<string name="account_status_stream_opening_error">خطأ عند فتح التدفق</string>
<string name="encryption_choice_unencrypted">غير مشفر</string>
<string name="encryption_choice_otr">رسالة مشفرة عبر OTR</string>
<string name="encryption_choice_pgp">رسالة مشفرة عبر OpenPGP</string>
@ -157,6 +163,8 @@
<string name="mgmt_account_are_you_sure">هل أنت متأكد ؟</string>
<string name="mgmt_account_delete_confirm_text">إذا قمت حذفت حسابك، فسوف تفقد سجل محادثاتك بالكامل</string>
<string name="attach_record_voice">تسجيل صوت</string>
<string name="account_settings_jabber_id">عنوان XMPP</string>
<string name="block_jabber_id">احجب عنوان XMPP</string>
<string name="account_settings_example_jabber_id">username@example.com</string>
<string name="password">كلمة السر</string>
<string name="error_out_of_memory">خارج الذاكرة. الصورة كبيرة جدا</string>
@ -186,11 +194,13 @@
<string name="omemo_fingerprint">بصمة OMEMO</string>
<string name="omemo_fingerprint_x509">بصمة v\\OMEMO</string>
<string name="omemo_fingerprint_selected_message">بصمة OMEMO للرسالة</string>
<string name="omemo_fingerprint_x509_selected_message">بصمة v\\OMEMO للرسالة</string>
<string name="other_devices">أجهزة أخرى</string>
<string name="trust_omemo_fingerprints">الثقة في بصمات أوميمو OMEMO</string>
<string name="fetching_keys">جارإحضار المفاتيح ...</string>
<string name="done">تم</string>
<string name="decrypt">فك الشيفرة</string>
<string name="bookmarks">الفواصل المرجعية</string>
<string name="search">بحث</string>
<string name="enter_contact">قم بإدخال جهة إتصال</string>
<string name="delete_contact">حذف جهة الإتصال</string>
@ -200,11 +210,18 @@
<string name="create">أضف</string>
<string name="select">إختر</string>
<string name="contact_already_exists">جهة الاتصال موجودة لديك مسبقا</string>
<string name="join">دخول</string>
<string name="join">التحق</string>
<string name="channel_full_jid_example">channel@conference.example.com/nick</string>
<string name="channel_bare_jid_example">channel@conference.example.com</string>
<string name="save_as_bookmark">حفظ بالمفضلة</string>
<string name="delete_bookmark">إحذف من المفضلة</string>
<string name="destroy_room">دمر فريق المحادثة</string>
<string name="destroy_channel">دمر القناة</string>
<string name="could_not_destroy_room">لم نتمكن مِن تدمير فريق المحادثة</string>
<string name="could_not_destroy_channel">لم نتمكن مِن تدمير القناة</string>
<string name="bookmark_already_exists">موجوده بالمفضلة سابقا</string>
<string name="action_edit_subject">تعديل موضوع مجموعة المحادثة</string>
<string name="topic">الموضوع</string>
<string name="joining_conference">في صدد الإنظمام إلى مجموعة المحادثة ...</string>
<string name="leave">غادر</string>
<string name="contact_added_you">جهة اتصال أضافتك </string>
@ -237,6 +254,7 @@
<string name="pref_allow_message_correction_summary">السماح لمراسليك بتعديل رسائلهم</string>
<string name="pref_expert_options">إعدادات متقدمة</string>
<string name="pref_expert_options_summary">كن حذراً مع هذه من فضلك</string>
<string name="title_activity_about_x">عن %s</string>
<string name="title_pref_quiet_hours">ساعات السكون</string>
<string name="title_pref_quiet_hours_start_time">وقت البداية</string>
<string name="title_pref_quiet_hours_end_time">وقت النهاية</string>
@ -244,6 +262,7 @@
<string name="pref_quiet_hours_summary">سوف تكتم التنبيهات إبان ساعات السكون</string>
<string name="pref_use_indicate_received">طلب تقارير تسليم الرسائل</string>
<string name="pref_expert_options_other">أخرى</string>
<string name="pref_autojoin">زامِن مع الفواصل المرجعية</string>
<string name="toast_message_omemo_fingerprint">تم نسخ بصمة OMEMO إلى الحافظة !</string>
<string name="conference_banned">حسابك محظور للإلتحاق بمجموعة المحادثة هذه</string>
<string name="conference_members_only">هذه المجموعة متاحة للأعضاء المنتمين إليها فقط</string>
@ -253,11 +272,16 @@
<string name="using_account">أنت تستعمل حساب %s</string>
<string name="not_connected_try_again">انقطع الإتصال .. حاول مرة أخرى</string>
<string name="check_x_filesize">تحقق من حجم %s</string>
<string name="check_x_filesize_on_host">تحقق مِن حجم %1$s على %2$s</string>
<string name="message_options">خيارات الرسالة</string>
<string name="quote">إقتبس</string>
<string name="paste_as_quote">ألصقه كاقتباس</string>
<string name="copy_original_url">أنسخ الرابط الأصلي</string>
<string name="send_again">أعد الإرسال</string>
<string name="file_url">رابط الملف</string>
<string name="jabber_id_copied_to_clipboard">تم نسخ عنوان الـ XMPP إلى الحافظة</string>
<string name="error_message_copied_to_clipboard">تم نسخ رسالة الخطأ إلى الحافظة</string>
<string name="web_address">عنوان الويب</string>
<string name="scan_qr_code">إمسح شفرة التّعرّف 2D</string>
<string name="show_qr_code">أظهر شفرة التّعرّف 2D</string>
<string name="show_block_list">إعرض قائمة المحبوسين</string>
@ -266,6 +290,10 @@
<string name="try_again">حاول مرة أخرى</string>
<string name="pref_keep_foreground_service">احتفظ بالتطبيق يعمل في المقدمة</string>
<string name="pref_keep_foreground_service_summary">منع نظام التشغيل من انهاء اتصالك</string>
<string name="pref_create_backup">أنشئ نسخة احتياطية</string>
<string name="notification_backup_created_title">تم إنشاء نسختك الاحتياطية</string>
<string name="notification_restored_backup_title">تم استرجاع نسختك الاحتياطية</string>
<string name="notification_restored_backup_subtitle">لا تنسى تنشيط الحساب.</string>
<string name="choose_file">اختيار ملف</string>
<string name="receiving_x_file">اكتمل الإستلام %1$s (%2$d%% بنسبة)</string>
<string name="download_x_file">تنزيل %s</string>
@ -280,8 +308,10 @@
<string name="file_deleted">تم حذف الملف</string>
<string name="no_application_found_to_open_file">لا يوجد تطبيق متاح لعرض الملف</string>
<string name="no_application_found_to_open_link">تعذر العثور على تطبيق يمكنه فتح الرابط</string>
<string name="pref_show_dynamic_tags">وسوم ديناميكية</string>
<string name="pref_show_dynamic_tags_summary">عرض علامات للقراءة فقط أسفل بيانات جهات الإتصال </string>
<string name="enable_notifications">تفعيل الإشعارات</string>
<string name="no_conference_server_found">لم يُعثر على أي خادم للمحادثات الجماعية</string>
<string name="conference_creation_failed">فشلت عملية إنشاء مجموعة المحادثة !</string>
<string name="account_image_description">الصورة الرمزية للحساب</string>
<string name="copy_omemo_clipboard_description">انسخ بصمة OMEMO إلى الحافظة</string>
@ -308,11 +338,14 @@
<string name="grant_admin_privileges">منح امتيازات الإداره</string>
<string name="remove_admin_privileges">إلغاء امتيازات الإدارة</string>
<string name="remove_from_room">التنحية من مجموعة المحادثة</string>
<string name="remove_from_channel">أزله مِن القناة</string>
<string name="could_not_change_affiliation">لا يمكن تغيير انتساب %s</string>
<string name="ban_from_conference">الحظر من دخول مجموعة المحادثة</string>
<string name="ban_now">حظر الآن</string>
<string name="could_not_change_role">لا يمكن تغيير دول %s</string>
<string name="channel_options">إعدادات القناة العمومية</string>
<string name="members_only">سرِّي ، للأعضاء فقط</string>
<string name="moderated">اجعل القناة تحت الإشراف</string>
<string name="you_are_not_participating">لست مشتركا في المجموعة</string>
<string name="modified_conference_options">تم تعديل خيارات فريق المحادثة !</string>
<string name="could_not_modify_conference_options">تعذر تغيير خيارات فريق المحادثة</string>
@ -359,6 +392,7 @@
<string name="recently_used">التي تم استعمالها كثيرا مؤخرا</string>
<string name="choose_quick_action">إختر حركة سريعة</string>
<string name="search_contacts">البحث في جهات الإتصال</string>
<string name="search_bookmarks">البحث في الفواصل المرجعية</string>
<string name="send_private_message">إبعث برسالة على الخاص</string>
<string name="user_has_left_conference">لقد غادَر %1$s فريق المحادثة !</string>
<string name="username">إسم المستخدم</string>
@ -413,6 +447,7 @@
<string name="notify_never">تعطيل الإخطارات</string>
<string name="notify_paused">الإشعارات موقفة</string>
<string name="pref_picture_compression">ضغط الصورة</string>
<string name="pref_picture_compression_summary">تغيير حجم الصور وضغطها</string>
<string name="always">دائماً</string>
<string name="automatically">آليا</string>
<string name="battery_optimizations_enabled">وضع تحسين أداء البطارية مفعّل</string>
@ -426,6 +461,8 @@
<string name="security_error_invalid_file_access">خطأ في الأمان : نفاذ غير سليم إلى ملف</string>
<string name="no_application_to_share_uri">تعذر العثور على تطبيق يُمكنُ بواسطته مشاركة الرابط</string>
<string name="share_uri_with">شارك الرابط مع ...</string>
<string name="agree_and_continue">وافق ثم واصل</string>
<string name="your_full_jid_will_be">عنوان XMPP الخاص بك سيكون: %s</string>
<string name="create_account">إنشاء حساب</string>
<string name="use_own_provider">إستخدم مزودي الخاص</string>
<string name="pick_your_username">إختر إسم المستخدم</string>
@ -442,6 +479,7 @@
<string name="choose_participants">إختر المشاركين</string>
<string name="creating_conference">جارٍ إنشاء مجموعة المحادثة ...</string>
<string name="invite_again">أعد إرسال الدعوة</string>
<string name="gp_disable">تعطيل</string>
<string name="gp_short">قصير</string>
<string name="gp_medium">متوسط</string>
<string name="gp_long">طويل</string>
@ -558,9 +596,11 @@
<string name="account_status_regis_web">لا يمكن تسجيل حسابات على هذا الخادوم إلا عبر موقع الويب</string>
<string name="open_website">فتح موقع الإنترنت</string>
<string name="application_found_to_open_website">تعذر العثور على تطبيق يُمكنه فتح موقع الويب</string>
<string name="pref_headsup_notifications_summary">أظهر الإشعارات العلوية</string>
<string name="today">اليوم</string>
<string name="yesterday">البارحة</string>
<string name="pref_validate_hostname">التحقق من صحة إسم المضيف بواسطة DNSSEC</string>
<string name="certificate_does_not_contain_jid">الشهادة لا تحتوي على عنوان XMPP</string>
<string name="server_info_partial">جُزْئِيًّا</string>
<string name="attach_record_video">تسجيل فيديو</string>
<string name="copy_to_clipboard">النسخ إلى الحافظة</string>
@ -574,11 +614,18 @@
<string name="mtm_cert_details">تفاصيل الشهادة :</string>
<string name="once">مرة واحدة</string>
<string name="pref_scroll_to_bottom">السحب إلى أسفل</string>
<string name="pref_scroll_to_bottom_summary">التمرير إلى أسفل بعد إرسال رسالة</string>
<string name="edit_status_message_title">تعديل رسالة حالة الحضور</string>
<string name="edit_status_message">تعديل رسالة حالة الحضور</string>
<string name="disable_encryption">تعطيل التعمية</string>
<string name="error_trustkey_device_list">تعذر جلب قائمة الأجهزة</string>
<string name="disable_now">تعطيله حالًا</string>
<string name="draft">المسودة:</string>
<string name="pref_omemo_setting">التعمية بـ OMEMO</string>
<string name="pref_omemo_setting_summary_default_on">سوف يُستخدَم OMEMO افتراضيا في المحادثات الجديدة.</string>
<string name="pref_font_size">حجم الخط</string>
<string name="default_on">نشِط مبدئيًا</string>
<string name="default_off">معطل مبدئيًا</string>
<string name="small">صغير</string>
<string name="medium">متوسط</string>
<string name="large">كبير</string>
@ -590,16 +637,28 @@
<string name="title_activity_share_location">مشاركة الموقع</string>
<string name="title_activity_show_location">إظهار الموقع</string>
<string name="share">مشاركة</string>
<string name="please_wait">يرجى الانتظار…</string>
<string name="search_messages">البحث عن رسائل</string>
<string name="view_conversation">مشاهدة المحادثة</string>
<string name="copy_link">نسخ العنوان الإلكتروني</string>
<string name="copy_jabber_id">انسخ عنوان الـ XMPP</string>
<string name="pref_start_search">بحث مباشر</string>
<string name="contact_name">إسم جهة الإتصال</string>
<string name="nickname">إسم مستعار</string>
<string name="group_chat_name">إسم</string>
<string name="providing_a_name_is_optional">إدخال الاسم اختياري</string>
<string name="create_dialog_group_chat_name">اسم فريق المحادثة</string>
<string name="unable_to_save_recording">لا يمكن حفظ التسجيل</string>
<string name="foreground_service_channel_name">الخدمة الأمامية</string>
<string name="notification_group_status_information">معلومات عن الحالة</string>
<string name="error_channel_name">مشاكل إتّصال</string>
<string name="notification_group_messages">رسائل</string>
<string name="messages_channel_name">رسائل</string>
<string name="pref_more_notification_settings">إعدادات الإشعار</string>
<string name="video_compression_channel_name">ضغط الفيديو</string>
<string name="view_media">اعرض الوسائط</string>
<string name="view_users">اعرض المشارِكين</string>
<string name="group_chat_members">المشارِكون</string>
<string name="pref_video_compression">جودة الفيديو</string>
<string name="video_360p">متوسط (360ب)</string>
<string name="video_720p">عالي (720ب)</string>
@ -607,10 +666,22 @@
<string name="choose_a_country">إختار الدولة</string>
<string name="phone_number">رقم هاتف</string>
<string name="verify_your_phone_number">تحقق من رقم هاتفك</string>
<string name="not_a_valid_phone_number">ليس %s برقم هاتف صالح.</string>
<string name="please_enter_your_phone_number">يرجى إدخال رقم هاتفك.</string>
<string name="search_countries">البحث عن الدول</string>
<string name="verify_x">تحقق مِن %s</string>
<string name="resend_sms">إعادة إرسال الإرسالية القصيرة</string>
<string name="wait_x">يرجى الانتظار (%s)</string>
<string name="back">رجوع</string>
<string name="yes">نعم</string>
<string name="no">لا</string>
<string name="verifying">جارٍ التحقق…</string>
<string name="requesting_sms">جارٍ طلب الرسالة النصية القصيرة…</string>
<string name="unknown_api_error_network">خطأ شبكي مجهول.</string>
<string name="unable_to_connect_to_server">تعذر الربط بالخادم.</string>
<string name="no_network_connection">ليس هناك اتصال بالشبكة.</string>
<string name="try_again_in_x">يرجى إعادة المحاولة في غضون %s</string>
<string name="update">تحديث</string>
<string name="your_name">إسمك</string>
<string name="enter_your_name">أدخل إسمك</string>
<string name="no_name_set_instructions">إضغط على زرّ التعديل لضبط إسمك</string>
@ -622,4 +693,31 @@
<string name="open_with">إفتح بـ...</string>
<string name="set_profile_picture">صورة حساب كونفرسايشنز</string>
<string name="choose_account">إختيار الحساب</string>
<string name="restore_backup">استرجِع نسخة احتياطية</string>
<string name="restore">استرجِع</string>
<string name="enter_jabber_id">ادخِل عنوان XMPP</string>
<string name="create_group_chat">أنشئ فريق محادثة</string>
<string name="join_public_channel">إلتحِق بقناة عمومية</string>
<string name="create_private_group_chat">أنشئ فريق محادثة خاص</string>
<string name="create_public_channel">أنشئ قناة عمومية</string>
<string name="create_dialog_channel_name">اسم القناة</string>
<string name="xmpp_address">عنوان XMPP</string>
<string name="please_enter_name">يرجى إدخال اسمٍ للقناة</string>
<string name="creating_channel">جارٍ إنشاء القناة العمومية…</string>
<string name="joined_an_existing_channel">لقد التحقت بقناة موجودة سابقا</string>
<string name="allow_participants_to_invite_others">اسمح لأي كان دعوة الآخرين</string>
<string name="owners_can_edit_subject">يمكن للمالِكين تعديل الموضوع.</string>
<string name="manage_permission">إدارة الصلاحيات</string>
<string name="search_participants">البحث عن مشارِكين</string>
<string name="file_too_large">حجم الملف كبير جدًا</string>
<string name="attach">أرفِق</string>
<string name="discover_channels">استكشاف القنوات</string>
<string name="search_channels">البحث عن قنوات</string>
<string name="i_already_have_an_account">لدي حساب</string>
<string name="add_existing_account">إضافة حساب موجود</string>
<string name="register_new_account">تسجيل حساب جديد</string>
<string name="add_anway">أضفه على أي حال</string>
<string name="event">حَدَث</string>
<string name="open_backup">افتح النسخة الاحتياطية</string>
<string name="please_enter_password">يرجى إدخال الكلمة السرية للحساب</string>
</resources>

View File

@ -874,4 +874,5 @@
<string name="account_already_setup">Dieses Konto wurde bereits eingerichtet</string>
<string name="please_enter_password">Bitte gib das Passwort für dieses Konto ein</string>
<string name="unable_to_perform_this_action">Diese Aktion kann nicht ausgeführt werden</string>
<string name="open_join_dialog">Öffentlichen Channel beitreten...</string>
</resources>

View File

@ -874,4 +874,5 @@
<string name="account_already_setup">Esta conta xa foi configurada</string>
<string name="please_enter_password">Introduza o contrasinal de esta conta</string>
<string name="unable_to_perform_this_action">Non se puido completar a acción</string>
<string name="open_join_dialog">Unirse a canle pública...</string>
</resources>

View File

@ -30,6 +30,7 @@
<string name="just_now">zojuist</string>
<string name="minute_ago">1 min. geleden</string>
<string name="minutes_ago">%d min. geleden</string>
<string name="x_unread_conversations">%d ongelezen gesprekken</string>
<string name="sending">versturen…</string>
<string name="message_decrypting">Bericht aan het ontsleutelen. Even geduld…</string>
<string name="pgp_message">OpenPGP-versleuteld bericht</string>

View File

@ -888,8 +888,9 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż
<string name="conversations_backup">Kopia zapasowa Conversations</string>
<string name="event">Zdarzenie</string>
<string name="open_backup">Otwórz kopię zapasową</string>
<string name="not_a_backup_file">Plik który otworzyłeś nie jest plikiem kopii zapasowej Conversations</string>
<string name="not_a_backup_file">Plik, który otworzyłeś, nie jest plikiem kopii zapasowej Conversations</string>
<string name="account_already_setup">To konto zostało już ustawione</string>
<string name="please_enter_password">Proszę podać hasło dla tego konta</string>
<string name="unable_to_perform_this_action">Nie można wykonać tej akcji</string>
<string name="open_join_dialog">Dołącz do publicznego kanału...</string>
</resources>

View File

@ -413,7 +413,7 @@
<string name="sending_x_file">Trimit %s</string>
<string name="offering_x_file">Ofer %s</string>
<string name="hide_offline">Ascunde deconectat</string>
<string name="contact_is_typing">%s tasteaza...</string>
<string name="contact_is_typing">%s tastează...</string>
<string name="contact_has_stopped_typing">%s s-a oprit din scris</string>
<string name="contacts_are_typing">%s tastează...</string>
<string name="contacts_have_stopped_typing">%s s-au oprit din scris</string>
@ -884,4 +884,5 @@ sau chiar pierderea mesajelor.\nÎn continuare veți fi rugați să dezactivați
<string name="account_already_setup">Acest cont a fost deja configurat</string>
<string name="please_enter_password">Va rugăm să introduceți parola pentru acest cont</string>
<string name="unable_to_perform_this_action">Nu se poate realiza această acțiune</string>
<string name="open_join_dialog">Alătură-te unui canal public...</string>
</resources>

View File

@ -3,7 +3,10 @@
<string name="action_settings">Inställningar</string>
<string name="action_add">Ny konversation</string>
<string name="action_accounts">Kontoinställningar</string>
<string name="action_end_conversation">Stäng denna konversation</string>
<string name="action_contact_details">Kontaktdetaljer</string>
<string name="action_muc_details">Gruppchattdetaljer</string>
<string name="channel_details">Kanaldetaljer</string>
<string name="action_secure">Säker konversation</string>
<string name="action_add_account">Lägg till konto</string>
<string name="action_edit_contact">Ändra namn</string>
@ -13,18 +16,25 @@
<string name="action_unblock_contact">Avblockera kontakt</string>
<string name="action_block_domain">Blockera domän</string>
<string name="action_unblock_domain">Avblockera domän</string>
<string name="action_block_participant">Blockera deltagare</string>
<string name="action_unblock_participant">Avblockera deltagare</string>
<string name="title_activity_manage_accounts">Hantera konton</string>
<string name="title_activity_settings">Inställningar</string>
<string name="title_activity_sharewith">Dela med konversation</string>
<string name="title_activity_start_conversation">Starta konversation</string>
<string name="title_activity_choose_contact">Välj kontakt</string>
<string name="title_activity_choose_contacts">Välj kontakter</string>
<string name="title_activity_share_via_account">Dela via konto</string>
<string name="title_activity_block_list">Blockeringslista</string>
<string name="just_now">just nu</string>
<string name="minute_ago">1 min sedan</string>
<string name="minutes_ago">%d min sedan</string>
<string name="x_unread_conversations">%d olästa konversationer</string>
<string name="sending">skickar…</string>
<string name="message_decrypting">Avkrypterar meddelande. Vänta…</string>
<string name="pgp_message">OpenPGP-krypterat meddelande</string>
<string name="nick_in_use">Nick används redan</string>
<string name="invalid_muc_nick">Ogiltigt nick</string>
<string name="admin">Admin</string>
<string name="owner">Ägare</string>
<string name="moderator">Moderator</string>
@ -36,11 +46,16 @@
<string name="block_domain_text">Blockera alla kontakter från %s?</string>
<string name="unblock_domain_text">Avblockera alla kontakter från %s?</string>
<string name="contact_blocked">Kontakt blockerad</string>
<string name="blocked">Blockerad</string>
<string name="remove_bookmark_text">Vill du ta bort %s som bokmärke? Konversationer associerade med detta bokmärke kommer inte tas bort.</string>
<string name="register_account">Registrera nytt konto på servern</string>
<string name="change_password_on_server">Byt lösenord på server</string>
<string name="share_with">Dela med…</string>
<string name="start_conversation">Börja konversation</string>
<string name="invite_contact">Bjud in kontakt</string>
<string name="invite">Bjud in</string>
<string name="contacts">Kontakter</string>
<string name="contact">Kontakt</string>
<string name="cancel">Avbryt</string>
<string name="set">Sätt</string>
<string name="add">Lägg till</string>
@ -66,6 +81,9 @@
<string name="sharing_files_please_wait">Delar filer. Vänta...</string>
<string name="action_clear_history">Rensa historik</string>
<string name="clear_conversation_history">Rensa konversationshistorik</string>
<string name="clear_histor_msg">Är du säker på att du vill ta bort alla meddelanden i denna konversation?\n\n<b>Varning:</b> Detta kommer inte att ta bort kopior av dessa meddelanden på andra enheter eller servrar.</string>
<string name="delete_file_dialog">Ta bort fil</string>
<string name="also_end_conversation">Stäng denna konversation efteråt</string>
<string name="choose_presence">Välj enhet</string>
<string name="send_unencrypted_message">Skicka okrypterat meddelande</string>
<string name="send_message">Skicka meddelande</string>
@ -106,8 +124,10 @@
<string name="pref_confirm_messages">Bekräfta meddelanden</string>
<string name="pref_confirm_messages_summary">Låt dina kontakter veta när du har mottagit och läst deras meddelanden</string>
<string name="pref_ui_options">Gränssnitt</string>
<string name="bad_key_for_encryption">Dålig krypterings-nyckel.</string>
<string name="accept">Acceptera</string>
<string name="error">Ett fel har inträffat</string>
<string name="recording_error">Fel</string>
<string name="your_account">Ditt konto</string>
<string name="send_presence_updates">Skicka tillgänglighetsuppdatering</string>
<string name="receive_presence_updates">Ta emot tillgänglighetsuppdateringar</string>
@ -150,8 +170,11 @@
<string name="mgmt_account_are_you_sure">Är du säker?</string>
<string name="mgmt_account_delete_confirm_text">Om du tar bort ditt konto kommer hela konversationshistoriken att försvinna</string>
<string name="attach_record_voice">Spela in röst</string>
<string name="account_settings_jabber_id">XMPP-adress</string>
<string name="block_jabber_id">Blockera XMPP-adress</string>
<string name="account_settings_example_jabber_id">användarnamn@exempel.se</string>
<string name="password">Lösenord</string>
<string name="invalid_jid">Detta är inte en giltig XMPP-adress</string>
<string name="error_out_of_memory">Slut på minne. Bilden är för stor</string>
<string name="add_phone_book_text">Vill du lägga till %s i din enhets kontakter?</string>
<string name="server_info_show_more">Server-info</string>
@ -186,8 +209,10 @@
<string name="fetching_keys">Hämtar nycklar...</string>
<string name="done">Klar</string>
<string name="decrypt">Avkryptera</string>
<string name="bookmarks">Bokmärken</string>
<string name="search">Sök</string>
<string name="enter_contact">Fyll i kontakt</string>
<string name="delete_contact">Ta bort kontakt</string>
<string name="view_contact_details">Se kontaktdetaljer</string>
<string name="block_contact">Blockera kontakt</string>
<string name="unblock_contact">Avblockera kontakt</string>
@ -198,10 +223,13 @@
<string name="save_as_bookmark">Spara som bokmärke</string>
<string name="delete_bookmark">Ta bort bokmärke</string>
<string name="bookmark_already_exists">Detta bokmärke finns redan</string>
<string name="topic">Ämne</string>
<string name="joining_conference">Går med i gruppchatt...</string>
<string name="leave">Lämna</string>
<string name="contact_added_you">Kontakten lade till dig i sin kontaktlista</string>
<string name="add_back">Addera tillbaka</string>
<string name="contact_has_read_up_to_this_point">%s har läst hit</string>
<string name="everyone_has_read_up_to_this_point">Alla har läst fram till hit</string>
<string name="publish">Publicera</string>
<string name="touch_to_choose_picture">Tryck på avatarbild för att välja en bild från bildgalleriet</string>
<string name="publishing">Publicerar…</string>
@ -220,6 +248,7 @@
<string name="skip">Hoppa över</string>
<string name="disable_notifications">Inaktivera notifieringar</string>
<string name="enable">Aktivera</string>
<string name="conference_requires_password">Gruppchatten kräver lösenord</string>
<string name="enter_password">Fyll i lösenord</string>
<string name="request_presence_updates">Begär tillgänglighetsuppdateringar från din kontakt först.\n\n<small>Detta används för att se vilken klient/klienter din kontakt använder.</small></string>
<string name="request_now">Begär nu</string>
@ -230,6 +259,7 @@
<string name="pref_allow_message_correction_summary">Tillåt att dina kontakter kan ändra sina meddelanden i efterhand</string>
<string name="pref_expert_options">Expertinställningar</string>
<string name="pref_expert_options_summary">Var försiktig med dem</string>
<string name="title_activity_about_x">Om %s</string>
<string name="title_pref_quiet_hours">Tysta timmar</string>
<string name="title_pref_quiet_hours_start_time">Starttid</string>
<string name="title_pref_quiet_hours_end_time">Sluttid</string>
@ -240,7 +270,10 @@
<string name="pref_use_indicate_received_summary">Mottagna meddelanden markeras med en grön bock om det stöds</string>
<string name="pref_use_send_button_to_indicate_status_summary">Färglägg skickaknappen för att indikera kontaktens status</string>
<string name="pref_expert_options_other">Annat</string>
<string name="pref_autojoin">Synkronisera med bokmärken</string>
<string name="toast_message_omemo_fingerprint">OMEMO-fingeravtryck har kopierats till urklipp!</string>
<string name="conference_resource_constraint">Resursbegränsning</string>
<string name="conference_shutdown">Gruppchatten stängdes ner</string>
<string name="using_account">använder konto %s</string>
<string name="checking_x">Kontrollerar %s på webbserver</string>
<string name="not_connected_try_again">Du är inte ansluten. Försök igen senare</string>
@ -248,9 +281,14 @@
<string name="check_x_filesize_on_host">Kontrollera filstorlek för %1$s på %2$s</string>
<string name="message_options">Meddelandealternativ</string>
<string name="quote">Citera</string>
<string name="paste_as_quote">Klistra in som citat</string>
<string name="copy_original_url">Kopiera orginal-URL</string>
<string name="send_again">Skicka igen</string>
<string name="file_url">Fil-URL</string>
<string name="url_copied_to_clipboard">Kopierade URL till urklipp</string>
<string name="jabber_id_copied_to_clipboard">Kopierade XMPP-adress till urklipp</string>
<string name="error_message_copied_to_clipboard">Kopierade felmeddelande till urklipp</string>
<string name="web_address">webbadress</string>
<string name="scan_qr_code">Scanna 2D-streckkod</string>
<string name="show_qr_code">Visa 2D-streckkod</string>
<string name="show_block_list">Visa blockeringslista</string>
@ -259,6 +297,14 @@
<string name="try_again">Försök igen</string>
<string name="pref_keep_foreground_service">Håll tjänst i förgrunden</string>
<string name="pref_keep_foreground_service_summary">Förehindrar operativsystemet att ta ner uppkopplingen</string>
<string name="pref_create_backup">Skapa säkerhetskopia</string>
<string name="pref_create_backup_summary">Säkerhetskopians filer lagras i %s</string>
<string name="notification_create_backup_title">Skapar filer för säkerhetskopia</string>
<string name="notification_backup_created_title">Din säkerhetskopia har skapats</string>
<string name="notification_backup_created_subtitle">Säkerhetskopians filer har lagrats i %s</string>
<string name="restoring_backup">Återställer säkerhetskopia</string>
<string name="notification_restored_backup_title">Din säkerhetskopia har återställts</string>
<string name="notification_restored_backup_subtitle">Glöm inte att aktivera kontot.</string>
<string name="choose_file">Välj fil</string>
<string name="receiving_x_file">Tar emot %1$s (%2$d%% klart)</string>
<string name="download_x_file">Ladda ner %s</string>
@ -272,8 +318,11 @@
<string name="file_transmission_failed">filöverföring lyckades inte</string>
<string name="file_deleted">Filen har blivit borttagen</string>
<string name="no_application_found_to_open_file">Ingen applikation kunde hittas för att öppna filen</string>
<string name="no_application_found_to_open_link">Ingen applikation kunde hittas för att öppna länken</string>
<string name="no_application_found_to_view_contact">Ingen applikation kunde hittas för att visa kontakten</string>
<string name="pref_show_dynamic_tags_summary">Visa skrivskyddade taggar under kontakter</string>
<string name="enable_notifications">Aktivera notifieringar</string>
<string name="conference_creation_failed">Misslyckades skapa gruppchatt!</string>
<string name="account_image_description">Kontots avatarbild</string>
<string name="copy_omemo_clipboard_description">Kopiera OMEMO-fingeravtryck till urklipp</string>
<string name="regenerate_omemo_key">Regenerera OMEMO-nyckel</string>
@ -298,6 +347,8 @@
<string name="advanced_mode">Avancerat läge</string>
<string name="grant_admin_privileges">Bevilja administratörsbehörighet</string>
<string name="remove_admin_privileges">Återkalla administratörsbehörighet</string>
<string name="remove_from_room">Ta bort från gruppchatt</string>
<string name="remove_from_channel">Ta bort från kanal</string>
<string name="could_not_change_affiliation">Kunde inte ändra tillhörigheten för %s</string>
<string name="ban_now">Bannlys nu</string>
<string name="could_not_change_role">Kunde inte ändra rollen för %s</string>
@ -349,6 +400,7 @@
<string name="recently_used">Senast använd</string>
<string name="choose_quick_action">Välj snabbfunktion</string>
<string name="send_private_message">Skicka privat meddelande</string>
<string name="user_has_left_conference">%1$s har lämnat gruppchatten!</string>
<string name="username">Användarnamn</string>
<string name="username_hint">Användarnamn</string>
<string name="invalid_username">Inte ett giltigt användanamn</string>
@ -360,8 +412,10 @@
<string name="account_status_bind_failure">Bind-fel</string>
<string name="account_status_host_unknown">Servern är inte ansvarig för domänen</string>
<string name="server_info_broken">Sönder</string>
<string name="pref_presence_settings">Tillgänglighet</string>
<string name="pref_away_when_screen_off">Status borta när skärmen är av</string>
<string name="pref_away_when_screen_off_summary">Sätter din tillgänglighet till borta när skrämen är av</string>
<string name="pref_dnd_on_silent_mode">\"Stör ej\" i tyst läge</string>
<string name="pref_treat_vibrate_as_silent">Hantera vibrationsläge som tyst läge</string>
<string name="pref_show_connection_options">Utökade anslutningsinställningar</string>
<string name="pref_show_connection_options_summary">Visa val av servernamn och port vid inställning av konto</string>
@ -398,10 +452,14 @@
<string name="shared_image_with_x">Delade bild med %s</string>
<string name="shared_images_with_x">Delade bilder med %s</string>
<string name="shared_text_with_x">Delade text med %s</string>
<string name="no_storage_permission">Conversations behöver tillgång till extern lagring</string>
<string name="no_camera_permission">Conversations behöver tillgång till kameran</string>
<string name="sync_with_contacts">Synkronisera med kontakter</string>
<string name="notify_on_all_messages">Notifiera för alla meddelanden</string>
<string name="notify_never">Notifieringar deaktiverade</string>
<string name="notify_paused">Notifieringar pausade</string>
<string name="pref_picture_compression">Bildkomprimering</string>
<string name="pref_picture_compression_summary">Ändra storlek på och komprimera bilder</string>
<string name="always">Alltid</string>
<string name="automatically">Automatiskt</string>
<string name="battery_optimizations_enabled">Batterioptimeringar aktiverade</string>
@ -417,6 +475,8 @@
<string name="security_error_invalid_file_access">Säkerhetsfel: Ogiltig filaccess</string>
<string name="no_application_to_share_uri">Ingen applikation kunde hittas för att dela URI</string>
<string name="share_uri_with">Dela URI med...</string>
<string name="agree_and_continue">Godkänn &amp; fortsätt</string>
<string name="your_full_jid_will_be">Din fullständiga XMPP-adress kommer att vara: %s</string>
<string name="create_account">Skapa konto</string>
<string name="use_own_provider">Använd min egen leverantör</string>
<string name="pick_your_username">Välj användarnamn</string>
@ -431,6 +491,7 @@
<string name="registration_please_wait">Registreringfel: Försök igen senare</string>
<string name="registration_password_too_weak">Registreringsfel: Lösenordet är för svagt</string>
<string name="choose_participants">Välj deltagare</string>
<string name="creating_conference">Skapar gruppchatt...</string>
<string name="invite_again">Bjud in igen</string>
<string name="gp_short">Kort</string>
<string name="gp_medium">Medium</string>
@ -494,10 +555,35 @@
<string name="verify_omemo_keys">Verifiera OMEMO-nycklar</string>
<string name="distrust_omemo_key">Lita ej på enhet</string>
<string name="distrust_omemo_key_text">Är du säker på att du vill ta bort verifieringen av denna enhet?\nDenna enhet och meddelanden som kommer från enheten kommer att markeras som ej pålitliga.</string>
<plurals name="seconds">
<item quantity="one">%d sekund</item>
<item quantity="other">%d sekunder</item>
</plurals>
<plurals name="minutes">
<item quantity="one">%d minut</item>
<item quantity="other">%d minuter</item>
</plurals>
<plurals name="hours">
<item quantity="one">%d timme</item>
<item quantity="other">%d timmar</item>
</plurals>
<plurals name="days">
<item quantity="one">%d dag</item>
<item quantity="other">%d dagar</item>
</plurals>
<plurals name="weeks">
<item quantity="one">%d vecka</item>
<item quantity="other">%d veckor</item>
</plurals>
<plurals name="months">
<item quantity="one">%d månad</item>
<item quantity="other">%d månader</item>
</plurals>
<string name="pref_automatically_delete_messages">Automatisk borttagning av meddelanden</string>
<string name="pref_automatically_delete_messages_description">Ta automatiskt bort meddelanden från denna enhet som är äldre än den konfigurerade tidsramen.</string>
<string name="encrypting_message">Krypterar meddelande</string>
<string name="not_fetching_history_retention_period">Hämtar inte meddelanden på grund av inställningen för borttagning av gamla meddelanden.</string>
<string name="transcoding_video">Komprimerar video</string>
<string name="corresponding_conversations_closed">Motsvarande konversationer är stängda.</string>
<string name="contact_blocked_past_tense">Kontakt blockerad.</string>
<string name="pref_notifications_from_strangers">Notifieringar från främlingar</string>
@ -508,4 +594,29 @@
<string name="online_right_now">online just nu</string>
<string name="retry_decryption">Försök dekryptera igen</string>
<string name="session_failure">Sessionsfel</string>
<string name="today">Idag</string>
<string name="yesterday">Igår</string>
<string name="certificate_does_not_contain_jid">Certifikatet innehåller ej en XMPP-adress</string>
<string name="attach_record_video">Spela in video</string>
<string name="message">Meddelande</string>
<string name="mtm_accept_cert">Godkänn okänt certifikat?</string>
<string name="draft">Utkast:</string>
<string name="title_activity_share_location">Dela plats</string>
<string name="title_activity_show_location">Visa plats</string>
<string name="share">Dela</string>
<string name="copy_jabber_id">Kopiera XMPP-adress</string>
<string name="create_dialog_group_chat_name">Gruppchattens namn</string>
<string name="group_chat_members">Deltagare</string>
<string name="choose_a_country">Välj ett land</string>
<string name="phone_number">telefonnummer</string>
<string name="verify_your_phone_number">Bekräfta ditt telefonnummer</string>
<string name="create_group_chat">Skapa gruppchatt</string>
<string name="create_private_group_chat">Skapa sluten gruppchatt</string>
<string name="create_dialog_channel_name">Kanalnamn</string>
<string name="please_enter_name">Vänligen ange ett namn på kanalen</string>
<string name="channel_already_exists">Denna kanal finns redan</string>
<string name="joined_an_existing_channel">Du har gått med i en befintlig kanal</string>
<string name="no_users_hint_group_chat">Denna slutna gruppchatt har inga deltagare.</string>
<string name="discover_channels">Upptäck kanaler</string>
<string name="this_looks_like_channel">Detta ser ut som en kanaladress</string>
</resources>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="ConversationsTheme.Dark" parent="ConversationsTheme.Dark.Base">
<item name="android:navigationBarColor">@color/black</item>
</style>
</resources>

View File

@ -1,33 +1,36 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources>
<string name="action_settings">设置</string>
<string name="action_add">会话</string>
<string name="action_add">聊天</string>
<string name="action_accounts">管理账户</string>
<string name="action_account">管理账户</string>
<string name="action_end_conversation">关闭对话</string>
<string name="action_end_conversation">关闭聊天</string>
<string name="action_contact_details">联系人详情</string>
<string name="action_muc_details">群聊详情</string>
<string name="channel_details">频道详情</string>
<string name="action_secure">安全对话</string>
<string name="action_secure">安全聊天</string>
<string name="action_add_account">添加账号</string>
<string name="action_edit_contact">编辑姓名</string>
<string name="action_add_phone_book">添加到地址薄</string>
<string name="action_delete_contact">从列表中删除</string>
<string name="action_block_contact">屏蔽联系人</string>
<string name="action_unblock_contact">解除联系人屏蔽</string>
<string name="action_block_domain">屏蔽域名</string>
<string name="action_unblock_domain">解除域名屏蔽</string>
<string name="action_add_phone_book">添加到联系人</string>
<string name="action_delete_contact">从XMPP联系人中删除</string>
<string name="action_block_contact">封禁联系人</string>
<string name="action_unblock_contact">解封联系人</string>
<string name="action_block_domain">封禁域名</string>
<string name="action_unblock_domain">解封域名</string>
<string name="action_block_participant">封禁成员</string>
<string name="action_unblock_participant">解封成员</string>
<string name="title_activity_manage_accounts">管理账户</string>
<string name="title_activity_settings">设置</string>
<string name="title_activity_sharewith">分享会话</string>
<string name="title_activity_start_conversation">开始会话</string>
<string name="title_activity_sharewith">通过Conversations分享</string>
<string name="title_activity_start_conversation">开始聊天</string>
<string name="title_activity_choose_contact">选择联系人</string>
<string name="title_activity_choose_contacts">选择联系人</string>
<string name="title_activity_share_via_account">通过帐户分享</string>
<string name="title_activity_block_list">屏蔽列表</string>
<string name="title_activity_block_list">封禁列表</string>
<string name="just_now">刚刚</string>
<string name="minute_ago">1分钟前</string>
<string name="minutes_ago">%d分钟前</string>
<string name="x_unread_conversations">%d条未读消息</string>
<string name="sending">正在发送…</string>
<string name="message_decrypting">解密信息中. 请稍候…</string>
<string name="pgp_message">OpenPGP 加密的信息</string>
@ -35,19 +38,19 @@
<string name="invalid_muc_nick">无效的用户名</string>
<string name="admin">管理员</string>
<string name="owner">所有者</string>
<string name="moderator"></string>
<string name="moderator"></string>
<string name="participant">参与者</string>
<string name="visitor">访客</string>
<string name="remove_contact_text">将 %s 从列表中移除? 与该联系人的会话消息不会清除.</string>
<string name="block_contact_text">您想阻止%s向您发送消息吗?</string>
<string name="unblock_contact_text">你想解除对 %s 的屏蔽吗,他们将可以发送信息给你</string>
<string name="block_domain_text">屏蔽 %s 中的所有联系人?</string>
<string name="unblock_domain_text">除对 %s 中所有联系人的屏蔽</string>
<string name="contact_blocked">联系人已屏蔽</string>
<string name="remove_contact_text">将 %s 从XMPP联系人中移除? 与该联系人的会话消息不会清除.</string>
<string name="block_contact_text">您想封禁%s吗?</string>
<string name="unblock_contact_text">您想解封 %s吗 </string>
<string name="block_domain_text">封禁 %s 中的所有联系人?</string>
<string name="unblock_domain_text">封%s 中所有联系人</string>
<string name="contact_blocked">联系人已封禁</string>
<string name="blocked">已屏蔽</string>
<string name="remove_bookmark_text">从书签中移除 %s ?相关会话消息不会被清除 .</string>
<string name="register_account">在服务器上注册新账户</string>
<string name="change_password_on_server">在服务器上改密码</string>
<string name="change_password_on_server">在服务器上改密码</string>
<string name="share_with">分享……</string>
<string name="start_conversation">开始会话</string>
<string name="invite_contact">邀请联系人</string>
@ -379,9 +382,14 @@
<string name="could_not_change_affiliation">不能修改 %s 的从属关系</string>
<string name="ban_from_conference">屏蔽群聊</string>
<string name="ban_from_channel">从频道中屏蔽</string>
<string name="removing_from_public_conference">%s将被从公共群聊中移除。只有将此用户封禁才能将他从群聊永远移除。</string>
<string name="ban_now">现在屏蔽</string>
<string name="could_not_change_role">不能修改 %s 的角色</string>
<string name="conference_options">私密群聊设置</string>
<string name="channel_options">公开群聊设置</string>
<string name="members_only">私密,只有成员可以加入</string>
<string name="non_anonymous">使XMPP地址对所有人可见</string>
<string name="moderated">使群聊受到管理</string>
<string name="you_are_not_participating">您尚未参与</string>
<string name="modified_conference_options">群组设置修改成功!</string>
<string name="could_not_modify_conference_options">无法更改群组设置</string>
@ -416,6 +424,8 @@
<string name="no_application_found_to_display_location">无法找到显示位置的应用</string>
<string name="location">位置</string>
<string name="title_undo_swipe_out_conversation">会话已关闭</string>
<string name="title_undo_swipe_out_group_chat">离开私密群聊</string>
<string name="title_undo_swipe_out_channel">离开公开群聊</string>
<string name="pref_dont_trust_system_cas_title">不相信系统 CA</string>
<string name="pref_dont_trust_system_cas_summary">所有证书必须人工通过</string>
<string name="pref_remove_trusted_certificates_title">移除证书</string>
@ -433,6 +443,7 @@
<string name="recently_used">最近常用</string>
<string name="choose_quick_action">选择快捷操作</string>
<string name="search_contacts">搜索联系人</string>
<string name="search_bookmarks">搜索书签</string>
<string name="send_private_message">发送私密消息</string>
<string name="user_has_left_conference">%1$s 离开了群聊!</string>
<string name="username">用户名</string>
@ -466,6 +477,7 @@
<string name="captcha_required">需要验证码</string>
<string name="captcha_hint">输入上图中的文字</string>
<string name="certificate_chain_is_not_trusted">证书链不受信任</string>
<string name="jid_does_not_match_certificate">XMPP地址与证书不匹配</string>
<string name="action_renew_certificate">更新证书</string>
<string name="error_fetching_omemo_key">获取 OMEMO 密钥错误!</string>
<string name="verified_omemo_key_with_certificate">请用证书验证 OMEMO 密钥!</string>
@ -490,6 +502,8 @@
<string name="no_storage_permission">Conversations 需要外部储存权限</string>
<string name="no_camera_permission">Conversations 需要摄像头权限</string>
<string name="sync_with_contacts">同步联系人</string>
<string name="sync_with_contacts_long">将服务器端联系人与本地联系人匹配可以显示联系人的全名与头像。\n\n此应用只在本地读取并匹配联系人。\n\n现在应用将请求联系人权限。</string>
<string name="sync_with_contacts_quicksy"><![CDATA[Quicksy可以匹配您的通讯录以确定哪些人已经在使用此应用。<br><br>我们并不储存这些号码。\n\n更多信息请阅读<a href="https://quicksy.im/#privacy">隐私政策</a>。接下来将请求通讯录权限。]]></string>
<string name="notify_on_all_messages">为所有信息显示通知</string>
<string name="notify_only_when_highlighted">只在被提到时通知</string>
<string name="notify_never">禁用通知</string>
@ -514,6 +528,9 @@
<string name="security_error_invalid_file_access">安全错误:文件访问权限无效</string>
<string name="no_application_to_share_uri">未找到可以分享此链接的应用</string>
<string name="share_uri_with">分享链接……</string>
<string name="agree_and_continue">同意 &amp; 继续</string>
<string name="magic_create_text">此向导将为您在conversations.im¹上创建一个账户。\n您的联系人可以通过您的XMPP完整地址与您聊天。</string>
<string name="your_full_jid_will_be">您的XMPP完整地址将是%s</string>
<string name="create_account">创建账户</string>
<string name="use_own_provider">使用我自己的服务端</string>
<string name="pick_your_username">输入您的用户名</string>
@ -641,6 +658,7 @@
<string name="yesterday">昨天</string>
<string name="pref_validate_hostname">使用 DNSSEC 来验证主机名</string>
<string name="pref_validate_hostname_summary">包含已验证的主机名的服务器证书被认为是已验证的</string>
<string name="certificate_does_not_contain_jid">证书不包含XMPP地址</string>
<string name="server_info_partial">部分的</string>
<string name="attach_record_video">录制视频</string>
<string name="copy_to_clipboard">复制</string>
@ -682,6 +700,7 @@
<string name="medium"></string>
<string name="large"></string>
<string name="not_encrypted_for_this_device">该设备的消息未加密。</string>
<string name="omemo_decryption_failed">解密OMEMO消息失败</string>
<string name="undo">撤销</string>
<string name="location_disabled">位置分享已停用</string>
<string name="action_fix_to_location">固定位置</string>
@ -699,7 +718,9 @@
<string name="gif">GIF</string>
<string name="view_conversation">查看对话</string>
<string name="pref_use_share_location_plugin">分享位置插件</string>
<string name="pref_use_share_location_plugin_summary">不使用内置地图,使用“分享位置”插件</string>
<string name="copy_link">复制web地址</string>
<string name="copy_jabber_id">复制XMPP地址</string>
<string name="p1_s3_filetransfer">用于S3的HTTP文件共享</string>
<string name="pref_start_search">直接搜索</string>
<string name="pref_start_search_summary">在“开始对话”屏幕上打开键盘并将光标放在搜索栏中</string>
@ -726,6 +747,8 @@
<string name="pref_more_notification_settings_summary">重要性,声音,振动</string>
<string name="video_compression_channel_name">视频压缩</string>
<string name="view_media">查看媒体文件</string>
<string name="view_users">查看成员</string>
<string name="group_chat_members">成员</string>
<string name="media_browser">媒体浏览器</string>
<string name="security_violation_not_attaching_file">文件由于违反安全策略而被删除。</string>
<string name="pref_video_compression">视频质量</string>
@ -734,4 +757,115 @@
<string name="video_720p">720p</string>
<string name="cancelled">已取消</string>
<string name="already_drafting_message">你已经在起草一条消息了。</string>
<string name="feature_not_implemented">功能不支持。</string>
<string name="invalid_country_code">无效国家代码</string>
<string name="choose_a_country">选择国家</string>
<string name="phone_number">手机号</string>
<string name="verify_your_phone_number">验证手机号</string>
<string name="enter_country_code_and_phone_number">Quicksy将发送验证码短信运营商可能收费。请输入国家代码和手机号</string>
<string name="we_will_be_verifying"><![CDATA[我们将验证<br/><br/><b>%s</b><br/><br/>。电话号码正确吗?]]></string>
<string name="not_a_valid_phone_number">%s不是有效的电话号码</string>
<string name="please_enter_your_phone_number">请输入手机号。</string>
<string name="search_countries">搜索国家</string>
<string name="verify_x">验证%s</string>
<string name="we_have_sent_you_an_sms_to_x"><![CDATA[短信已发至 <b>%s</b>。]]></string>
<string name="we_have_sent_you_another_sms">已重新发送6位数验证码短信</string>
<string name="please_enter_pin_below">输入6位数的PIN</string>
<string name="resend_sms">重新发送短信</string>
<string name="resend_sms_in">重发短信(%s</string>
<string name="wait_x">请稍候(%s</string>
<string name="back">返回</string>
<string name="possible_pin">已自动从剪贴板粘贴验证码</string>
<string name="please_enter_pin">请输入6位代码</string>
<string name="abort_registration_procedure">确定放弃注册?</string>
<string name="yes"></string>
<string name="no"></string>
<string name="verifying">正在验证......</string>
<string name="requesting_sms">请求短信...</string>
<string name="incorrect_pin">验证码错误。</string>
<string name="pin_expired">验证码已失效</string>
<string name="unknown_api_error_network">未知网络错误</string>
<string name="unknown_api_error_response">未知服务器应答</string>
<string name="unable_to_connect_to_server">无法连接服务器。</string>
<string name="unable_to_establish_secure_connection">无法建立安全连接。</string>
<string name="unable_to_find_server">找不到服务器</string>
<string name="something_went_wrong_processing_your_request">处理请求时出错</string>
<string name="invalid_user_input">用户输入无效</string>
<string name="temporarily_unavailable">暂时无法连接。请稍候再试。</string>
<string name="no_network_connection">无网络连接</string>
<string name="try_again_in_x">请在%s后重试</string>
<string name="rate_limited">频率过高</string>
<string name="too_many_attempts">尝试次数过多</string>
<string name="the_app_is_out_of_date">您正在使用旧版应用。</string>
<string name="update">更新</string>
<string name="logged_in_with_another_device">此号码已在其他设备上登录。</string>
<string name="enter_your_name_instructions">请输入您的姓名。这样,对方就能知道您是谁。</string>
<string name="your_name">您的姓名</string>
<string name="enter_your_name">输入姓名</string>
<string name="no_name_set_instructions">点击编辑按钮以编辑用户名。</string>
<string name="reject_request">拒绝请求</string>
<string name="install_orbot">安装Orbot</string>
<string name="start_orbot">启动Orbot</string>
<string name="no_market_app_installed">软件商店未安装</string>
<string name="group_chat_will_make_your_jabber_id_public">此群聊将公开你的XMPP地址</string>
<string name="ebook">电子书</string>
<string name="video_original">原始(未压缩)</string>
<string name="open_with">打开方式</string>
<string name="set_profile_picture">聊天头像</string>
<string name="choose_account">选择账户</string>
<string name="restore_backup">恢复备份</string>
<string name="restore">恢复</string>
<string name="enter_password_to_restore">输入%s的密码以恢复备份</string>
<string name="restore_warning">仅在迁移或丢失原设备时恢复备份。</string>
<string name="unable_to_restore_backup">无法恢复备份。</string>
<string name="unable_to_decrypt_backup">无法解密备份。密码是否正确?</string>
<string name="backup_channel_name">备份与恢复</string>
<string name="enter_jabber_id">输入XMPP地址</string>
<string name="create_group_chat">创建群聊</string>
<string name="join_public_channel">加入公开群聊</string>
<string name="create_private_group_chat">创建私密群聊</string>
<string name="create_public_channel">创建公开群聊</string>
<string name="create_dialog_channel_name">群聊名称</string>
<string name="xmpp_address">XMPP地址</string>
<string name="please_enter_name">请为群聊提供一个名称。</string>
<string name="please_enter_xmpp_address">请提供XMPP地址。</string>
<string name="this_is_an_xmpp_address">这是一个XMPP地址。请提供一个名称。</string>
<string name="creating_channel">创建公开群聊</string>
<string name="channel_already_exists">群聊已存在</string>
<string name="joined_an_existing_channel">您加入了一个已经存在的群聊</string>
<string name="unable_to_set_channel_configuration">无法配置群聊</string>
<string name="allow_participants_to_edit_subject">允许任何成员修改主题</string>
<string name="allow_participants_to_invite_others">允许任何成员邀请其他人</string>
<string name="anyone_can_edit_subject">允许任何成员修改主题</string>
<string name="owners_can_edit_subject">拥有者可修改话题</string>
<string name="admins_can_edit_subject">管理员可修改主题</string>
<string name="owners_can_invite_others">所有者可以邀请其他人</string>
<string name="anyone_can_invite_others">允许任何成员邀请其他人</string>
<string name="jabber_ids_are_visible_to_admins">XMPP地址对管理员可见。</string>
<string name="jabber_ids_are_visible_to_anyone">XMPP地址对所有人可见</string>
<string name="no_users_hint_channel">此公开群聊无成员。邀请成员或使用分享按钮分享地址。</string>
<string name="no_users_hint_group_chat">此私密群聊无成员</string>
<string name="manage_permission">管理权限</string>
<string name="search_participants">搜索成员</string>
<string name="file_too_large">文件过大</string>
<string name="attach">附加</string>
<string name="discover_channels">发现群聊</string>
<string name="search_channels">搜索群聊</string>
<string name="channel_discovery_opt_in_title">可能侵犯隐私!</string>
<string name="channel_discover_opt_in_message"><![CDATA[探索群聊功能使用一个叫<a href="https://search.jabbercat.org">search.jabbercat.org</a>的第三方服务。在探索群聊时您的IP地址和搜索内容将传送到他们的服务器上。有关更多信息请参阅他们的<a href="https://search.jabbercat.org/privacy">隐私政策</a>。]]></string>
<string name="i_already_have_an_account">我已有账户</string>
<string name="add_existing_account">添加已有账户</string>
<string name="register_new_account">注册新账户</string>
<string name="this_looks_like_a_domain">这好像是一个域名地址</string>
<string name="add_anway">仍然添加</string>
<string name="this_looks_like_channel">这好像是一个群聊地址</string>
<string name="share_backup_files">分享备份文件</string>
<string name="conversations_backup">备份文件</string>
<string name="event">事件</string>
<string name="open_backup">打开备份</string>
<string name="not_a_backup_file">选择的文件不是备份文件</string>
<string name="account_already_setup">账户已设置</string>
<string name="please_enter_password">请输入此账户的密码</string>
<string name="unable_to_perform_this_action">无法执行此操作</string>
<string name="open_join_dialog">加入公开群聊</string>
</resources>

View File

@ -337,6 +337,7 @@
<string name="x_file_offered_for_download">%s offered for download</string>
<string name="cancel_transmission">Cancel transmission</string>
<string name="file_transmission_failed">file transmission failed</string>
<string name="file_transmission_cancelled">file transmission cancelled</string>
<string name="file_deleted">The file has been deleted</string>
<string name="no_application_found_to_open_file">No application found to open file</string>
<string name="no_application_found_to_open_link">No application found to open link</string>
@ -861,7 +862,7 @@
<string name="discover_channels">Discover channels</string>
<string name="search_channels">Search channels</string>
<string name="channel_discovery_opt_in_title">Possible privacy violation!</string>
<string name="channel_discover_opt_in_message"><![CDATA[Channel discovery uses a third party service called <a href="https://search.jabbercat.org">search.jabbercat.org</a>.<br><br>Using this feature will transmit your IP address and search terms to that service. See their <a href="https://search.jabbercat.org/privacy">Privacy Policy</a> for more information.]]></string>
<string name="channel_discover_opt_in_message"><![CDATA[Channel discovery uses a third party service called <a href="https://search.jabber.network">search.jabber.network</a>.<br><br>Using this feature will transmit your IP address and search terms to that service. See their <a href="https://search.jabber.network/privacy">Privacy Policy</a> for more information.]]></string>
<string name="i_already_have_an_account">I already have an account</string>
<string name="add_existing_account">Add existing account</string>
<string name="register_new_account">Register new account</string>
@ -876,4 +877,5 @@
<string name="account_already_setup">This account has already been setup</string>
<string name="please_enter_password">Please enter the password for this account</string>
<string name="unable_to_perform_this_action">Unable to perform this action</string>
<string name="open_join_dialog">Join public channel…</string>
</resources>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="ConversationsTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="colorPrimary">@color/green600</item>
@ -114,13 +114,12 @@
<item type="reference" name="icon_enable_undecided_device">@drawable/ic_new_releases_black_24dp</item>
</style>
<style name="ConversationsTheme.Dark" parent="ConversationsTheme.Dark.Base" />
<style name="ConversationsTheme.Dark.Base" parent="Theme.AppCompat.NoActionBar">
<style name="ConversationsTheme.Dark" parent="Theme.AppCompat.NoActionBar">
<item name="colorPrimary">@color/green800</item>
<item name="colorPrimaryDark">@color/green900</item>
<item name="colorAccent">@color/blue_a100</item>
<item name="popupOverlayStyle">@style/ThemeOverlay.AppCompat.Dark</item>
<item name="android:navigationBarColor" tools:targetApi="21">@color/black</item>
<item name="color_background_primary">@color/grey800</item>
<item name="color_background_secondary">@color/grey900</item>
@ -281,6 +280,7 @@
<item name="android:windowFullscreen">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowBackground">@android:color/black</item>
<item name="android:navigationBarColor" tools:targetApi="21">@color/black</item>
</style>
<style name="SplashTheme" parent="Theme.AppCompat.NoActionBar">

View File

@ -6,4 +6,5 @@
<string name="pref_broadcast_last_activity_summary">إجعل كلّ جهات إتصالك تعلم أنك تستعمل كويكسي</string>
<string name="no_microphone_permission">كويكسي يحتاج الإتصال بالمايكروفون</string>
<string name="set_profile_picture">صورة حساب كويكسي</string>
<string name="not_available_in_your_country">إن كويكسي Quicksy غير متوفر في بلدكم.</string>
</resources>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="crash_report_title">Quicksy har kraschat</string>
<string name="no_camera_permission">Quicksy behöver tillgång till kameran</string>
<string name="pref_broadcast_last_activity_summary">Berätta för alla dina kontakter när du använder Quicksy</string>
<string name="no_microphone_permission">Quicksy behöver tillgång till mikrofonen</string>
<string name="not_available_in_your_country">Quicksy är inte tillgängligt i ditt land.</string>
<string name="unknown_security_error">Okänt säkerhetsfel.</string>
</resources>

View File

@ -19,4 +19,8 @@
<string name="no_microphone_permission">Quicksy需要麦克风权限</string>
<string name="foreground_service_channel_description">此通知类别用于显示表明Quicksy正在运行的永久通知。</string>
<string name="set_profile_picture">Quicksy个人资料图片</string>
</resources>
<string name="not_available_in_your_country">Quicksy在您的国家无服务。</string>
<string name="unable_to_verify_server_identity">无法确认服务器身份</string>
<string name="unknown_security_error">未知安全错误</string>
<string name="timeout_while_connecting_to_server">服务器已超时</string>
</resources>