Compare commits

..

1 commit

Author SHA1 Message Date
Daniel Gultsch
d33a46c77d disable printing letters on tiles 2016-10-26 20:28:11 +02:00
221 changed files with 2515 additions and 7744 deletions

View file

@ -1,12 +1,11 @@
language: android language: android
jdk:
- oraclejdk8
android: android:
components: components:
- platform-tools - platform-tools
- tools - tools
- build-tools-25.0.2 - build-tools-23.0.3
- android-25 - build-tools-19.1.0
- android-24
- extra-android-m2repository - extra-android-m2repository
- extra-google-m2repository - extra-google-m2repository
- extra-google-google_play_services - extra-google-google_play_services

View file

@ -1,40 +1,5 @@
###Changelog ###Changelog
####Version 1.15.5
* show nick as bold text when mentioned in conference
* bug fixes
####Version 1.15.4
* bug fixes
####Version 1.15.3
* show offline contacts in MUC as grayed-out
* don't transcode gifs. add overlay indication to gifs
* bug fixes
####Version 1.15.2
* bug fixes
####Version 1.15.1
* support for POSH (RFC7711)
* support for quoting messages (via select text)
* verified messages show shield icon. unverified messages show lock
####Version 1.15.0
* New [Blind Trust Before Verification](https://gultsch.de/trust.html) mode
* Easily share Barcode and XMPP uri from Account details
* Automatically deactivate own devices after 7 day of inactivity
* Improvements fo doze/push mode
* bug fixes
####Version 1.14.9
* warn in account details when data saver is enabled
* automatically enable foreground service after detecting frequent restarts
* bug fixes
####Version 1.14.8
* bug fixes
####Version 1.14.7 ####Version 1.14.7
* error message accessible via context menu for failed messages * error message accessible via context menu for failed messages
* don't include pgp signature in anonymous mucs * don't include pgp signature in anonymous mucs

View file

@ -120,13 +120,8 @@ My Bitcoin Address is: `1NxSU1YxYzJVDpX1rcESAA3NJki7kRgeeu`
#### How do I create an account? #### How do I create an account?
XMPP, like email, is a federated protocol, which means that there is not one company you can create an *official XMPP account* with. Instead there are hundreds, or even thousands, of providers out there. One of those providers is our very own [conversations.im](https://account.conversations.im). If you dont like to use *conversations.im* use a web search engine of your choice to find another provider. Or maybe your university has one. Or you can run your own. Or ask a friend to run one. Once you've found one, you can use Conversations to create an account. Just select *register new account* on server within the create account dialog. XMPP, like email, is a federated protocol, which means that there is not one company you can create an *official XMPP account* with. Instead there are hundreds, or even thousands, of providers out there. One of those providers is our very own [conversations.im](https://account.conversations.im). If you dont like to use *conversations.im* use a web search engine of your choice to find another provider. Or maybe your university has one. Or you can run your own. Or ask a friend to run one. Once you've found one, you can use Conversations to create an account. Just select *register new account* on server within the create account dialog.
##### Domain hosting
Using your own domain not only gives you a more recognizable Jabber ID, it also gives you the flexibility to migrate your account between different XMPP providers. This is a good compromise between the responsibilities of having to operate your own server and the downsides of being dependent on a single provider.
Learn more about [conversations.im Jabber/XMPP domain hosting](https://account.conversations.im/domain/).
##### Running your own ##### Running your own
If you already have a server somewhere and are willing and able to put the necessary work in, one alternative-in the spirit of federation-is to run your own. We recommend either [Prosody](https://prosody.im/) or [ejabberd](https://www.ejabberd.im/). Both of which have their own strengths. Ejabberd is slightly more mature nowadays but Prosody is arguably easier to set up. If you have a server somewhere and are willing to put some work in, the best alternative-in the spirit of federation-is to run your own. We recommand either [Prosody](https://prosody.im/) or [ejabberd](https://www.ejabberd.im/). Both of which have their own strengths. Ejabberd is slightly more mature nowadays but Prosody is arguably easier to set up.
For Prosody you need a couple of so called [community modules](https://modules.prosody.im/) most of which are maintained by the same people that develop Prosody. For Prosody you need a couple of so called [community modules](https://modules.prosody.im/) most of which are maintained by the same people that develop Prosody.
@ -250,7 +245,7 @@ other clients.
#### How do I backup / move Conversations to a new device? #### 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.) 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.)
If you migrate to a new device and would still like to keep your history please use a third party backup tool like [oandbackup](https://github.com/jensstein/oandbackup) or ```adb backup``` from your computer. It is important that your deactivate your account before backup and activate it only after a successful restore. Otherwise OMEMO might not work afterwards. If you migrate to a new device and would still like to keep your history please use a third party backup tool like [oandbackup](https://github.com/jensstein/oandbackup) or ```adb backup``` from your computer. It is important that your deactivate your account before backup and activate it only after a succesful restore. Otherwise OMEMO might not work afterwards.
#### Conversations is missing a certain feature #### Conversations is missing a certain feature
@ -298,7 +293,7 @@ To use OpenPGP you have to install the open source app
manage accounts and choose renew PGP announcement from the contextual menu. manage accounts and choose renew PGP announcement from the contextual menu.
#### OMEMO is grayed out. What do I do? #### OMEMO is grayed out. What do I do?
OMEMO has two requirements: Your server and the server of your contact need to support PEP. Both of you can verify that individually by opening your account details and selecting ```Server info``` from the menu. The appearing table should list PEP as available. The second requirement is mutual presence subscription. You can verify that by opening the contact details and see if both check boxes *Send presence updates* and *Receive presence updates* are checked. OMEMO has two requirments: Your server and the server of your contact need to support PEP. Both of you can verify that indivually by opening your account details and selecting ```Server info``` from the menu. The appearing table should list PEP as available. The second requirement is mutal presence subscription. You can verify that be going into the contact details and see if the both check boxes *Send presence updates* and *Receive presence updates* are checked.
#### How does the encryption for conferences work? #### How does the encryption for conferences work?
@ -325,7 +320,7 @@ is disabled.
Every participant has to announce their OpenPGP key (see answer above). Every participant has to announce their OpenPGP key (see answer above).
If you would like to send encrypted messages to a conference you have to make If you would like to send encrypted messages to a conference you have to make
sure that you have every participant's public key in your OpenKeychain. sure that you have every participant's public key in your OpenKeychain.
Right now there is no check in Conversations to ensure that. Right now there is no check in Conversations to ensurethat.
You have to take care of that yourself. Go to the conference details and You have to take care of that yourself. Go to the conference details and
touch every key id (The hexadecimal number below a contact). This will send you touch every key id (The hexadecimal number below a contact). This will send you
to OpenKeychain which will assist you on adding the key. This works best in to OpenKeychain which will assist you on adding the key. This works best in
@ -334,21 +329,12 @@ feature is regarded experimental. Conversations is the only client that uses
XEP-0027 with conferences. (The XEP neither specifically allows nor disallows XEP-0027 with conferences. (The XEP neither specifically allows nor disallows
this.) this.)
#### Why is Conversations not end-to-end encrypted by default
We briefly had OMEMO as the default E2EE but it turned out to be a usability nightmare and thus we reverted that. You can find more information in [the commit message](https://github.com/siacs/Conversations/commit/035d0c79572d5981c53d1bff7f30b484c6542f17) of that change.
Quick reminder that Conversations **always** uses TLS to connect to your server. It wont even connect to a server without TLS.
#### What is Blind Trust Before Verification / why are messages marked with a red lock?
Read more about the concept on https://gultsch.de/trust.html
### What clients do I use on other platforms ### What clients do I use on other platforms
There are XMPP Clients available for all major platforms. There are XMPP Clients available for all major platforms.
####Windows / Linux ####Windows / Linux
For your desktop computer we recommend that you use [Gajim](https://gajim.org). You need to install the plugins `OMEMO`, `HTTP Upload` and `URL image preview` to get the best compatibility with Conversations. Plugins can be installed from within the app. For your desktop computer we recommand that you use [Gajim](https://gajim.org). You need to install the plugins `OMEMO`, `HTTP Upload` and `URL image preview` to get the best compatibiltiy with Conversations. Plugins can be installed from withhin the app.
####iOS ####iOS
Unfortunately we dont have a recommendation for iPhones right now. There are two clients available [ChatSecure](https://chatsecure.org/) and [Monal](https://monal.im/). Both with their own pros and cons. Unfortunatly we dont have a recommandation for iPhones right now. There are two clients available [ChatSecure](https://chatsecure.org/) and [Monal](https://monal.im/). Both with their own pros and cons.
### Development ### Development
@ -397,23 +383,12 @@ To add a new dependency to the `libs/` directory (replacing "name", "branch" and
If something goes wrong Conversations usually exposes very little information in If something goes wrong Conversations usually exposes very little information in
the UI (other than the fact that something didn't work). However with adb the UI (other than the fact that something didn't work). However with adb
(android debug bridge) you can squeeze some more information out of Conversations. (android debug bridge) you squeeze some more information out of Conversations.
These information are especially useful if you are experiencing trouble with These information are especially useful if you are experiencing trouble with
your connection or with file transfer. your connection or with file transfer.
To use adb you have to connect your mobile phone to your computer with an USB cable
and install `adb`. Most Linux systems have prebuilt packages for that tool. On
Debian/Ubuntu for example it is called `android-tools-adb`.
Furthermore you might have to enable 'USB debugging' in the Developer options of your
phone. After that you can just execute the following on your computer:
adb -d logcat -v time -s conversations adb -d logcat -v time -s conversations
If need be there are also some Apps on the PlayStore that can be used to show the logcat
directly on your rooted phone. (Search for logcat). However in regards to further processing
(for example to create an issue here on Github) it is more convenient to just use your PC.
#### I found a bug #### I found a bug
Please report it to our [issue tracker][issues]. If your app crashes please Please report it to our [issue tracker][issues]. If your app crashes please

View file

@ -1,54 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="48"
height="48"
viewBox="0 0 48 48"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="ic_verified_fingerprint.svg">
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1916"
inkscape:window-height="1156"
id="namedview6"
showgrid="false"
inkscape:zoom="4.9166667"
inkscape:cx="-3.3559322"
inkscape:cy="24"
inkscape:window-x="0"
inkscape:window-y="20"
inkscape:window-maximized="0"
inkscape:current-layer="svg2" />
<path
d="M24 2L6 10v12c0 11.11 7.67 21.47 18 24 10.33-2.53 18-12.89 18-24V10L24 2zm-4 32l-8-8 2.83-2.83L20 28.34l13.17-13.17L36 18 20 34z"
id="path4"
style="fill:#259b24;fill-opacity:0.87" />
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -1,68 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="24"
height="24"
viewBox="0 0 24 24"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="play_gif.svg">
<metadata
id="metadata14">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1200"
id="namedview12"
showgrid="false"
inkscape:zoom="9.8333333"
inkscape:cx="1.5762712"
inkscape:cy="11.084746"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg2" />
<defs
id="defs4">
<path
id="a"
d="M24 24H0V0h24v24z" />
</defs>
<clipPath
id="b">
<use
xlink:href="#a"
overflow="visible"
id="use8" />
</clipPath>
<path
d="M11.5 9H13v6h-1.5zM9 9H6c-.6 0-1 .5-1 1v4c0 .5.4 1 1 1h3c.6 0 1-.5 1-1v-2H8.5v1.5h-2v-3H10V10c0-.5-.4-1-1-1zm10 1.5V9h-4.5v6H16v-2h2v-1.5h-2v-1z"
clip-path="url(#b)"
id="path10"
style="fill:#ffffff;fill-opacity:0.7019608" />
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -13,8 +13,7 @@ resolutions = {
images = { images = {
'ic_launcher.svg' => ['ic_launcher', 48], 'ic_launcher.svg' => ['ic_launcher', 48],
'main_logo.svg' => ['main_logo', 200], 'main_logo.svg' => ['main_logo', 200],
'play_video.svg' => ['play_video', 128], 'play_video.svg' => ['play_video', 96],
'play_gif.svg' => ['play_gif', 128],
'conversations_mono.svg' => ['ic_notification', 24], 'conversations_mono.svg' => ['ic_notification', 24],
'ic_received_indicator.svg' => ['ic_received_indicator', 12], 'ic_received_indicator.svg' => ['ic_received_indicator', 12],
'ic_send_text_offline.svg' => ['ic_send_text_offline', 36], 'ic_send_text_offline.svg' => ['ic_send_text_offline', 36],
@ -51,7 +50,6 @@ images = {
'ic_notifications_off_white80.svg' => ['ic_notifications_off_white80', 24], 'ic_notifications_off_white80.svg' => ['ic_notifications_off_white80', 24],
'ic_notifications_paused_white80.svg' => ['ic_notifications_paused_white80', 24], 'ic_notifications_paused_white80.svg' => ['ic_notifications_paused_white80', 24],
'ic_notifications_white80.svg' => ['ic_notifications_white80', 24], 'ic_notifications_white80.svg' => ['ic_notifications_white80', 24],
'ic_verified_fingerprint.svg' => ['ic_verified_fingerprint', 36],
'md_switch_thumb_disable.svg' => ['switch_thumb_disable', 48], 'md_switch_thumb_disable.svg' => ['switch_thumb_disable', 48],
'md_switch_thumb_off_normal.svg' => ['switch_thumb_off_normal', 48], 'md_switch_thumb_off_normal.svg' => ['switch_thumb_off_normal', 48],
'md_switch_thumb_off_pressed.svg' => ['switch_thumb_off_pressed', 48], 'md_switch_thumb_off_pressed.svg' => ['switch_thumb_off_pressed', 48],
@ -59,7 +57,7 @@ images = {
'md_switch_thumb_on_pressed.svg' => ['switch_thumb_on_pressed', 48], 'md_switch_thumb_on_pressed.svg' => ['switch_thumb_on_pressed', 48],
'message_bubble_received.svg' => ['message_bubble_received.9', 0], 'message_bubble_received.svg' => ['message_bubble_received.9', 0],
'message_bubble_received_grey.svg' => ['message_bubble_received_grey.9', 0], 'message_bubble_received_grey.svg' => ['message_bubble_received_grey.9', 0],
'message_bubble_received_dark.svg' => ['message_bubble_received_dark.9', 0], 'message_bubble_received_dark.svg' => ['message_bubble_received_dark.9', 0],
'message_bubble_received_warning.svg' => ['message_bubble_received_warning.9', 0], 'message_bubble_received_warning.svg' => ['message_bubble_received_warning.9', 0],
'message_bubble_received_white.svg' => ['message_bubble_received_white.9', 0], 'message_bubble_received_white.svg' => ['message_bubble_received_white.9', 0],
'message_bubble_sent.svg' => ['message_bubble_sent.9', 0], 'message_bubble_sent.svg' => ['message_bubble_sent.9', 0],

View file

@ -23,10 +23,10 @@ configurations {
dependencies { dependencies {
compile project(':libs:MemorizingTrustManager') compile project(':libs:MemorizingTrustManager')
playstoreCompile 'com.google.android.gms:play-services-gcm:10.0.1' playstoreCompile 'com.google.android.gms:play-services-gcm:9.4.0'
compile 'org.sufficientlysecure:openpgp-api:10.0' compile 'org.sufficientlysecure:openpgp-api:10.0'
compile 'com.soundcloud.android:android-crop:1.0.1@aar' compile 'com.soundcloud.android:android-crop:1.0.1@aar'
compile 'com.android.support:support-v13:25.1.0' compile 'com.android.support:support-v13:24.2.0'
compile 'org.bouncycastle:bcprov-jdk15on:1.52' compile 'org.bouncycastle:bcprov-jdk15on:1.52'
compile 'org.bouncycastle:bcmail-jdk15on:1.52' compile 'org.bouncycastle:bcmail-jdk15on:1.52'
compile 'org.jitsi:org.otr4j:0.22' compile 'org.jitsi:org.otr4j:0.22'
@ -35,12 +35,11 @@ dependencies {
compile 'com.google.zxing:android-integration:3.2.1' compile 'com.google.zxing:android-integration:3.2.1'
compile 'de.measite.minidns:minidns:0.1.7' compile 'de.measite.minidns:minidns:0.1.7'
compile 'de.timroes.android:EnhancedListView:0.3.4' compile 'de.timroes.android:EnhancedListView:0.3.4'
compile 'me.leolin:ShortcutBadger:1.1.11@aar' compile 'me.leolin:ShortcutBadger:1.1.4@aar'
compile 'com.kyleduo.switchbutton:library:1.2.8' compile 'com.kyleduo.switchbutton:library:1.2.8'
compile 'org.whispersystems:axolotl-android:1.3.4' compile 'org.whispersystems:axolotl-android:1.3.4'
compile 'com.makeramen:roundedimageview:2.2.0' compile 'com.makeramen:roundedimageview:2.2.0'
compile "com.wefika:flowlayout:0.4.1" compile "com.wefika:flowlayout:0.4.1"
compile 'net.ypresto.androidtranscoder:android-transcoder:0.2.0'
} }
ext { ext {
@ -50,14 +49,14 @@ ext {
} }
android { android {
compileSdkVersion 25 compileSdkVersion 24
buildToolsVersion "25.0.2" buildToolsVersion "23.0.3"
defaultConfig { defaultConfig {
minSdkVersion 14 minSdkVersion 14
targetSdkVersion 25 targetSdkVersion 24
versionCode 193 versionCode 180
versionName "1.15.5" versionName "1.14.7"
archivesBaseName += "-$versionName" archivesBaseName += "-$versionName"
applicationId "eu.siacs.conversations" applicationId "eu.siacs.conversations"
} }

View file

@ -29,4 +29,3 @@
* XEP-0363: HTTP File Upload * XEP-0363: HTTP File Upload
* XEP-0368: SRV records for XMPP over TLS * XEP-0368: SRV records for XMPP over TLS
* XEP-0377: Spam Reporting * XEP-0377: Spam Reporting
* XEP-0384: OMEMO Encryption

View file

@ -7,14 +7,14 @@ buildscript {
} }
} }
apply plugin: 'com.android.library' apply plugin: 'android-library'
android { android {
compileSdkVersion 25 compileSdkVersion 19
buildToolsVersion "25.0.2" buildToolsVersion "19.1"
defaultConfig { defaultConfig {
minSdkVersion 14 minSdkVersion 7
targetSdkVersion 25 targetSdkVersion 19
} }
sourceSets { sourceSets {

View file

@ -35,33 +35,15 @@ import android.app.PendingIntent;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.util.Base64;
import android.util.Log;
import android.util.SparseArray; import android.util.SparseArray;
import android.os.Handler; import android.os.Handler;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.NoSuchAlgorithmException;
import java.security.cert.*; import java.security.cert.*;
import java.security.KeyStore; import java.security.KeyStore;
import java.security.KeyStoreException; import java.security.KeyStoreException;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
@ -69,10 +51,8 @@ import java.util.Collection;
import java.util.Enumeration; import java.util.Enumeration;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.regex.Pattern;
import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.TrustManagerFactory;
@ -88,15 +68,7 @@ import javax.net.ssl.X509TrustManager;
* <b>WARNING:</b> This only works if a dedicated thread is used for * <b>WARNING:</b> This only works if a dedicated thread is used for
* opening sockets! * opening sockets!
*/ */
public class MemorizingTrustManager { public class MemorizingTrustManager implements X509TrustManager {
private static final Pattern PATTERN_IPV4 = Pattern.compile("\\A(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z");
private static final Pattern PATTERN_IPV6_HEX4DECCOMPRESSED = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) ::((?:[0-9A-Fa-f]{1,4}:)*)(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z");
private static final Pattern PATTERN_IPV6_6HEX4DEC = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}:){6,6})(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z");
private static final Pattern PATTERN_IPV6_HEXCOMPRESSED = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)\\z");
private static final Pattern PATTERN_IPV6 = Pattern.compile("\\A(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\\z");
final static String DECISION_INTENT = "de.duenndns.ssl.DECISION"; final static String DECISION_INTENT = "de.duenndns.ssl.DECISION";
final static String DECISION_INTENT_ID = DECISION_INTENT + ".decisionId"; final static String DECISION_INTENT_ID = DECISION_INTENT + ".decisionId";
final static String DECISION_INTENT_CERT = DECISION_INTENT + ".cert"; final static String DECISION_INTENT_CERT = DECISION_INTENT + ".cert";
@ -122,7 +94,6 @@ public class MemorizingTrustManager {
private KeyStore appKeyStore; private KeyStore appKeyStore;
private X509TrustManager defaultTrustManager; private X509TrustManager defaultTrustManager;
private X509TrustManager appTrustManager; private X509TrustManager appTrustManager;
private String poshCacheDir;
/** Creates an instance of the MemorizingTrustManager class that falls back to a custom TrustManager. /** Creates an instance of the MemorizingTrustManager class that falls back to a custom TrustManager.
* *
@ -178,11 +149,28 @@ public class MemorizingTrustManager {
File dir = app.getDir(KEYSTORE_DIR, Context.MODE_PRIVATE); File dir = app.getDir(KEYSTORE_DIR, Context.MODE_PRIVATE);
keyStoreFile = new File(dir + File.separator + KEYSTORE_FILE); keyStoreFile = new File(dir + File.separator + KEYSTORE_FILE);
poshCacheDir = app.getFilesDir().getAbsolutePath()+"/posh_cache/";
appKeyStore = loadAppKeyStore(); appKeyStore = loadAppKeyStore();
} }
/**
* Returns a X509TrustManager list containing a new instance of
* TrustManagerFactory.
*
* This function is meant for convenience only. You can use it
* as follows to integrate TrustManagerFactory for HTTPS sockets:
*
* <pre>
* SSLContext sc = SSLContext.getInstance("TLS");
* sc.init(null, MemorizingTrustManager.getInstanceList(this),
* new java.security.SecureRandom());
* HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
* </pre>
* @param c Activity or Service to show the Dialog / Notification
*/
public static X509TrustManager[] getInstanceList(Context c) {
return new X509TrustManager[] { new MemorizingTrustManager(c) };
}
/** /**
* Binds an Activity to the MTM for displaying the query dialog. * Binds an Activity to the MTM for displaying the query dialog.
@ -401,7 +389,7 @@ public class MemorizingTrustManager {
return false; return false;
} }
public void checkCertTrusted(X509Certificate[] chain, String authType, String domain, boolean isServer, boolean interactive) public void checkCertTrusted(X509Certificate[] chain, String authType, boolean isServer, boolean interactive)
throws CertificateException throws CertificateException
{ {
LOGGER.log(Level.FINE, "checkCertTrusted(" + chain + ", " + authType + ", " + isServer + ")"); LOGGER.log(Level.FINE, "checkCertTrusted(" + chain + ", " + authType + ", " + isServer + ")");
@ -431,15 +419,6 @@ public class MemorizingTrustManager {
else else
defaultTrustManager.checkClientTrusted(chain, authType); defaultTrustManager.checkClientTrusted(chain, authType);
} catch (CertificateException e) { } catch (CertificateException e) {
boolean trustSystemCAs = !PreferenceManager.getDefaultSharedPreferences(master).getBoolean("dont_trust_system_cas", false);
if (domain != null && isServer && trustSystemCAs && !isIp(domain)) {
String hash = getBase64Hash(chain[0],"SHA-256");
List<String> fingerprints = getPoshFingerprints(domain);
if (hash != null && fingerprints.contains(hash)) {
Log.d("mtm","trusted cert fingerprint of "+domain+" via posh");
return;
}
}
e.printStackTrace(); e.printStackTrace();
if (interactive) { if (interactive) {
interactCert(chain, authType, e); interactCert(chain, authType, e);
@ -450,147 +429,20 @@ public class MemorizingTrustManager {
} }
} }
private List<String> getPoshFingerprints(String domain) { public void checkClientTrusted(X509Certificate[] chain, String authType)
List<String> cached = getPoshFingerprintsFromCache(domain); throws CertificateException
if (cached == null) { {
return getPoshFingerprintsFromServer(domain); checkCertTrusted(chain, authType, false,true);
} else {
return cached;
}
} }
private List<String> getPoshFingerprintsFromServer(String domain) { public void checkServerTrusted(X509Certificate[] chain, String authType)
return getPoshFingerprintsFromServer(domain, "https://"+domain+"/.well-known/posh/xmpp-client.json",-1,true); throws CertificateException
{
checkCertTrusted(chain, authType, true,true);
} }
private List<String> getPoshFingerprintsFromServer(String domain, String url, int maxTtl, boolean followUrl) { public X509Certificate[] getAcceptedIssuers()
Log.d("mtm","downloading json for "+domain+" from "+url); {
try {
List<String> results = new ArrayList<>();
HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection();
connection.setConnectTimeout(5000);
connection.setReadTimeout(5000);
BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String inputLine;
StringBuilder builder = new StringBuilder();
while ((inputLine = in.readLine()) != null) {
builder.append(inputLine);
}
JSONObject jsonObject = new JSONObject(builder.toString());
in.close();
int expires = jsonObject.getInt("expires");
if (expires <= 0) {
return new ArrayList<>();
}
if (maxTtl >= 0) {
expires = Math.min(maxTtl,expires);
}
String redirect;
try {
redirect = jsonObject.getString("url");
} catch (JSONException e) {
redirect = null;
}
if (followUrl && redirect != null && redirect.toLowerCase().startsWith("https")) {
return getPoshFingerprintsFromServer(domain, redirect, expires, false);
}
JSONArray fingerprints = jsonObject.getJSONArray("fingerprints");
for(int i = 0; i < fingerprints.length(); i++) {
JSONObject fingerprint = fingerprints.getJSONObject(i);
String sha256 = fingerprint.getString("sha-256");
if (sha256 != null) {
results.add(sha256);
}
}
writeFingerprintsToCache(domain, results,1000L * expires+System.currentTimeMillis());
return results;
} catch (Exception e) {
Log.d("mtm","error fetching posh "+e.getMessage());
return new ArrayList<>();
}
}
private File getPoshCacheFile(String domain) {
return new File(poshCacheDir+domain+".json");
}
private void writeFingerprintsToCache(String domain, List<String> results, long expires) {
File file = getPoshCacheFile(domain);
file.getParentFile().mkdirs();
try {
file.createNewFile();
JSONObject jsonObject = new JSONObject();
jsonObject.put("expires",expires);
jsonObject.put("fingerprints",new JSONArray(results));
FileOutputStream outputStream = new FileOutputStream(file);
outputStream.write(jsonObject.toString().getBytes());
outputStream.flush();
outputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
private List<String> getPoshFingerprintsFromCache(String domain) {
File file = getPoshCacheFile(domain);
try {
InputStream is = new FileInputStream(file);
BufferedReader buf = new BufferedReader(new InputStreamReader(is));
String line = buf.readLine();
StringBuilder sb = new StringBuilder();
while(line != null){
sb.append(line).append("\n");
line = buf.readLine();
}
JSONObject jsonObject = new JSONObject(sb.toString());
is.close();
long expires = jsonObject.getLong("expires");
long expiresIn = expires - System.currentTimeMillis();
if (expiresIn < 0) {
file.delete();
return null;
} else {
Log.d("mtm","posh fingerprints expire in "+(expiresIn/1000)+"s");
}
List<String> result = new ArrayList<>();
JSONArray jsonArray = jsonObject.getJSONArray("fingerprints");
for(int i = 0; i < jsonArray.length(); ++i) {
result.add(jsonArray.getString(i));
}
return result;
} catch (FileNotFoundException e) {
return null;
} catch (IOException e) {
return null;
} catch (JSONException e) {
file.delete();
return null;
}
}
private static boolean isIp(final String server) {
return server != null && (
PATTERN_IPV4.matcher(server).matches()
|| PATTERN_IPV6.matcher(server).matches()
|| PATTERN_IPV6_6HEX4DEC.matcher(server).matches()
|| PATTERN_IPV6_HEX4DECCOMPRESSED.matcher(server).matches()
|| PATTERN_IPV6_HEXCOMPRESSED.matcher(server).matches());
}
private static String getBase64Hash(X509Certificate certificate, String digest) throws CertificateEncodingException {
MessageDigest md;
try {
md = MessageDigest.getInstance(digest);
} catch (NoSuchAlgorithmException e) {
return null;
}
md.update(certificate.getEncoded());
return Base64.encodeToString(md.digest(),Base64.NO_WRAP);
}
private X509Certificate[] getAcceptedIssuers() {
LOGGER.log(Level.FINE, "getAcceptedIssuers()"); LOGGER.log(Level.FINE, "getAcceptedIssuers()");
return defaultTrustManager.getAcceptedIssuers(); return defaultTrustManager.getAcceptedIssuers();
} }
@ -701,6 +553,22 @@ public class MemorizingTrustManager {
certDetails(si, cert); certDetails(si, cert);
return si.toString(); return si.toString();
} }
// We can use Notification.Builder once MTM's minSDK is >= 11
@SuppressWarnings("deprecation")
void startActivityNotification(Intent intent, int decisionId, String certName) {
Notification n = new Notification(android.R.drawable.ic_lock_lock,
master.getString(R.string.mtm_notification),
System.currentTimeMillis());
PendingIntent call = PendingIntent.getActivity(master, 0, intent, 0);
n.setLatestEventInfo(master.getApplicationContext(),
master.getString(R.string.mtm_notification),
certName, call);
n.flags |= Notification.FLAG_AUTO_CANCEL;
notificationManager.notify(NOTIFICATION_ID + decisionId, n);
}
/** /**
* Returns the top-most entry of the activity stack. * Returns the top-most entry of the activity stack.
* *
@ -730,6 +598,7 @@ public class MemorizingTrustManager {
getUI().startActivity(ni); getUI().startActivity(ni);
} catch (Exception e) { } catch (Exception e) {
LOGGER.log(Level.FINE, "startActivity(MemorizingActivity)", e); LOGGER.log(Level.FINE, "startActivity(MemorizingActivity)", e);
startActivityNotification(ni, myId, message);
} }
} }
}); });
@ -839,39 +708,22 @@ public class MemorizingTrustManager {
} }
public X509TrustManager getNonInteractive(String domain) {
return new NonInteractiveMemorizingTrustManager(domain);
}
public X509TrustManager getInteractive(String domain) {
return new InteractiveMemorizingTrustManager(domain);
}
public X509TrustManager getNonInteractive() { public X509TrustManager getNonInteractive() {
return new NonInteractiveMemorizingTrustManager(null); return new NonInteractiveMemorizingTrustManager();
}
public X509TrustManager getInteractive() {
return new InteractiveMemorizingTrustManager(null);
} }
private class NonInteractiveMemorizingTrustManager implements X509TrustManager { private class NonInteractiveMemorizingTrustManager implements X509TrustManager {
private final String domain;
public NonInteractiveMemorizingTrustManager(String domain) {
this.domain = domain;
}
@Override @Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { public void checkClientTrusted(X509Certificate[] chain, String authType)
MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, false, false); throws CertificateException {
MemorizingTrustManager.this.checkCertTrusted(chain, authType, false, false);
} }
@Override @Override
public void checkServerTrusted(X509Certificate[] chain, String authType) public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException { throws CertificateException {
MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, true, false); MemorizingTrustManager.this.checkCertTrusted(chain, authType, true, false);
} }
@Override @Override
@ -880,28 +732,4 @@ public class MemorizingTrustManager {
} }
} }
private class InteractiveMemorizingTrustManager implements X509TrustManager {
private final String domain;
public InteractiveMemorizingTrustManager(String domain) {
this.domain = domain;
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, false, true);
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, true, true);
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return MemorizingTrustManager.this.getAcceptedIssuers();
}
}
} }

View file

@ -21,8 +21,4 @@ public class PushManagementService {
public boolean isStub() { public boolean isStub() {
return true; return true;
} }
public boolean availableAndUseful(Account account) {
return false;
}
} }

View file

@ -19,8 +19,6 @@
android:name="android.permission.READ_PHONE_STATE" android:name="android.permission.READ_PHONE_STATE"
tools:node="remove" /> tools:node="remove" />
<uses-sdk tools:overrideLibrary="net.ypresto.androidtranscoder" />
<application <application
android:allowBackup="true" android:allowBackup="true"
android:icon="@drawable/ic_launcher" android:icon="@drawable/ic_launcher"
@ -199,18 +197,13 @@
<provider <provider
android:name="android.support.v4.content.FileProvider" android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.files" android:authorities="eu.siacs.conversations.files"
android:exported="false" android:exported="false"
android:grantUriPermissions="true"> android:grantUriPermissions="true">
<meta-data <meta-data
android:name="android.support.FILE_PROVIDER_PATHS" android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" /> android:resource="@xml/file_paths" />
</provider> </provider>
<provider
android:authorities="${applicationId}.barcodes"
android:name=".services.BarcodeProvider"
android:exported="false"
android:grantUriPermissions="true"/>
</application> </application>

View file

@ -61,6 +61,11 @@ public final class Config {
public static final int CONNECT_DISCO_TIMEOUT = 20; public static final int CONNECT_DISCO_TIMEOUT = 20;
public static final int MINI_GRACE_PERIOD = 750; public static final int MINI_GRACE_PERIOD = 750;
public static final boolean PUSH_MODE = false; //closes the tcp connection when going to background
//and after PING_MIN_INTERVAL of inactivity
//very experimental. only enable this if you want
//to around with GCM push
public static final int AVATAR_SIZE = 192; public static final int AVATAR_SIZE = 192;
public static final Bitmap.CompressFormat AVATAR_FORMAT = Bitmap.CompressFormat.WEBP; public static final Bitmap.CompressFormat AVATAR_FORMAT = Bitmap.CompressFormat.WEBP;
@ -78,14 +83,6 @@ public final class Config {
public static final int MAX_DISPLAY_MESSAGE_CHARS = 4096; public static final int MAX_DISPLAY_MESSAGE_CHARS = 4096;
public static final long MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000;
public static final long OMEMO_AUTO_EXPIRY = 7 * MILLISECONDS_IN_DAY;
public static final boolean REMOVE_BROKEN_DEVICES = false;
public static final boolean OMEMO_PADDING = false;
public static boolean PUT_AUTH_TAG_INTO_KEY = false;
public static final boolean DISABLE_PROXY_LOOKUP = false; //useful to debug ibb public static final boolean DISABLE_PROXY_LOOKUP = false; //useful to debug ibb
public static final boolean DISABLE_HTTP_UPLOAD = false; public static final boolean DISABLE_HTTP_UPLOAD = false;
public static final boolean DISABLE_STRING_PREP = false; // setting to true might increase startup performance public static final boolean DISABLE_STRING_PREP = false; // setting to true might increase startup performance
@ -97,25 +94,21 @@ public final class Config {
public static final boolean REPORT_WRONG_FILESIZE_IN_OTR_JINGLE = true; public static final boolean REPORT_WRONG_FILESIZE_IN_OTR_JINGLE = true;
public static final boolean X509_VERIFICATION = false; //use x509 certificates to verify OMEMO keys public static final boolean SHOW_REGENERATE_AXOLOTL_KEYS_BUTTON = false;
public static final boolean ONLY_INTERNAL_STORAGE = false; //use internal storage instead of sdcard to save attachments public static final boolean X509_VERIFICATION = false; //use x509 certificates to verify OMEMO keys
public static final boolean IGNORE_ID_REWRITE_IN_MUC = true; public static final boolean IGNORE_ID_REWRITE_IN_MUC = true;
public static final boolean PARSE_REAL_JID_FROM_MUC_MAM = false; //dangerous if server doesnt filter public static final boolean PARSE_REAL_JID_FROM_MUC_MAM = false; //dangerous if server doesnt filter
public static final long MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000;
public static final long MAM_MAX_CATCHUP = MILLISECONDS_IN_DAY / 2; public static final long MAM_MAX_CATCHUP = MILLISECONDS_IN_DAY / 2;
public static final int MAM_MAX_MESSAGES = 500; public static final int MAM_MAX_MESSAGES = 500;
public static final long FREQUENT_RESTARTS_DETECTION_WINDOW = 12 * 60 * 60 * 1000; // 10 hours
public static final long FREQUENT_RESTARTS_THRESHOLD = 0; // previous value was 16;
public static final ChatState DEFAULT_CHATSTATE = ChatState.ACTIVE; public static final ChatState DEFAULT_CHATSTATE = ChatState.ACTIVE;
public static final int TYPING_TIMEOUT = 8; public static final int TYPING_TIMEOUT = 8;
public static final int EXPIRY_INTERVAL = 30 * 60 * 1000; // 30 minutes
public static final String ENABLED_CIPHERS[] = { public static final String ENABLED_CIPHERS[] = {
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA384", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA384",

View file

@ -194,9 +194,8 @@ public class OtrService extends OtrCryptoEngineImpl implements OtrEngineHost {
} catch (final InvalidJidException ignored) { } catch (final InvalidJidException ignored) {
} }
packet.setType(MessagePacket.TYPE_CHAT); packet.setType(MessagePacket.TYPE_CHAT);
packet.addChild("encryption","urn:xmpp:eme:0")
.setAttribute("namespace","urn:xmpp:otr:0");
account.getXmppConnection().sendMessagePacket(packet); account.getXmppConnection().sendMessagePacket(packet);
} }

View file

@ -25,17 +25,13 @@ import java.security.PrivateKey;
import java.security.Security; import java.security.Security;
import java.security.Signature; import java.security.Signature;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Random; import java.util.Random;
import java.util.Set; import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
@ -77,8 +73,6 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
private int numPublishTriesOnEmptyPep = 0; private int numPublishTriesOnEmptyPep = 0;
private boolean pepBroken = false; private boolean pepBroken = false;
private AtomicBoolean ownPushPending = new AtomicBoolean(false);
@Override @Override
public void onAdvancedStreamFeaturesAvailable(Account account) { public void onAdvancedStreamFeaturesAvailable(Account account) {
if (Config.supportOmemo() if (Config.supportOmemo()
@ -94,7 +88,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
for(Jid jid : jids) { for(Jid jid : jids) {
if (deviceIds.get(jid) != null) { if (deviceIds.get(jid) != null) {
for (Integer foreignId : this.deviceIds.get(jid)) { for (Integer foreignId : this.deviceIds.get(jid)) {
AxolotlAddress address = new AxolotlAddress(jid.toPreppedString(), foreignId); AxolotlAddress address = new AxolotlAddress(jid.toString(), foreignId);
if (fetchStatusMap.getAll(address).containsValue(FetchStatus.ERROR)) { if (fetchStatusMap.getAll(address).containsValue(FetchStatus.ERROR)) {
return true; return true;
} }
@ -104,23 +98,6 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
return false; return false;
} }
public void preVerifyFingerprint(Contact contact, String fingerprint) {
axolotlStore.preVerifyFingerprint(contact.getAccount(), contact.getJid().toBareJid().toPreppedString(), fingerprint);
}
public void preVerifyFingerprint(Account account, String fingerprint) {
axolotlStore.preVerifyFingerprint(account, account.getJid().toBareJid().toPreppedString(), fingerprint);
}
public boolean hasVerifiedKeys(String name) {
for(XmppAxolotlSession session : this.sessions.getAll(new AxolotlAddress(name,0)).values()) {
if (session.getTrust().isVerified()) {
return true;
}
}
return false;
}
private static class AxolotlAddressMap<T> { private static class AxolotlAddressMap<T> {
protected Map<String, Map<Integer, T>> map; protected Map<String, Map<Integer, T>> map;
protected final Object MAP_LOCK = new Object(); protected final Object MAP_LOCK = new Object();
@ -187,6 +164,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
private void putDevicesForJid(String bareJid, List<Integer> deviceIds, SQLiteAxolotlStore store) { private void putDevicesForJid(String bareJid, List<Integer> deviceIds, SQLiteAxolotlStore store) {
for (Integer deviceId : deviceIds) { for (Integer deviceId : deviceIds) {
AxolotlAddress axolotlAddress = new AxolotlAddress(bareJid, deviceId); AxolotlAddress axolotlAddress = new AxolotlAddress(bareJid, deviceId);
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building session for remote address: " + axolotlAddress.toString());
IdentityKey identityKey = store.loadSession(axolotlAddress).getSessionState().getRemoteIdentityKey(); IdentityKey identityKey = store.loadSession(axolotlAddress).getSessionState().getRemoteIdentityKey();
if(Config.X509_VERIFICATION) { if(Config.X509_VERIFICATION) {
X509Certificate certificate = store.getFingerprintCertificate(identityKey.getFingerprint().replaceAll("\\s", "")); X509Certificate certificate = store.getFingerprintCertificate(identityKey.getFingerprint().replaceAll("\\s", ""));
@ -222,7 +200,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
public void put(AxolotlAddress address, XmppAxolotlSession value) { public void put(AxolotlAddress address, XmppAxolotlSession value) {
super.put(address, value); super.put(address, value);
value.setNotFresh(); value.setNotFresh();
xmppConnectionService.syncRosterToDisk(account); //TODO why? xmppConnectionService.syncRosterToDisk(account);
} }
public void put(XmppAxolotlSession session) { public void put(XmppAxolotlSession session) {
@ -235,7 +213,6 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
SUCCESS, SUCCESS,
SUCCESS_VERIFIED, SUCCESS_VERIFIED,
TIMEOUT, TIMEOUT,
SUCCESS_TRUSTED,
ERROR ERROR
} }
@ -279,18 +256,18 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
return axolotlStore.getIdentityKeyPair().getPublicKey().getFingerprint().replaceAll("\\s", ""); return axolotlStore.getIdentityKeyPair().getPublicKey().getFingerprint().replaceAll("\\s", "");
} }
public Set<IdentityKey> getKeysWithTrust(FingerprintStatus status) { public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust) {
return axolotlStore.getContactKeysWithTrust(account.getJid().toBareJid().toPreppedString(), status); return axolotlStore.getContactKeysWithTrust(account.getJid().toBareJid().toPreppedString(), trust);
} }
public Set<IdentityKey> getKeysWithTrust(FingerprintStatus status, Jid jid) { public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust, Jid jid) {
return axolotlStore.getContactKeysWithTrust(jid.toBareJid().toPreppedString(), status); return axolotlStore.getContactKeysWithTrust(jid.toBareJid().toPreppedString(), trust);
} }
public Set<IdentityKey> getKeysWithTrust(FingerprintStatus status, List<Jid> jids) { public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust, List<Jid> jids) {
Set<IdentityKey> keys = new HashSet<>(); Set<IdentityKey> keys = new HashSet<>();
for(Jid jid : jids) { for(Jid jid : jids) {
keys.addAll(axolotlStore.getContactKeysWithTrust(jid.toPreppedString(), status)); keys.addAll(axolotlStore.getContactKeysWithTrust(jid.toPreppedString(), trust));
} }
return keys; return keys;
} }
@ -312,20 +289,14 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
return new AxolotlAddress(jid.toPreppedString(), 0); return new AxolotlAddress(jid.toPreppedString(), 0);
} }
public Collection<XmppAxolotlSession> findOwnSessions() { private Set<XmppAxolotlSession> findOwnSessions() {
AxolotlAddress ownAddress = getAddressForJid(account.getJid().toBareJid()); AxolotlAddress ownAddress = getAddressForJid(account.getJid().toBareJid());
ArrayList<XmppAxolotlSession> s = new ArrayList<>(this.sessions.getAll(ownAddress).values()); return new HashSet<>(this.sessions.getAll(ownAddress).values());
Collections.sort(s);
return s;
} }
private Set<XmppAxolotlSession> findSessionsForContact(Contact contact) {
public Collection<XmppAxolotlSession> findSessionsForContact(Contact contact) {
AxolotlAddress contactAddress = getAddressForJid(contact.getJid()); AxolotlAddress contactAddress = getAddressForJid(contact.getJid());
ArrayList<XmppAxolotlSession> s = new ArrayList<>(this.sessions.getAll(contactAddress).values()); return new HashSet<>(this.sessions.getAll(contactAddress).values());
Collections.sort(s);
return s;
} }
private Set<XmppAxolotlSession> findSessionsForConversation(Conversation conversation) { private Set<XmppAxolotlSession> findSessionsForConversation(Conversation conversation) {
@ -336,6 +307,22 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
return sessions; return sessions;
} }
public Set<String> getFingerprintsForOwnSessions() {
Set<String> fingerprints = new HashSet<>();
for (XmppAxolotlSession session : findOwnSessions()) {
fingerprints.add(session.getFingerprint());
}
return fingerprints;
}
public Set<String> getFingerprintsForContact(final Contact contact) {
Set<String> fingerprints = new HashSet<>();
for (XmppAxolotlSession session : findSessionsForContact(contact)) {
fingerprints.add(session.getFingerprint());
}
return fingerprints;
}
private boolean hasAny(Jid jid) { private boolean hasAny(Jid jid) {
return sessions.hasAny(getAddressForJid(jid)); return sessions.hasAny(getAddressForJid(jid));
} }
@ -364,66 +351,62 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
return axolotlStore.getLocalRegistrationId(); return axolotlStore.getLocalRegistrationId();
} }
public AxolotlAddress getOwnAxolotlAddress() {
return new AxolotlAddress(account.getJid().toBareJid().toPreppedString(),getOwnDeviceId());
}
public Set<Integer> getOwnDeviceIds() { public Set<Integer> getOwnDeviceIds() {
return this.deviceIds.get(account.getJid().toBareJid()); return this.deviceIds.get(account.getJid().toBareJid());
} }
private void setTrustOnSessions(final Jid jid, @NonNull final Set<Integer> deviceIds,
final XmppAxolotlSession.Trust from,
final XmppAxolotlSession.Trust to) {
for (Integer deviceId : deviceIds) {
AxolotlAddress address = new AxolotlAddress(jid.toBareJid().toPreppedString(), deviceId);
XmppAxolotlSession session = sessions.get(address);
if (session != null && session.getFingerprint() != null
&& session.getTrust() == from) {
session.setTrust(to);
}
}
}
public void registerDevices(final Jid jid, @NonNull final Set<Integer> deviceIds) { public void registerDevices(final Jid jid, @NonNull final Set<Integer> deviceIds) {
boolean me = jid.toBareJid().equals(account.getJid().toBareJid()); if (jid.toBareJid().equals(account.getJid().toBareJid())) {
if (me && ownPushPending.getAndSet(false)) { if (!deviceIds.isEmpty()) {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": ignoring own device update because of pending push"); Log.d(Config.LOGTAG, getLogprefix(account) + "Received non-empty own device list. Resetting publish attempts and pepBroken status.");
return; pepBroken = false;
} numPublishTriesOnEmptyPep = 0;
boolean needsPublishing = me && !deviceIds.contains(getOwnDeviceId());
if (me) {
deviceIds.remove(getOwnDeviceId());
}
Set<Integer> expiredDevices = new HashSet<>(axolotlStore.getSubDeviceSessions(jid.toBareJid().toPreppedString()));
expiredDevices.removeAll(deviceIds);
for (Integer deviceId : expiredDevices) {
AxolotlAddress address = new AxolotlAddress(jid.toBareJid().toPreppedString(), deviceId);
XmppAxolotlSession session = sessions.get(address);
if (session != null && session.getFingerprint() != null) {
if (session.getTrust().isActive()) {
session.setTrust(session.getTrust().toInactive());
}
} }
} if (deviceIds.contains(getOwnDeviceId())) {
Set<Integer> newDevices = new HashSet<>(deviceIds); deviceIds.remove(getOwnDeviceId());
for (Integer deviceId : newDevices) { } else {
AxolotlAddress address = new AxolotlAddress(jid.toBareJid().toPreppedString(), deviceId); publishOwnDeviceId(deviceIds);
XmppAxolotlSession session = sessions.get(address);
if (session != null && session.getFingerprint() != null) {
if (!session.getTrust().isActive()) {
Log.d(Config.LOGTAG,"reactivating device with fingerprint "+session.getFingerprint());
session.setTrust(session.getTrust().toActive());
}
}
}
if (me) {
if (Config.OMEMO_AUTO_EXPIRY != 0) {
needsPublishing |= deviceIds.removeAll(getExpiredDevices());
} }
for (Integer deviceId : deviceIds) { for (Integer deviceId : deviceIds) {
AxolotlAddress ownDeviceAddress = new AxolotlAddress(jid.toBareJid().toPreppedString(), deviceId); AxolotlAddress ownDeviceAddress = new AxolotlAddress(jid.toBareJid().toPreppedString(), deviceId);
if (sessions.get(ownDeviceAddress) == null) { if (sessions.get(ownDeviceAddress) == null) {
FetchStatus status = fetchStatusMap.get(ownDeviceAddress); buildSessionFromPEP(ownDeviceAddress);
if (status == null || status == FetchStatus.TIMEOUT) {
fetchStatusMap.put(ownDeviceAddress, FetchStatus.PENDING);
this.buildSessionFromPEP(ownDeviceAddress);
}
} }
} }
if (needsPublishing) {
publishOwnDeviceId(deviceIds);
}
} }
Set<Integer> expiredDevices = new HashSet<>(axolotlStore.getSubDeviceSessions(jid.toBareJid().toPreppedString()));
expiredDevices.removeAll(deviceIds);
setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.TRUSTED,
XmppAxolotlSession.Trust.INACTIVE_TRUSTED);
setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.TRUSTED_X509,
XmppAxolotlSession.Trust.INACTIVE_TRUSTED_X509);
setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.UNDECIDED,
XmppAxolotlSession.Trust.INACTIVE_UNDECIDED);
setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.UNTRUSTED,
XmppAxolotlSession.Trust.INACTIVE_UNTRUSTED);
Set<Integer> newDevices = new HashSet<>(deviceIds);
setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_TRUSTED,
XmppAxolotlSession.Trust.TRUSTED);
setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_TRUSTED_X509,
XmppAxolotlSession.Trust.TRUSTED_X509);
setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_UNDECIDED,
XmppAxolotlSession.Trust.UNDECIDED);
setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_UNTRUSTED,
XmppAxolotlSession.Trust.UNTRUSTED);
this.deviceIds.put(jid, deviceIds); this.deviceIds.put(jid, deviceIds);
mXmppConnectionService.updateConversationUi(); //update the lock icon
mXmppConnectionService.keyStatusUpdated(null); mXmppConnectionService.keyStatusUpdated(null);
} }
@ -436,13 +419,16 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
deviceIds.add(getOwnDeviceId()); deviceIds.add(getOwnDeviceId());
IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds); IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds);
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Wiping all other devices from Pep:" + publish); Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Wiping all other devices from Pep:" + publish);
mXmppConnectionService.sendIqPacket(account, publish, null); mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
@Override
public void onIqPacketReceived(Account account, IqPacket packet) {
// TODO: implement this!
}
});
} }
public void distrustFingerprint(final String fingerprint) { public void purgeKey(final String fingerprint) {
final String fp = fingerprint.replaceAll("\\s", ""); axolotlStore.setFingerprintTrust(fingerprint.replaceAll("\\s", ""), XmppAxolotlSession.Trust.COMPROMISED);
final FingerprintStatus fingerprintStatus = axolotlStore.getFingerprintStatus(fp);
axolotlStore.setFingerprintStatus(fp,fingerprintStatus.toUntrusted());
} }
public void publishOwnDeviceIdIfNeeded() { public void publishOwnDeviceIdIfNeeded() {
@ -459,62 +445,42 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
} else { } else {
Element item = mXmppConnectionService.getIqParser().getItem(packet); Element item = mXmppConnectionService.getIqParser().getItem(packet);
Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item);
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": retrieved own device list: "+deviceIds); if (!deviceIds.contains(getOwnDeviceId())) {
registerDevices(account.getJid().toBareJid(),deviceIds); publishOwnDeviceId(deviceIds);
}
}
});
}
private Set<Integer> getExpiredDevices() {
Set<Integer> devices = new HashSet<>();
for(XmppAxolotlSession session : findOwnSessions()) {
if (session.getTrust().isActive()) {
long diff = System.currentTimeMillis() - session.getTrust().getLastActivation();
if (diff > Config.OMEMO_AUTO_EXPIRY) {
long lastMessageDiff = System.currentTimeMillis() - mXmppConnectionService.databaseBackend.getLastTimeFingerprintUsed(account,session.getFingerprint());
long hours = Math.round(lastMessageDiff/(1000*60.0*60.0));
if (lastMessageDiff > Config.OMEMO_AUTO_EXPIRY) {
devices.add(session.getRemoteAddress().getDeviceId());
session.setTrust(session.getTrust().toInactive());
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": added own device " + session.getFingerprint() + " to list of expired devices. Last message received "+hours+" hours ago");
} else {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": own device "+session.getFingerprint()+" was active "+hours+" hours ago");
} }
} }
} }
} });
return devices;
} }
public void publishOwnDeviceId(Set<Integer> deviceIds) { public void publishOwnDeviceId(Set<Integer> deviceIds) {
Set<Integer> deviceIdsCopy = new HashSet<>(deviceIds); Set<Integer> deviceIdsCopy = new HashSet<>(deviceIds);
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "publishing own device ids"); if (!deviceIdsCopy.contains(getOwnDeviceId())) {
if (deviceIdsCopy.isEmpty()) { Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Own device " + getOwnDeviceId() + " not in PEP devicelist.");
if (numPublishTriesOnEmptyPep >= publishTriesThreshold) { if (deviceIdsCopy.isEmpty()) {
Log.w(Config.LOGTAG, getLogprefix(account) + "Own device publish attempt threshold exceeded, aborting..."); if (numPublishTriesOnEmptyPep >= publishTriesThreshold) {
pepBroken = true; Log.w(Config.LOGTAG, getLogprefix(account) + "Own device publish attempt threshold exceeded, aborting...");
return;
} else {
numPublishTriesOnEmptyPep++;
Log.w(Config.LOGTAG, getLogprefix(account) + "Own device list empty, attempting to publish (try " + numPublishTriesOnEmptyPep + ")");
}
} else {
numPublishTriesOnEmptyPep = 0;
}
deviceIdsCopy.add(getOwnDeviceId());
IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIdsCopy);
ownPushPending.set(true);
mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
@Override
public void onIqPacketReceived(Account account, IqPacket packet) {
ownPushPending.set(false);
if (packet.getType() == IqPacket.TYPE.ERROR) {
pepBroken = true; pepBroken = true;
Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing own device id" + packet.findChild("error")); return;
} else {
numPublishTriesOnEmptyPep++;
Log.w(Config.LOGTAG, getLogprefix(account) + "Own device list empty, attempting to publish (try " + numPublishTriesOnEmptyPep + ")");
} }
} else {
numPublishTriesOnEmptyPep = 0;
} }
}); deviceIdsCopy.add(getOwnDeviceId());
IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIdsCopy);
mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
@Override
public void onIqPacketReceived(Account account, IqPacket packet) {
if (packet.getType() == IqPacket.TYPE.ERROR) {
pepBroken = true;
Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing own device id" + packet.findChild("error"));
}
}
});
}
} }
public void publishDeviceVerificationAndBundle(final SignedPreKeyRecord signedPreKeyRecord, public void publishDeviceVerificationAndBundle(final SignedPreKeyRecord signedPreKeyRecord,
@ -726,16 +692,16 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
return jids; return jids;
} }
public FingerprintStatus getFingerprintTrust(String fingerprint) { public XmppAxolotlSession.Trust getFingerprintTrust(String fingerprint) {
return axolotlStore.getFingerprintStatus(fingerprint); return axolotlStore.getFingerprintTrust(fingerprint);
} }
public X509Certificate getFingerprintCertificate(String fingerprint) { public X509Certificate getFingerprintCertificate(String fingerprint) {
return axolotlStore.getFingerprintCertificate(fingerprint); return axolotlStore.getFingerprintCertificate(fingerprint);
} }
public void setFingerprintTrust(String fingerprint, FingerprintStatus status) { public void setFingerprintTrust(String fingerprint, XmppAxolotlSession.Trust trust) {
axolotlStore.setFingerprintStatus(fingerprint, status); axolotlStore.setFingerprintTrust(fingerprint, trust);
} }
private void verifySessionWithPEP(final XmppAxolotlSession session) { private void verifySessionWithPEP(final XmppAxolotlSession session) {
@ -758,7 +724,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
mXmppConnectionService.getMemorizingTrustManager().getNonInteractive().checkClientTrusted(verification.first, "RSA"); mXmppConnectionService.getMemorizingTrustManager().getNonInteractive().checkClientTrusted(verification.first, "RSA");
String fingerprint = session.getFingerprint(); String fingerprint = session.getFingerprint();
Log.d(Config.LOGTAG, "verified session with x.509 signature. fingerprint was: "+fingerprint); Log.d(Config.LOGTAG, "verified session with x.509 signature. fingerprint was: "+fingerprint);
setFingerprintTrust(fingerprint, FingerprintStatus.createActiveVerified(true)); setFingerprintTrust(fingerprint, XmppAxolotlSession.Trust.TRUSTED_X509);
axolotlStore.setFingerprintCertificate(fingerprint, verification.first[0]); axolotlStore.setFingerprintCertificate(fingerprint, verification.first[0]);
fetchStatusMap.put(address, FetchStatus.SUCCESS_VERIFIED); fetchStatusMap.put(address, FetchStatus.SUCCESS_VERIFIED);
Bundle information = CryptoHelper.extractCertificateInformation(verification.first[0]); Bundle information = CryptoHelper.extractCertificateInformation(verification.first[0]);
@ -792,44 +758,25 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
} }
} }
private final Set<Integer> PREVIOUSLY_REMOVED_FROM_ANNOUNCEMENT = new HashSet<>();
private void finishBuildingSessionsFromPEP(final AxolotlAddress address) { private void finishBuildingSessionsFromPEP(final AxolotlAddress address) {
AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toPreppedString(), 0); AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toPreppedString(), 0);
Map<Integer, FetchStatus> own = fetchStatusMap.getAll(ownAddress); if (!fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING)
Map<Integer, FetchStatus> remote = fetchStatusMap.getAll(address); && !fetchStatusMap.getAll(address).containsValue(FetchStatus.PENDING)) {
if (!own.containsValue(FetchStatus.PENDING) && !remote.containsValue(FetchStatus.PENDING)) {
FetchStatus report = null; FetchStatus report = null;
if (own.containsValue(FetchStatus.SUCCESS) || remote.containsValue(FetchStatus.SUCCESS)) { if (fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.SUCCESS_VERIFIED)
report = FetchStatus.SUCCESS; | fetchStatusMap.getAll(address).containsValue(FetchStatus.SUCCESS_VERIFIED)) {
} else if (own.containsValue(FetchStatus.SUCCESS_VERIFIED) || remote.containsValue(FetchStatus.SUCCESS_VERIFIED)) {
report = FetchStatus.SUCCESS_VERIFIED; report = FetchStatus.SUCCESS_VERIFIED;
} else if (own.containsValue(FetchStatus.SUCCESS_TRUSTED) || remote.containsValue(FetchStatus.SUCCESS_TRUSTED)) { } else if (fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.ERROR)
report = FetchStatus.SUCCESS_TRUSTED; || fetchStatusMap.getAll(address).containsValue(FetchStatus.ERROR)) {
} else if (own.containsValue(FetchStatus.ERROR) || remote.containsValue(FetchStatus.ERROR)) {
report = FetchStatus.ERROR; report = FetchStatus.ERROR;
} }
mXmppConnectionService.keyStatusUpdated(report); mXmppConnectionService.keyStatusUpdated(report);
} }
if (Config.REMOVE_BROKEN_DEVICES) {
Set<Integer> ownDeviceIds = new HashSet<>(getOwnDeviceIds());
boolean publish = false;
for (Map.Entry<Integer, FetchStatus> entry : own.entrySet()) {
int id = entry.getKey();
if (entry.getValue() == FetchStatus.ERROR && PREVIOUSLY_REMOVED_FROM_ANNOUNCEMENT.add(id) && ownDeviceIds.remove(id)) {
publish = true;
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": error fetching own device with id " + id + ". removing from announcement");
}
}
if (publish) {
publishOwnDeviceId(ownDeviceIds);
}
}
} }
private void buildSessionFromPEP(final AxolotlAddress address) { private void buildSessionFromPEP(final AxolotlAddress address) {
Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building new session for " + address.toString()); Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building new sesstion for " + address.toString());
if (address.equals(getOwnAxolotlAddress())) { if (address.getDeviceId() == getOwnDeviceId()) {
throw new AssertionError("We should NEVER build a session with ourselves. What happened here?!"); throw new AssertionError("We should NEVER build a session with ourselves. What happened here?!");
} }
@ -876,16 +823,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
if (Config.X509_VERIFICATION) { if (Config.X509_VERIFICATION) {
verifySessionWithPEP(session); verifySessionWithPEP(session);
} else { } else {
FingerprintStatus status = getFingerprintTrust(bundle.getIdentityKey().getFingerprint().replaceAll("\\s","")); fetchStatusMap.put(address, FetchStatus.SUCCESS);
FetchStatus fetchStatus;
if (status != null && status.isVerified()) {
fetchStatus = FetchStatus.SUCCESS_VERIFIED;
} else if (status != null && status.isTrusted()) {
fetchStatus = FetchStatus.SUCCESS_TRUSTED;
} else {
fetchStatus = FetchStatus.SUCCESS;
}
fetchStatusMap.put(address, fetchStatus);
finishBuildingSessionsFromPEP(address); finishBuildingSessionsFromPEP(address);
} }
} catch (UntrustedIdentityException | InvalidKeyException e) { } catch (UntrustedIdentityException | InvalidKeyException e) {
@ -912,7 +850,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Finding devices without session for " + jid); Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Finding devices without session for " + jid);
if (deviceIds.get(jid) != null) { if (deviceIds.get(jid) != null) {
for (Integer foreignId : this.deviceIds.get(jid)) { for (Integer foreignId : this.deviceIds.get(jid)) {
AxolotlAddress address = new AxolotlAddress(jid.toPreppedString(), foreignId); AxolotlAddress address = new AxolotlAddress(jid.toString(), foreignId);
if (sessions.get(address) == null) { if (sessions.get(address) == null) {
IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey();
if (identityKey != null) { if (identityKey != null) {
@ -983,8 +921,8 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
sessions.addAll(findOwnSessions()); sessions.addAll(findOwnSessions());
boolean verified = false; boolean verified = false;
for(XmppAxolotlSession session : sessions) { for(XmppAxolotlSession session : sessions) {
if (session.getTrust().isTrustedAndActive()) { if (session.getTrust().trusted()) {
if (session.getTrust().getTrust() == FingerprintStatus.Trust.VERIFIED_X509) { if (session.getTrust() == XmppAxolotlSession.Trust.TRUSTED_X509) {
verified = true; verified = true;
} else { } else {
return false; return false;
@ -1009,39 +947,46 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
} }
@Nullable @Nullable
private boolean buildHeader(XmppAxolotlMessage axolotlMessage, Conversation conversation) { private XmppAxolotlMessage buildHeader(Conversation conversation) {
final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(
account.getJid().toBareJid(), getOwnDeviceId());
Set<XmppAxolotlSession> remoteSessions = findSessionsForConversation(conversation); Set<XmppAxolotlSession> remoteSessions = findSessionsForConversation(conversation);
Collection<XmppAxolotlSession> ownSessions = findOwnSessions(); Set<XmppAxolotlSession> ownSessions = findOwnSessions();
if (remoteSessions.isEmpty()) { if (remoteSessions.isEmpty()) {
return false; return null;
} }
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building axolotl foreign keyElements...");
for (XmppAxolotlSession session : remoteSessions) { for (XmppAxolotlSession session : remoteSessions) {
Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account) + session.getRemoteAddress().toString());
axolotlMessage.addDevice(session); axolotlMessage.addDevice(session);
} }
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building axolotl own keyElements...");
for (XmppAxolotlSession session : ownSessions) { for (XmppAxolotlSession session : ownSessions) {
Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account) + session.getRemoteAddress().toString());
axolotlMessage.addDevice(session); axolotlMessage.addDevice(session);
} }
return true; return axolotlMessage;
} }
@Nullable @Nullable
public XmppAxolotlMessage encrypt(Message message) { public XmppAxolotlMessage encrypt(Message message) {
final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().toBareJid(), getOwnDeviceId()); XmppAxolotlMessage axolotlMessage = buildHeader(message.getConversation());
final String content;
if (message.hasFileOnRemoteHost()) { if (axolotlMessage != null) {
content = message.getFileParams().url.toString(); final String content;
} else { if (message.hasFileOnRemoteHost()) {
content = message.getBody(); content = message.getFileParams().url.toString();
} } else {
try { content = message.getBody();
axolotlMessage.encrypt(content); }
} catch (CryptoFailedException e) { try {
Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to encrypt message: " + e.getMessage()); axolotlMessage.encrypt(content);
return null; } catch (CryptoFailedException e) {
} Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to encrypt message: " + e.getMessage());
if (!buildHeader(axolotlMessage,message.getConversation())) { return null;
return null; }
} }
return axolotlMessage; return axolotlMessage;
@ -1068,12 +1013,8 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
executor.execute(new Runnable() { executor.execute(new Runnable() {
@Override @Override
public void run() { public void run() {
final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().toBareJid(), getOwnDeviceId()); XmppAxolotlMessage axolotlMessage = buildHeader(conversation);
if (buildHeader(axolotlMessage,conversation)) { onMessageCreatedCallback.run(axolotlMessage);
onMessageCreatedCallback.run(axolotlMessage);
} else {
onMessageCreatedCallback.run(null);
}
} }
}); });
} }
@ -1097,7 +1038,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
} }
private XmppAxolotlSession getReceivingSession(XmppAxolotlMessage message) { private XmppAxolotlSession getReceivingSession(XmppAxolotlMessage message) {
AxolotlAddress senderAddress = new AxolotlAddress(message.getFrom().toPreppedString(), AxolotlAddress senderAddress = new AxolotlAddress(message.getFrom().toString(),
message.getSenderDeviceId()); message.getSenderDeviceId());
XmppAxolotlSession session = sessions.get(senderAddress); XmppAxolotlSession session = sessions.get(senderAddress);
if (session == null) { if (session == null) {
@ -1122,7 +1063,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
session.resetPreKeyId(); session.resetPreKeyId();
} }
} catch (CryptoFailedException e) { } catch (CryptoFailedException e) {
Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to decrypt message from "+message.getFrom()+": " + e.getMessage()); Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to decrypt message: " + e.getMessage());
} }
if (session.isFresh() && plaintextMessage != null) { if (session.isFresh() && plaintextMessage != null) {
@ -1136,12 +1077,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage; XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage;
XmppAxolotlSession session = getReceivingSession(message); XmppAxolotlSession session = getReceivingSession(message);
try { keyTransportMessage = message.getParameters(session, getOwnDeviceId());
keyTransportMessage = message.getParameters(session, getOwnDeviceId());
} catch (CryptoFailedException e) {
Log.d(Config.LOGTAG,"could not decrypt keyTransport message "+e.getMessage());
keyTransportMessage = null;
}
if (session.isFresh() && keyTransportMessage != null) { if (session.isFresh() && keyTransportMessage != null) {
putFreshSession(session); putFreshSession(session);

View file

@ -1,11 +1,6 @@
package eu.siacs.conversations.crypto.axolotl; package eu.siacs.conversations.crypto.axolotl;
public class CryptoFailedException extends Exception { public class CryptoFailedException extends Exception {
public CryptoFailedException(String msg) {
super(msg);
}
public CryptoFailedException(Exception e){ public CryptoFailedException(Exception e){
super(e); super(e);
} }

View file

@ -1,180 +0,0 @@
package eu.siacs.conversations.crypto.axolotl;
import android.content.ContentValues;
import android.database.Cursor;
public class FingerprintStatus implements Comparable<FingerprintStatus> {
private static final long DO_NOT_OVERWRITE = -1;
private Trust trust = Trust.UNTRUSTED;
private boolean active = false;
private long lastActivation = DO_NOT_OVERWRITE;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
FingerprintStatus that = (FingerprintStatus) o;
return active == that.active && trust == that.trust;
}
@Override
public int hashCode() {
int result = trust.hashCode();
result = 31 * result + (active ? 1 : 0);
return result;
}
private FingerprintStatus() {
}
public ContentValues toContentValues() {
final ContentValues contentValues = new ContentValues();
contentValues.put(SQLiteAxolotlStore.TRUST,trust.toString());
contentValues.put(SQLiteAxolotlStore.ACTIVE,active ? 1 : 0);
if (lastActivation != DO_NOT_OVERWRITE) {
contentValues.put(SQLiteAxolotlStore.LAST_ACTIVATION,lastActivation);
}
return contentValues;
}
public static FingerprintStatus fromCursor(Cursor cursor) {
final FingerprintStatus status = new FingerprintStatus();
try {
status.trust = Trust.valueOf(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.TRUST)));
} catch(IllegalArgumentException e) {
status.trust = Trust.UNTRUSTED;
}
status.active = cursor.getInt(cursor.getColumnIndex(SQLiteAxolotlStore.ACTIVE)) > 0;
status.lastActivation = cursor.getLong(cursor.getColumnIndex(SQLiteAxolotlStore.LAST_ACTIVATION));
return status;
}
public static FingerprintStatus createActiveUndecided() {
final FingerprintStatus status = new FingerprintStatus();
status.trust = Trust.UNDECIDED;
status.active = true;
status.lastActivation = System.currentTimeMillis();
return status;
}
public static FingerprintStatus createActiveTrusted() {
final FingerprintStatus status = new FingerprintStatus();
status.trust = Trust.TRUSTED;
status.active = true;
status.lastActivation = System.currentTimeMillis();
return status;
}
public static FingerprintStatus createActiveVerified(boolean x509) {
final FingerprintStatus status = new FingerprintStatus();
status.trust = x509 ? Trust.VERIFIED_X509 : Trust.VERIFIED;
status.active = true;
return status;
}
public static FingerprintStatus createActive(boolean trusted) {
final FingerprintStatus status = new FingerprintStatus();
status.trust = trusted ? Trust.TRUSTED : Trust.UNTRUSTED;
status.active = true;
return status;
}
public boolean isTrustedAndActive() {
return active && isTrusted();
}
public boolean isTrusted() {
return trust == Trust.TRUSTED || isVerified();
}
public boolean isVerified() {
return trust == Trust.VERIFIED || trust == Trust.VERIFIED_X509;
}
public boolean isCompromised() {
return trust == Trust.COMPROMISED;
}
public boolean isActive() {
return active;
}
public FingerprintStatus toActive() {
FingerprintStatus status = new FingerprintStatus();
status.trust = trust;
if (!status.active) {
status.lastActivation = System.currentTimeMillis();
}
status.active = true;
return status;
}
public FingerprintStatus toInactive() {
FingerprintStatus status = new FingerprintStatus();
status.trust = trust;
status.active = false;
return status;
}
public Trust getTrust() {
return trust;
}
public FingerprintStatus toVerified() {
FingerprintStatus status = new FingerprintStatus();
status.active = active;
status.trust = Trust.VERIFIED;
return status;
}
public FingerprintStatus toUntrusted() {
FingerprintStatus status = new FingerprintStatus();
status.active = active;
status.trust = Trust.UNTRUSTED;
return status;
}
public static FingerprintStatus createInactiveVerified() {
final FingerprintStatus status = new FingerprintStatus();
status.trust = Trust.VERIFIED;
status.active = false;
return status;
}
@Override
public int compareTo(FingerprintStatus o) {
if (active == o.active) {
if (lastActivation > o.lastActivation) {
return -1;
} else if (lastActivation < o.lastActivation) {
return 1;
} else {
return 0;
}
} else if (active){
return -1;
} else {
return 1;
}
}
public long getLastActivation() {
return lastActivation;
}
public enum Trust {
COMPROMISED,
UNDECIDED,
UNTRUSTED,
TRUSTED,
VERIFIED,
VERIFIED_X509
}
}

View file

@ -21,10 +21,7 @@ import java.util.Set;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
public class SQLiteAxolotlStore implements AxolotlStore { public class SQLiteAxolotlStore implements AxolotlStore {
@ -38,10 +35,7 @@ public class SQLiteAxolotlStore implements AxolotlStore {
public static final String KEY = "key"; public static final String KEY = "key";
public static final String FINGERPRINT = "fingerprint"; public static final String FINGERPRINT = "fingerprint";
public static final String NAME = "name"; public static final String NAME = "name";
public static final String TRUSTED = "trusted"; //no longer used public static final String TRUSTED = "trusted";
public static final String TRUST = "trust";
public static final String ACTIVE = "active";
public static final String LAST_ACTIVATION = "last_activation";
public static final String OWN = "ownkey"; public static final String OWN = "ownkey";
public static final String CERTIFICATE = "certificate"; public static final String CERTIFICATE = "certificate";
@ -57,11 +51,11 @@ public class SQLiteAxolotlStore implements AxolotlStore {
private int localRegistrationId; private int localRegistrationId;
private int currentPreKeyId = 0; private int currentPreKeyId = 0;
private final LruCache<String, FingerprintStatus> trustCache = private final LruCache<String, XmppAxolotlSession.Trust> trustCache =
new LruCache<String, FingerprintStatus>(NUM_TRUSTS_TO_CACHE) { new LruCache<String, XmppAxolotlSession.Trust>(NUM_TRUSTS_TO_CACHE) {
@Override @Override
protected FingerprintStatus create(String fingerprint) { protected XmppAxolotlSession.Trust create(String fingerprint) {
return mXmppConnectionService.databaseBackend.getFingerprintStatus(account, fingerprint); return mXmppConnectionService.databaseBackend.isIdentityKeyTrusted(account, fingerprint);
} }
}; };
@ -191,20 +185,7 @@ public class SQLiteAxolotlStore implements AxolotlStore {
@Override @Override
public void saveIdentity(String name, IdentityKey identityKey) { public void saveIdentity(String name, IdentityKey identityKey) {
if (!mXmppConnectionService.databaseBackend.loadIdentityKeys(account, name).contains(identityKey)) { if (!mXmppConnectionService.databaseBackend.loadIdentityKeys(account, name).contains(identityKey)) {
String fingerprint = identityKey.getFingerprint().replaceAll("\\s", ""); mXmppConnectionService.databaseBackend.storeIdentityKey(account, name, identityKey);
FingerprintStatus status = getFingerprintStatus(fingerprint);
if (status == null) {
if (mXmppConnectionService.blindTrustBeforeVerification() && !account.getAxolotlService().hasVerifiedKeys(name)) {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": blindly trusted "+fingerprint+" of "+name);
status = FingerprintStatus.createActiveTrusted();
} else {
status = FingerprintStatus.createActiveUndecided();
}
} else {
status = status.toActive();
}
mXmppConnectionService.databaseBackend.storeIdentityKey(account, name, identityKey, status);
trustCache.remove(fingerprint);
} }
} }
@ -227,12 +208,12 @@ public class SQLiteAxolotlStore implements AxolotlStore {
return true; return true;
} }
public FingerprintStatus getFingerprintStatus(String fingerprint) { public XmppAxolotlSession.Trust getFingerprintTrust(String fingerprint) {
return (fingerprint == null)? null : trustCache.get(fingerprint); return (fingerprint == null)? null : trustCache.get(fingerprint);
} }
public void setFingerprintStatus(String fingerprint, FingerprintStatus status) { public void setFingerprintTrust(String fingerprint, XmppAxolotlSession.Trust trust) {
mXmppConnectionService.databaseBackend.setIdentityKeyTrust(account, fingerprint, status); mXmppConnectionService.databaseBackend.setIdentityKeyTrust(account, fingerprint, trust);
trustCache.remove(fingerprint); trustCache.remove(fingerprint);
} }
@ -244,8 +225,8 @@ public class SQLiteAxolotlStore implements AxolotlStore {
return mXmppConnectionService.databaseBackend.getIdentityKeyCertifcate(account, fingerprint); return mXmppConnectionService.databaseBackend.getIdentityKeyCertifcate(account, fingerprint);
} }
public Set<IdentityKey> getContactKeysWithTrust(String bareJid, FingerprintStatus status) { public Set<IdentityKey> getContactKeysWithTrust(String bareJid, XmppAxolotlSession.Trust trust) {
return mXmppConnectionService.databaseBackend.loadIdentityKeys(account, bareJid, status); return mXmppConnectionService.databaseBackend.loadIdentityKeys(account, bareJid, trust);
} }
public long getContactNumTrustedKeys(String bareJid) { public long getContactNumTrustedKeys(String bareJid) {
@ -447,8 +428,4 @@ public class SQLiteAxolotlStore implements AxolotlStore {
public void removeSignedPreKey(int signedPreKeyId) { public void removeSignedPreKey(int signedPreKeyId) {
mXmppConnectionService.databaseBackend.deleteSignedPreKey(account, signedPreKeyId); mXmppConnectionService.databaseBackend.deleteSignedPreKey(account, signedPreKeyId);
} }
public void preVerifyFingerprint(Account account, String name, String fingerprint) {
mXmppConnectionService.databaseBackend.storePreVerification(account,name,fingerprint,FingerprintStatus.createInactiveVerified());
}
} }

View file

@ -3,7 +3,6 @@ package eu.siacs.conversations.crypto.axolotl;
import android.util.Base64; import android.util.Base64;
import android.util.Log; import android.util.Log;
import java.security.InvalidAlgorithmParameterException; import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException; import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
@ -41,9 +40,8 @@ public class XmppAxolotlMessage {
private byte[] innerKey; private byte[] innerKey;
private byte[] ciphertext = null; private byte[] ciphertext = null;
private byte[] authtagPlusInnerKey = null;
private byte[] iv = null; private byte[] iv = null;
private final Map<Integer, XmppAxolotlSession.AxolotlKey> keys; private final Map<Integer, byte[]> keys;
private final Jid from; private final Jid from;
private final int sourceDeviceId; private final int sourceDeviceId;
@ -106,8 +104,7 @@ public class XmppAxolotlMessage {
try { try {
Integer recipientId = Integer.parseInt(keyElement.getAttribute(REMOTEID)); Integer recipientId = Integer.parseInt(keyElement.getAttribute(REMOTEID));
byte[] key = Base64.decode(keyElement.getContent().trim(), Base64.DEFAULT); byte[] key = Base64.decode(keyElement.getContent().trim(), Base64.DEFAULT);
boolean isPreKey =keyElement.getAttributeAsBoolean("prekey"); this.keys.put(recipientId, key);
this.keys.put(recipientId, new XmppAxolotlSession.AxolotlKey(key,isPreKey));
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
throw new IllegalArgumentException("invalid remote id"); throw new IllegalArgumentException("invalid remote id");
} }
@ -165,15 +162,7 @@ public class XmppAxolotlMessage {
IvParameterSpec ivSpec = new IvParameterSpec(iv); IvParameterSpec ivSpec = new IvParameterSpec(iv);
Cipher cipher = Cipher.getInstance(CIPHERMODE, PROVIDER); Cipher cipher = Cipher.getInstance(CIPHERMODE, PROVIDER);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec); cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec);
this.ciphertext = cipher.doFinal(Config.OMEMO_PADDING ? getPaddedBytes(plaintext) : plaintext.getBytes()); this.ciphertext = cipher.doFinal(plaintext.getBytes());
if (Config.PUT_AUTH_TAG_INTO_KEY && this.ciphertext != null) {
this.authtagPlusInnerKey = new byte[16+16];
byte[] ciphertext = new byte[this.ciphertext.length - 16];
System.arraycopy(this.ciphertext,0,ciphertext,0,ciphertext.length);
System.arraycopy(this.ciphertext,ciphertext.length,authtagPlusInnerKey,16,16);
System.arraycopy(this.innerKey,0,authtagPlusInnerKey,0,this.innerKey.length);
this.ciphertext = ciphertext;
}
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
| IllegalBlockSizeException | BadPaddingException | NoSuchProviderException | IllegalBlockSizeException | BadPaddingException | NoSuchProviderException
| InvalidAlgorithmParameterException e) { | InvalidAlgorithmParameterException e) {
@ -181,22 +170,6 @@ public class XmppAxolotlMessage {
} }
} }
private static byte[] getPaddedBytes(String plaintext) {
int plainLength = plaintext.getBytes().length;
int pad = Math.max(64,(plainLength / 32 + 1) * 32) - plainLength;
SecureRandom random = new SecureRandom();
int left = random.nextInt(pad);
int right = pad - left;
StringBuilder builder = new StringBuilder(plaintext);
for(int i = 0; i < left; ++i) {
builder.insert(0,random.nextBoolean() ? "\t" : " ");
}
for(int i = 0; i < right; ++i) {
builder.append(random.nextBoolean() ? "\t" : " ");
}
return builder.toString().getBytes();
}
public Jid getFrom() { public Jid getFrom() {
return this.from; return this.from;
} }
@ -210,12 +183,7 @@ public class XmppAxolotlMessage {
} }
public void addDevice(XmppAxolotlSession session) { public void addDevice(XmppAxolotlSession session) {
XmppAxolotlSession.AxolotlKey key; byte[] key = session.processSending(innerKey);
if (authtagPlusInnerKey != null) {
key = session.processSending(authtagPlusInnerKey);
} else {
key = session.processSending(innerKey);
}
if (key != null) { if (key != null) {
keys.put(session.getRemoteAddress().getDeviceId(), key); keys.put(session.getRemoteAddress().getDeviceId(), key);
} }
@ -233,33 +201,30 @@ public class XmppAxolotlMessage {
Element encryptionElement = new Element(CONTAINERTAG, AxolotlService.PEP_PREFIX); Element encryptionElement = new Element(CONTAINERTAG, AxolotlService.PEP_PREFIX);
Element headerElement = encryptionElement.addChild(HEADER); Element headerElement = encryptionElement.addChild(HEADER);
headerElement.setAttribute(SOURCEID, sourceDeviceId); headerElement.setAttribute(SOURCEID, sourceDeviceId);
for (Map.Entry<Integer, XmppAxolotlSession.AxolotlKey> keyEntry : keys.entrySet()) { for (Map.Entry<Integer, byte[]> keyEntry : keys.entrySet()) {
Element keyElement = new Element(KEYTAG); Element keyElement = new Element(KEYTAG);
keyElement.setAttribute(REMOTEID, keyEntry.getKey()); keyElement.setAttribute(REMOTEID, keyEntry.getKey());
if (keyEntry.getValue().prekey) { keyElement.setContent(Base64.encodeToString(keyEntry.getValue(), Base64.DEFAULT));
keyElement.setAttribute("prekey","true");
}
keyElement.setContent(Base64.encodeToString(keyEntry.getValue().key, Base64.NO_WRAP));
headerElement.addChild(keyElement); headerElement.addChild(keyElement);
} }
headerElement.addChild(IVTAG).setContent(Base64.encodeToString(iv, Base64.NO_WRAP)); headerElement.addChild(IVTAG).setContent(Base64.encodeToString(iv, Base64.DEFAULT));
if (ciphertext != null) { if (ciphertext != null) {
Element payload = encryptionElement.addChild(PAYLOAD); Element payload = encryptionElement.addChild(PAYLOAD);
payload.setContent(Base64.encodeToString(ciphertext, Base64.NO_WRAP)); payload.setContent(Base64.encodeToString(ciphertext, Base64.DEFAULT));
} }
return encryptionElement; return encryptionElement;
} }
private byte[] unpackKey(XmppAxolotlSession session, Integer sourceDeviceId) throws CryptoFailedException { private byte[] unpackKey(XmppAxolotlSession session, Integer sourceDeviceId) {
XmppAxolotlSession.AxolotlKey encryptedKey = keys.get(sourceDeviceId); byte[] encryptedKey = keys.get(sourceDeviceId);
if (encryptedKey == null) { return (encryptedKey != null) ? session.processReceiving(encryptedKey) : null;
throw new CryptoFailedException("Message was not encrypted for this device");
}
return session.processReceiving(encryptedKey);
} }
public XmppAxolotlKeyTransportMessage getParameters(XmppAxolotlSession session, Integer sourceDeviceId) throws CryptoFailedException { public XmppAxolotlKeyTransportMessage getParameters(XmppAxolotlSession session, Integer sourceDeviceId) {
return new XmppAxolotlKeyTransportMessage(session.getFingerprint(), unpackKey(session, sourceDeviceId), getIV()); byte[] key = unpackKey(session, sourceDeviceId);
return (key != null)
? new XmppAxolotlKeyTransportMessage(session.getFingerprint(), key, getIV())
: null;
} }
public XmppAxolotlPlaintextMessage decrypt(XmppAxolotlSession session, Integer sourceDeviceId) throws CryptoFailedException { public XmppAxolotlPlaintextMessage decrypt(XmppAxolotlSession session, Integer sourceDeviceId) throws CryptoFailedException {
@ -267,19 +232,6 @@ public class XmppAxolotlMessage {
byte[] key = unpackKey(session, sourceDeviceId); byte[] key = unpackKey(session, sourceDeviceId);
if (key != null) { if (key != null) {
try { try {
if (key.length >= 32) {
int authtaglength = key.length - 16;
Log.d(Config.LOGTAG,"found auth tag as part of omemo key");
byte[] newCipherText = new byte[key.length - 16 + ciphertext.length];
byte[] newKey = new byte[16];
System.arraycopy(ciphertext, 0, newCipherText, 0, ciphertext.length);
System.arraycopy(key, 16, newCipherText, ciphertext.length, authtaglength);
System.arraycopy(key,0,newKey,0,newKey.length);
ciphertext = newCipherText;
key = newKey;
}
Cipher cipher = Cipher.getInstance(CIPHERMODE, PROVIDER); Cipher cipher = Cipher.getInstance(CIPHERMODE, PROVIDER);
SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE); SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE);
IvParameterSpec ivSpec = new IvParameterSpec(iv); IvParameterSpec ivSpec = new IvParameterSpec(iv);
@ -287,7 +239,7 @@ public class XmppAxolotlMessage {
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
String plaintext = new String(cipher.doFinal(ciphertext)); String plaintext = new String(cipher.doFinal(ciphertext));
plaintextMessage = new XmppAxolotlPlaintextMessage(Config.OMEMO_PADDING ? plaintext.trim() : plaintext, session.getFingerprint()); plaintextMessage = new XmppAxolotlPlaintextMessage(plaintext, session.getFingerprint());
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
| InvalidAlgorithmParameterException | IllegalBlockSizeException | InvalidAlgorithmParameterException | IllegalBlockSizeException

View file

@ -4,7 +4,6 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.util.Log; import android.util.Log;
import org.bouncycastle.math.ec.PreCompInfo;
import org.whispersystems.libaxolotl.AxolotlAddress; import org.whispersystems.libaxolotl.AxolotlAddress;
import org.whispersystems.libaxolotl.DuplicateMessageException; import org.whispersystems.libaxolotl.DuplicateMessageException;
import org.whispersystems.libaxolotl.IdentityKey; import org.whispersystems.libaxolotl.IdentityKey;
@ -19,13 +18,14 @@ import org.whispersystems.libaxolotl.UntrustedIdentityException;
import org.whispersystems.libaxolotl.protocol.CiphertextMessage; import org.whispersystems.libaxolotl.protocol.CiphertextMessage;
import org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage; import org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage;
import org.whispersystems.libaxolotl.protocol.WhisperMessage; import org.whispersystems.libaxolotl.protocol.WhisperMessage;
import org.whispersystems.libaxolotl.util.guava.Optional;
import java.util.HashMap;
import java.util.Map;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.utils.CryptoHelper;
public class XmppAxolotlSession implements Comparable<XmppAxolotlSession> { public class XmppAxolotlSession {
private final SessionCipher cipher; private final SessionCipher cipher;
private final SQLiteAxolotlStore sqLiteAxolotlStore; private final SQLiteAxolotlStore sqLiteAxolotlStore;
private final AxolotlAddress remoteAddress; private final AxolotlAddress remoteAddress;
@ -34,6 +34,76 @@ public class XmppAxolotlSession implements Comparable<XmppAxolotlSession> {
private Integer preKeyId = null; private Integer preKeyId = null;
private boolean fresh = true; private boolean fresh = true;
public enum Trust {
UNDECIDED(0),
TRUSTED(1),
UNTRUSTED(2),
COMPROMISED(3),
INACTIVE_TRUSTED(4),
INACTIVE_UNDECIDED(5),
INACTIVE_UNTRUSTED(6),
TRUSTED_X509(7),
INACTIVE_TRUSTED_X509(8);
private static final Map<Integer, Trust> trustsByValue = new HashMap<>();
static {
for (Trust trust : Trust.values()) {
trustsByValue.put(trust.getCode(), trust);
}
}
private final int code;
Trust(int code) {
this.code = code;
}
public int getCode() {
return this.code;
}
public String toString() {
switch (this) {
case UNDECIDED:
return "Trust undecided " + getCode();
case TRUSTED:
return "Trusted " + getCode();
case COMPROMISED:
return "Compromised " + getCode();
case INACTIVE_TRUSTED:
return "Inactive (Trusted)" + getCode();
case INACTIVE_UNDECIDED:
return "Inactive (Undecided)" + getCode();
case INACTIVE_UNTRUSTED:
return "Inactive (Untrusted)" + getCode();
case TRUSTED_X509:
return "Trusted (X509) " + getCode();
case INACTIVE_TRUSTED_X509:
return "Inactive (Trusted (X509)) " + getCode();
case UNTRUSTED:
default:
return "Untrusted " + getCode();
}
}
public static Trust fromBoolean(Boolean trusted) {
return trusted ? TRUSTED : UNTRUSTED;
}
public static Trust fromCode(int code) {
return trustsByValue.get(code);
}
public boolean trusted() {
return this == TRUSTED_X509 || this == TRUSTED;
}
public boolean trustedInactive() {
return this == INACTIVE_TRUSTED_X509 || this == INACTIVE_TRUSTED;
}
}
public XmppAxolotlSession(Account account, SQLiteAxolotlStore store, AxolotlAddress remoteAddress, IdentityKey identityKey) { public XmppAxolotlSession(Account account, SQLiteAxolotlStore store, AxolotlAddress remoteAddress, IdentityKey identityKey) {
this(account, store, remoteAddress); this(account, store, remoteAddress);
this.identityKey = identityKey; this.identityKey = identityKey;
@ -75,86 +145,79 @@ public class XmppAxolotlSession implements Comparable<XmppAxolotlSession> {
this.fresh = false; this.fresh = false;
} }
protected void setTrust(FingerprintStatus status) { protected void setTrust(Trust trust) {
sqLiteAxolotlStore.setFingerprintStatus(getFingerprint(), status); sqLiteAxolotlStore.setFingerprintTrust(getFingerprint(), trust);
} }
public FingerprintStatus getTrust() { protected Trust getTrust() {
FingerprintStatus status = sqLiteAxolotlStore.getFingerprintStatus(getFingerprint()); Trust trust = sqLiteAxolotlStore.getFingerprintTrust(getFingerprint());
return (status == null) ? FingerprintStatus.createActiveUndecided() : status; return (trust == null) ? Trust.UNDECIDED : trust;
} }
@Nullable @Nullable
public byte[] processReceiving(AxolotlKey encryptedKey) throws CryptoFailedException { public byte[] processReceiving(byte[] encryptedKey) {
byte[] plaintext; byte[] plaintext = null;
FingerprintStatus status = getTrust(); Trust trust = getTrust();
if (!status.isCompromised()) { switch (trust) {
try { case INACTIVE_TRUSTED:
CiphertextMessage ciphertextMessage; case UNDECIDED:
case UNTRUSTED:
case TRUSTED:
case INACTIVE_TRUSTED_X509:
case TRUSTED_X509:
try { try {
ciphertextMessage = new PreKeyWhisperMessage(encryptedKey.key); try {
Optional<Integer> optionalPreKeyId = ((PreKeyWhisperMessage) ciphertextMessage).getPreKeyId(); PreKeyWhisperMessage message = new PreKeyWhisperMessage(encryptedKey);
IdentityKey identityKey = ((PreKeyWhisperMessage) ciphertextMessage).getIdentityKey(); if (!message.getPreKeyId().isPresent()) {
if (!optionalPreKeyId.isPresent()) { Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "PreKeyWhisperMessage did not contain a PreKeyId");
throw new CryptoFailedException("PreKeyWhisperMessage did not contain a PreKeyId"); break;
}
Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "PreKeyWhisperMessage received, new session ID:" + message.getSignedPreKeyId() + "/" + message.getPreKeyId());
IdentityKey msgIdentityKey = message.getIdentityKey();
if (this.identityKey != null && !this.identityKey.equals(msgIdentityKey)) {
Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Had session with fingerprint " + this.getFingerprint() + ", received message with fingerprint " + msgIdentityKey.getFingerprint());
} else {
this.identityKey = msgIdentityKey;
plaintext = cipher.decrypt(message);
preKeyId = message.getPreKeyId().get();
}
} catch (InvalidMessageException | InvalidVersionException e) {
Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "WhisperMessage received");
WhisperMessage message = new WhisperMessage(encryptedKey);
plaintext = cipher.decrypt(message);
} catch (InvalidKeyException | InvalidKeyIdException | UntrustedIdentityException e) {
Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error decrypting axolotl header, " + e.getClass().getName() + ": " + e.getMessage());
} }
preKeyId = optionalPreKeyId.get(); } catch (LegacyMessageException | InvalidMessageException | DuplicateMessageException | NoSessionException e) {
if (this.identityKey != null && !this.identityKey.equals(identityKey)) { Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error decrypting axolotl header, " + e.getClass().getName() + ": " + e.getMessage());
throw new CryptoFailedException("Received PreKeyWhisperMessage but preexisting identity key changed."); }
if (plaintext != null) {
if (trust == Trust.INACTIVE_TRUSTED) {
setTrust(Trust.TRUSTED);
} else if (trust == Trust.INACTIVE_TRUSTED_X509) {
setTrust(Trust.TRUSTED_X509);
} }
this.identityKey = identityKey;
} catch (InvalidVersionException | InvalidMessageException e) {
ciphertextMessage = new WhisperMessage(encryptedKey.key);
} }
if (ciphertextMessage instanceof PreKeyWhisperMessage) {
plaintext = cipher.decrypt((PreKeyWhisperMessage) ciphertextMessage); break;
} else {
plaintext = cipher.decrypt((WhisperMessage) ciphertextMessage); case COMPROMISED:
} default:
} catch (InvalidKeyException | LegacyMessageException | InvalidMessageException | DuplicateMessageException | NoSessionException | InvalidKeyIdException | UntrustedIdentityException e) { // ignore
if (!(e instanceof DuplicateMessageException)) { break;
e.printStackTrace();
}
throw new CryptoFailedException("Error decrypting WhisperMessage " + e.getClass().getSimpleName() + ": " + e.getMessage());
}
if (!status.isActive()) {
setTrust(status.toActive());
}
} else {
throw new CryptoFailedException("not encrypting omemo message from fingerprint "+getFingerprint()+" because it was marked as compromised");
} }
return plaintext; return plaintext;
} }
@Nullable @Nullable
public AxolotlKey processSending(@NonNull byte[] outgoingMessage) { public byte[] processSending(@NonNull byte[] outgoingMessage) {
FingerprintStatus status = getTrust(); Trust trust = getTrust();
if (status.isTrustedAndActive()) { if (trust.trusted()) {
CiphertextMessage ciphertextMessage = cipher.encrypt(outgoingMessage); CiphertextMessage ciphertextMessage = cipher.encrypt(outgoingMessage);
return new AxolotlKey(ciphertextMessage.serialize(),ciphertextMessage.getType() == CiphertextMessage.PREKEY_TYPE); return ciphertextMessage.serialize();
} else { } else {
return null; return null;
} }
} }
public Account getAccount() {
return account;
}
@Override
public int compareTo(XmppAxolotlSession o) {
return getTrust().compareTo(o.getTrust());
}
public static class AxolotlKey {
public final byte[] key;
public final boolean prekey;
public AxolotlKey(byte[] key, boolean prekey) {
this.key = key;
this.prekey = prekey;
}
}
} }

View file

@ -1,228 +0,0 @@
package eu.siacs.conversations.crypto.sasl;
import android.annotation.TargetApi;
import android.os.Build;
import android.util.Base64;
import android.util.LruCache;
import org.bouncycastle.crypto.Digest;
import org.bouncycastle.crypto.macs.HMac;
import org.bouncycastle.crypto.params.KeyParameter;
import java.math.BigInteger;
import java.nio.charset.Charset;
import java.security.InvalidKeyException;
import java.security.SecureRandom;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.xml.TagWriter;
@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
abstract class ScramMechanism extends SaslMechanism {
// TODO: When channel binding (SCRAM-SHA1-PLUS) is supported in future, generalize this to indicate support and/or usage.
private final static String GS2_HEADER = "n,,";
private String clientFirstMessageBare;
private final String clientNonce;
private byte[] serverSignature = null;
static HMac HMAC;
static Digest DIGEST;
private static final byte[] CLIENT_KEY_BYTES = "Client Key".getBytes();
private static final byte[] SERVER_KEY_BYTES = "Server Key".getBytes();
private static class KeyPair {
final byte[] clientKey;
final byte[] serverKey;
KeyPair(final byte[] clientKey, final byte[] serverKey) {
this.clientKey = clientKey;
this.serverKey = serverKey;
}
}
static {
CACHE = new LruCache<String, KeyPair>(10) {
protected KeyPair create(final String k) {
// Map keys are "bytesToHex(JID),bytesToHex(password),bytesToHex(salt),iterations".
// Changing any of these values forces a cache miss. `CryptoHelper.bytesToHex()'
// is applied to prevent commas in the strings breaking things.
final String[] kparts = k.split(",", 4);
try {
final byte[] saltedPassword, serverKey, clientKey;
saltedPassword = hi(CryptoHelper.hexToString(kparts[1]).getBytes(),
Base64.decode(CryptoHelper.hexToString(kparts[2]), Base64.DEFAULT), Integer.valueOf(kparts[3]));
serverKey = hmac(saltedPassword, SERVER_KEY_BYTES);
clientKey = hmac(saltedPassword, CLIENT_KEY_BYTES);
return new KeyPair(clientKey, serverKey);
} catch (final InvalidKeyException | NumberFormatException e) {
return null;
}
}
};
}
private static final LruCache<String, KeyPair> CACHE;
protected State state = State.INITIAL;
ScramMechanism(final TagWriter tagWriter, final Account account, final SecureRandom rng) {
super(tagWriter, account, rng);
// This nonce should be different for each authentication attempt.
clientNonce = new BigInteger(100, this.rng).toString(32);
clientFirstMessageBare = "";
}
@Override
public String getClientFirstMessage() {
if (clientFirstMessageBare.isEmpty() && state == State.INITIAL) {
clientFirstMessageBare = "n=" + CryptoHelper.saslEscape(CryptoHelper.saslPrep(account.getUsername())) +
",r=" + this.clientNonce;
state = State.AUTH_TEXT_SENT;
}
return Base64.encodeToString(
(GS2_HEADER + clientFirstMessageBare).getBytes(Charset.defaultCharset()),
Base64.NO_WRAP);
}
@Override
public String getResponse(final String challenge) throws AuthenticationException {
switch (state) {
case AUTH_TEXT_SENT:
if (challenge == null) {
throw new AuthenticationException("challenge can not be null");
}
byte[] serverFirstMessage = Base64.decode(challenge, Base64.DEFAULT);
final Tokenizer tokenizer = new Tokenizer(serverFirstMessage);
String nonce = "";
int iterationCount = -1;
String salt = "";
for (final String token : tokenizer) {
if (token.charAt(1) == '=') {
switch (token.charAt(0)) {
case 'i':
try {
iterationCount = Integer.parseInt(token.substring(2));
} catch (final NumberFormatException e) {
throw new AuthenticationException(e);
}
break;
case 's':
salt = token.substring(2);
break;
case 'r':
nonce = token.substring(2);
break;
case 'm':
/*
* RFC 5802:
* m: This attribute is reserved for future extensibility. In this
* version of SCRAM, its presence in a client or a server message
* MUST cause authentication failure when the attribute is parsed by
* the other end.
*/
throw new AuthenticationException("Server sent reserved token: `m'");
}
}
}
if (iterationCount < 0) {
throw new AuthenticationException("Server did not send iteration count");
}
if (nonce.isEmpty() || !nonce.startsWith(clientNonce)) {
throw new AuthenticationException("Server nonce does not contain client nonce: " + nonce);
}
if (salt.isEmpty()) {
throw new AuthenticationException("Server sent empty salt");
}
final String clientFinalMessageWithoutProof = "c=" + Base64.encodeToString(
GS2_HEADER.getBytes(), Base64.NO_WRAP) + ",r=" + nonce;
final byte[] authMessage = (clientFirstMessageBare + ',' + new String(serverFirstMessage) + ','
+ clientFinalMessageWithoutProof).getBytes();
// Map keys are "bytesToHex(JID),bytesToHex(password),bytesToHex(salt),iterations".
final KeyPair keys = CACHE.get(
CryptoHelper.bytesToHex(account.getJid().toBareJid().toString().getBytes()) + ","
+ CryptoHelper.bytesToHex(account.getPassword().getBytes()) + ","
+ CryptoHelper.bytesToHex(salt.getBytes()) + ","
+ String.valueOf(iterationCount)
);
if (keys == null) {
throw new AuthenticationException("Invalid keys generated");
}
final byte[] clientSignature;
try {
serverSignature = hmac(keys.serverKey, authMessage);
final byte[] storedKey = digest(keys.clientKey);
clientSignature = hmac(storedKey, authMessage);
} catch (final InvalidKeyException e) {
throw new AuthenticationException(e);
}
final byte[] clientProof = new byte[keys.clientKey.length];
for (int i = 0; i < clientProof.length; i++) {
clientProof[i] = (byte) (keys.clientKey[i] ^ clientSignature[i]);
}
final String clientFinalMessage = clientFinalMessageWithoutProof + ",p=" +
Base64.encodeToString(clientProof, Base64.NO_WRAP);
state = State.RESPONSE_SENT;
return Base64.encodeToString(clientFinalMessage.getBytes(), Base64.NO_WRAP);
case RESPONSE_SENT:
try {
final String clientCalculatedServerFinalMessage = "v=" +
Base64.encodeToString(serverSignature, Base64.NO_WRAP);
if (!clientCalculatedServerFinalMessage.equals(new String(Base64.decode(challenge, Base64.DEFAULT)))) {
throw new Exception();
}
state = State.VALID_SERVER_RESPONSE;
return "";
} catch(Exception e) {
throw new AuthenticationException("Server final message does not match calculated final message");
}
default:
throw new InvalidStateException(state);
}
}
private static synchronized byte[] hmac(final byte[] key, final byte[] input)
throws InvalidKeyException {
HMAC.init(new KeyParameter(key));
HMAC.update(input, 0, input.length);
final byte[] out = new byte[HMAC.getMacSize()];
HMAC.doFinal(out, 0);
return out;
}
public static synchronized byte[] digest(byte[] bytes) {
DIGEST.reset();
DIGEST.update(bytes, 0, bytes.length);
final byte[] out = new byte[DIGEST.getDigestSize()];
DIGEST.doFinal(out, 0);
return out;
}
/*
* Hi() is, essentially, PBKDF2 [RFC2898] with HMAC() as the
* pseudorandom function (PRF) and with dkLen == output length of
* HMAC() == output length of H().
*/
private static synchronized byte[] hi(final byte[] key, final byte[] salt, final int iterations)
throws InvalidKeyException {
byte[] u = hmac(key, CryptoHelper.concatenateByteArrays(salt, CryptoHelper.ONE));
byte[] out = u.clone();
for (int i = 1; i < iterations; i++) {
u = hmac(key, u);
for (int j = 0; j < u.length; j++) {
out[j] ^= u[j];
}
}
return out;
}
}

View file

@ -1,21 +1,77 @@
package eu.siacs.conversations.crypto.sasl; package eu.siacs.conversations.crypto.sasl;
import android.util.Base64;
import android.util.LruCache;
import org.bouncycastle.crypto.Digest;
import org.bouncycastle.crypto.digests.SHA1Digest; import org.bouncycastle.crypto.digests.SHA1Digest;
import org.bouncycastle.crypto.macs.HMac; import org.bouncycastle.crypto.macs.HMac;
import org.bouncycastle.crypto.params.KeyParameter;
import java.math.BigInteger;
import java.nio.charset.Charset;
import java.security.InvalidKeyException;
import java.security.SecureRandom; import java.security.SecureRandom;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.xml.TagWriter; import eu.siacs.conversations.xml.TagWriter;
public class ScramSha1 extends ScramMechanism { public class ScramSha1 extends SaslMechanism {
// TODO: When channel binding (SCRAM-SHA1-PLUS) is supported in future, generalize this to indicate support and/or usage.
final private static String GS2_HEADER = "n,,";
private String clientFirstMessageBare;
final private String clientNonce;
private byte[] serverSignature = null;
private static HMac HMAC;
private static Digest DIGEST;
private static final byte[] CLIENT_KEY_BYTES = "Client Key".getBytes();
private static final byte[] SERVER_KEY_BYTES = "Server Key".getBytes();
public static class KeyPair {
final public byte[] clientKey;
final public byte[] serverKey;
public KeyPair(final byte[] clientKey, final byte[] serverKey) {
this.clientKey = clientKey;
this.serverKey = serverKey;
}
}
private static final LruCache<String, KeyPair> CACHE;
static { static {
DIGEST = new SHA1Digest(); DIGEST = new SHA1Digest();
HMAC = new HMac(new SHA1Digest()); HMAC = new HMac(new SHA1Digest());
CACHE = new LruCache<String, KeyPair>(10) {
protected KeyPair create(final String k) {
// Map keys are "bytesToHex(JID),bytesToHex(password),bytesToHex(salt),iterations".
// Changing any of these values forces a cache miss. `CryptoHelper.bytesToHex()'
// is applied to prevent commas in the strings breaking things.
final String[] kparts = k.split(",", 4);
try {
final byte[] saltedPassword, serverKey, clientKey;
saltedPassword = hi(CryptoHelper.hexToString(kparts[1]).getBytes(),
Base64.decode(CryptoHelper.hexToString(kparts[2]), Base64.DEFAULT), Integer.valueOf(kparts[3]));
serverKey = hmac(saltedPassword, SERVER_KEY_BYTES);
clientKey = hmac(saltedPassword, CLIENT_KEY_BYTES);
return new KeyPair(clientKey, serverKey);
} catch (final InvalidKeyException | NumberFormatException e) {
return null;
}
}
};
} }
private State state = State.INITIAL;
public ScramSha1(final TagWriter tagWriter, final Account account, final SecureRandom rng) { public ScramSha1(final TagWriter tagWriter, final Account account, final SecureRandom rng) {
super(tagWriter, account, rng); super(tagWriter, account, rng);
// This nonce should be different for each authentication attempt.
clientNonce = new BigInteger(100, this.rng).toString(32);
clientFirstMessageBare = "";
} }
@Override @Override
@ -27,4 +83,156 @@ public class ScramSha1 extends ScramMechanism {
public String getMechanism() { public String getMechanism() {
return "SCRAM-SHA-1"; return "SCRAM-SHA-1";
} }
@Override
public String getClientFirstMessage() {
if (clientFirstMessageBare.isEmpty() && state == State.INITIAL) {
clientFirstMessageBare = "n=" + CryptoHelper.saslEscape(CryptoHelper.saslPrep(account.getUsername())) +
",r=" + this.clientNonce;
state = State.AUTH_TEXT_SENT;
}
return Base64.encodeToString(
(GS2_HEADER + clientFirstMessageBare).getBytes(Charset.defaultCharset()),
Base64.NO_WRAP);
}
@Override
public String getResponse(final String challenge) throws AuthenticationException {
switch (state) {
case AUTH_TEXT_SENT:
if (challenge == null) {
throw new AuthenticationException("challenge can not be null");
}
byte[] serverFirstMessage = Base64.decode(challenge, Base64.DEFAULT);
final Tokenizer tokenizer = new Tokenizer(serverFirstMessage);
String nonce = "";
int iterationCount = -1;
String salt = "";
for (final String token : tokenizer) {
if (token.charAt(1) == '=') {
switch (token.charAt(0)) {
case 'i':
try {
iterationCount = Integer.parseInt(token.substring(2));
} catch (final NumberFormatException e) {
throw new AuthenticationException(e);
}
break;
case 's':
salt = token.substring(2);
break;
case 'r':
nonce = token.substring(2);
break;
case 'm':
/*
* RFC 5802:
* m: This attribute is reserved for future extensibility. In this
* version of SCRAM, its presence in a client or a server message
* MUST cause authentication failure when the attribute is parsed by
* the other end.
*/
throw new AuthenticationException("Server sent reserved token: `m'");
}
}
}
if (iterationCount < 0) {
throw new AuthenticationException("Server did not send iteration count");
}
if (nonce.isEmpty() || !nonce.startsWith(clientNonce)) {
throw new AuthenticationException("Server nonce does not contain client nonce: " + nonce);
}
if (salt.isEmpty()) {
throw new AuthenticationException("Server sent empty salt");
}
final String clientFinalMessageWithoutProof = "c=" + Base64.encodeToString(
GS2_HEADER.getBytes(), Base64.NO_WRAP) + ",r=" + nonce;
final byte[] authMessage = (clientFirstMessageBare + ',' + new String(serverFirstMessage) + ','
+ clientFinalMessageWithoutProof).getBytes();
// Map keys are "bytesToHex(JID),bytesToHex(password),bytesToHex(salt),iterations".
final KeyPair keys = CACHE.get(
CryptoHelper.bytesToHex(account.getJid().toBareJid().toString().getBytes()) + ","
+ CryptoHelper.bytesToHex(account.getPassword().getBytes()) + ","
+ CryptoHelper.bytesToHex(salt.getBytes()) + ","
+ String.valueOf(iterationCount)
);
if (keys == null) {
throw new AuthenticationException("Invalid keys generated");
}
final byte[] clientSignature;
try {
serverSignature = hmac(keys.serverKey, authMessage);
final byte[] storedKey = digest(keys.clientKey);
clientSignature = hmac(storedKey, authMessage);
} catch (final InvalidKeyException e) {
throw new AuthenticationException(e);
}
final byte[] clientProof = new byte[keys.clientKey.length];
for (int i = 0; i < clientProof.length; i++) {
clientProof[i] = (byte) (keys.clientKey[i] ^ clientSignature[i]);
}
final String clientFinalMessage = clientFinalMessageWithoutProof + ",p=" +
Base64.encodeToString(clientProof, Base64.NO_WRAP);
state = State.RESPONSE_SENT;
return Base64.encodeToString(clientFinalMessage.getBytes(), Base64.NO_WRAP);
case RESPONSE_SENT:
try {
final String clientCalculatedServerFinalMessage = "v=" +
Base64.encodeToString(serverSignature, Base64.NO_WRAP);
if (!clientCalculatedServerFinalMessage.equals(new String(Base64.decode(challenge, Base64.DEFAULT)))) {
throw new Exception();
};
state = State.VALID_SERVER_RESPONSE;
return "";
} catch(Exception e) {
throw new AuthenticationException("Server final message does not match calculated final message");
}
default:
throw new InvalidStateException(state);
}
}
public static synchronized byte[] hmac(final byte[] key, final byte[] input)
throws InvalidKeyException {
HMAC.init(new KeyParameter(key));
HMAC.update(input, 0, input.length);
final byte[] out = new byte[HMAC.getMacSize()];
HMAC.doFinal(out, 0);
return out;
}
public static synchronized byte[] digest(byte[] bytes) {
DIGEST.reset();
DIGEST.update(bytes, 0, bytes.length);
final byte[] out = new byte[DIGEST.getDigestSize()];
DIGEST.doFinal(out, 0);
return out;
}
/*
* Hi() is, essentially, PBKDF2 [RFC2898] with HMAC() as the
* pseudorandom function (PRF) and with dkLen == output length of
* HMAC() == output length of H().
*/
private static synchronized byte[] hi(final byte[] key, final byte[] salt, final int iterations)
throws InvalidKeyException {
byte[] u = hmac(key, CryptoHelper.concatenateByteArrays(salt, CryptoHelper.ONE));
byte[] out = u.clone();
for (int i = 1; i < iterations; i++) {
u = hmac(key, u);
for (int j = 0; j < u.length; j++) {
out[j] ^= u[j];
}
}
return out;
}
} }

View file

@ -1,30 +0,0 @@
package eu.siacs.conversations.crypto.sasl;
import org.bouncycastle.crypto.digests.SHA256Digest;
import org.bouncycastle.crypto.macs.HMac;
import java.security.SecureRandom;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.xml.TagWriter;
public class ScramSha256 extends ScramMechanism {
static {
DIGEST = new SHA256Digest();
HMAC = new HMac(new SHA256Digest());
}
public ScramSha256(final TagWriter tagWriter, final Account account, final SecureRandom rng) {
super(tagWriter, account, rng);
}
@Override
public int getPriority() {
return 25;
}
@Override
public String getMechanism() {
return "SCRAM-SHA-256";
}
}

View file

@ -15,20 +15,16 @@ import org.json.JSONObject;
import java.security.PublicKey; import java.security.PublicKey;
import java.security.interfaces.DSAPublicKey; import java.security.interfaces.DSAPublicKey;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.CopyOnWriteArraySet;
import eu.siacs.conversations.R; import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.OtrService; import eu.siacs.conversations.crypto.OtrService;
import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.XmppUri;
import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid; import eu.siacs.conversations.xmpp.jid.Jid;
@ -341,10 +337,6 @@ public class Account extends AbstractEntity {
} }
} }
public State getTrueStatus() {
return this.status;
}
public void setStatus(final State status) { public void setStatus(final State status) {
this.status = status; this.status = status;
} }
@ -494,7 +486,7 @@ public class Account extends AbstractEntity {
if (publicKey == null || !(publicKey instanceof DSAPublicKey)) { if (publicKey == null || !(publicKey instanceof DSAPublicKey)) {
return null; return null;
} }
this.otrFingerprint = new OtrCryptoEngineImpl().getFingerprint(publicKey).toLowerCase(Locale.US); this.otrFingerprint = new OtrCryptoEngineImpl().getFingerprint(publicKey);
return this.otrFingerprint; return this.otrFingerprint;
} catch (final OtrCryptoException ignored) { } catch (final OtrCryptoException ignored) {
return null; return null;
@ -607,43 +599,14 @@ public class Account extends AbstractEntity {
} }
public String getShareableUri() { public String getShareableUri() {
List<XmppUri.Fingerprint> fingerprints = this.getFingerprints(); final String fingerprint = this.getOtrFingerprint();
String uri = "xmpp:"+this.getJid().toBareJid().toString(); if (fingerprint != null) {
if (fingerprints.size() > 0) { return "xmpp:" + this.getJid().toBareJid().toString() + "?otr-fingerprint="+fingerprint;
return XmppUri.getFingerprintUri(uri,fingerprints,';');
} else { } else {
return uri; return "xmpp:" + this.getJid().toBareJid().toString();
} }
} }
public String getShareableLink() {
List<XmppUri.Fingerprint> fingerprints = this.getFingerprints();
String uri = "https://conversations.im/i/"+this.getJid().toBareJid().toString();
if (fingerprints.size() > 0) {
return XmppUri.getFingerprintUri(uri,fingerprints,'&');
} else {
return uri;
}
}
private List<XmppUri.Fingerprint> getFingerprints() {
ArrayList<XmppUri.Fingerprint> fingerprints = new ArrayList<>();
final String otr = this.getOtrFingerprint();
if (otr != null) {
fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OTR,otr));
}
if (axolotlService == null) {
return fingerprints;
}
fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OMEMO,axolotlService.getOwnFingerprint().substring(2),axolotlService.getOwnDeviceId()));
for(XmppAxolotlSession session : axolotlService.findOwnSessions()) {
if (session.getTrust().isVerified() && session.getTrust().isActive()) {
fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OMEMO,session.getFingerprint().substring(2).replaceAll("\\s",""),session.getRemoteAddress().getDeviceId()));
}
}
return fingerprints;
}
public boolean isBlocked(final ListItem contact) { public boolean isBlocked(final ListItem contact) {
final Jid jid = contact.getJid(); final Jid jid = contact.getJid();
return jid != null && (blocklist.contains(jid.toBareJid()) || blocklist.contains(jid.toDomainJid())); return jid != null && (blocklist.contains(jid.toBareJid()) || blocklist.contains(jid.toDomainJid()));

View file

@ -121,7 +121,7 @@ public class Contact implements ListItem, Blockable {
} else if (this.presenceName != null && mutualPresenceSubscription()) { } else if (this.presenceName != null && mutualPresenceSubscription()) {
return this.presenceName; return this.presenceName;
} else if (jid.hasLocalpart()) { } else if (jid.hasLocalpart()) {
return jid.getUnescapedLocalpart(); return jid.getLocalpart();
} else { } else {
return jid.getDomainpart(); return jid.getDomainpart();
} }
@ -301,7 +301,7 @@ public class Contact implements ListItem, Blockable {
for (int i = 0; i < prints.length(); ++i) { for (int i = 0; i < prints.length(); ++i) {
final String print = prints.isNull(i) ? null : prints.getString(i); final String print = prints.isNull(i) ? null : prints.getString(i);
if (print != null && !print.isEmpty()) { if (print != null && !print.isEmpty()) {
fingerprints.add(prints.getString(i).toLowerCase(Locale.US)); fingerprints.add(prints.getString(i));
} }
} }
} }

View file

@ -20,9 +20,6 @@ import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; 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.Config;
import eu.siacs.conversations.crypto.PgpDecryptionService; import eu.siacs.conversations.crypto.PgpDecryptionService;
@ -92,7 +89,6 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
private String mLastReceivedOtrMessageId = null; private String mLastReceivedOtrMessageId = null;
private String mFirstMamReference = null; private String mFirstMamReference = null;
private Message correctingMessage; private Message correctingMessage;
public AtomicBoolean messagesLoaded = new AtomicBoolean(true);
public boolean hasMessagesLeftOnServer() { public boolean hasMessagesLeftOnServer() {
return messagesLeftOnServer; return messagesLeftOnServer;
@ -467,7 +463,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
if (generatedName != null) { if (generatedName != null) {
return generatedName; return generatedName;
} else { } else {
return getJid().getUnescapedLocalpart(); return getJid().getLocalpart();
} }
} }
} else { } else {
@ -631,7 +627,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
return null; return null;
} }
DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession().getRemotePublicKey(); DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession().getRemotePublicKey();
this.otrFingerprint = getAccount().getOtrService().getFingerprint(remotePubKey).toLowerCase(Locale.US); this.otrFingerprint = getAccount().getOtrService().getFingerprint(remotePubKey);
} catch (final OtrCryptoException | UnsupportedOperationException ignored) { } catch (final OtrCryptoException | UnsupportedOperationException ignored) {
return null; return null;
} }
@ -683,41 +679,54 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
return this.nextCounterpart; return this.nextCounterpart;
} }
private int getMostRecentlyUsedIncomingEncryption() {
synchronized (this.messages) {
for(int i = this.messages.size() -1; i >= 0; --i) {
final Message m = this.messages.get(i);
if (m.getStatus() == Message.STATUS_RECEIVED) {
final int e = m.getEncryption();
if (e == Message.ENCRYPTION_DECRYPTED || e == Message.ENCRYPTION_DECRYPTION_FAILED) {
return Message.ENCRYPTION_PGP;
} else {
return e;
}
}
}
}
return Message.ENCRYPTION_NONE;
}
public int getNextEncryption() { public int getNextEncryption() {
return fixAvailableEncryption(this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, getDefaultEncryption())); final AxolotlService axolotlService = getAccount().getAxolotlService();
} int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1);
if (next == -1) {
private int fixAvailableEncryption(int selectedEncryption) { if (Config.supportOmemo()
switch(selectedEncryption) { && axolotlService != null
case Message.ENCRYPTION_NONE: && mode == MODE_SINGLE
return Config.supportUnencrypted() ? selectedEncryption : getDefaultEncryption(); && axolotlService.isConversationAxolotlCapable(this)
case Message.ENCRYPTION_AXOLOTL: && getAccount().getSelfContact().getPresences().allOrNonSupport(AxolotlService.PEP_DEVICE_LIST_NOTIFY)
return Config.supportOmemo() ? selectedEncryption : getDefaultEncryption(); && getContact().getPresences().allOrNonSupport(AxolotlService.PEP_DEVICE_LIST_NOTIFY)) {
case Message.ENCRYPTION_OTR: return Message.ENCRYPTION_AXOLOTL;
return Config.supportOtr() ? selectedEncryption : getDefaultEncryption(); } else {
case Message.ENCRYPTION_PGP: next = this.getMostRecentlyUsedIncomingEncryption();
case Message.ENCRYPTION_DECRYPTED: }
case Message.ENCRYPTION_DECRYPTION_FAILED:
return Config.supportOpenPgp() ? Message.ENCRYPTION_PGP : getDefaultEncryption();
default:
return getDefaultEncryption();
} }
}
private int getDefaultEncryption() { if (!Config.supportUnencrypted() && next <= 0) {
AxolotlService axolotlService = account.getAxolotlService(); if (Config.supportOmemo()
if (Config.supportUnencrypted()) { && ((axolotlService != null && axolotlService.isConversationAxolotlCapable(this)) || !Config.multipleEncryptionChoices())) {
return Message.ENCRYPTION_NONE; return Message.ENCRYPTION_AXOLOTL;
} else if (Config.supportOmemo() } else if (Config.supportOtr() && mode == MODE_SINGLE) {
&& (axolotlService != null && axolotlService.isConversationAxolotlCapable(this) || !Config.multipleEncryptionChoices())) { return Message.ENCRYPTION_OTR;
return Message.ENCRYPTION_AXOLOTL; } else if (Config.supportOpenPgp()
} else if (Config.supportOtr() && mode == MODE_SINGLE) { && (mode == MODE_SINGLE) || !Config.multipleEncryptionChoices()) {
return Message.ENCRYPTION_OTR; return Message.ENCRYPTION_PGP;
} else if (Config.supportOpenPgp()) { }
return Message.ENCRYPTION_PGP; } else if (next == Message.ENCRYPTION_AXOLOTL
} else { && (!Config.supportOmemo() || axolotlService == null || !axolotlService.isConversationAxolotlCapable(this))) {
return Message.ENCRYPTION_NONE; next = Message.ENCRYPTION_NONE;
} }
return next;
} }
public void setNextEncryption(int encryption) { public void setNextEncryption(int encryption) {
@ -933,17 +942,6 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
account.getPgpDecryptionService().decrypt(messages); account.getPgpDecryptionService().decrypt(messages);
} }
public void expireOldMessages(long timestamp) {
synchronized (this.messages) {
for(ListIterator<Message> iterator = this.messages.listIterator(); iterator.hasNext();) {
if (iterator.next().getTimeSent() < timestamp) {
iterator.remove();
}
}
untieMessages();
}
}
public void sort() { public void sort() {
synchronized (this.messages) { synchronized (this.messages) {
Collections.sort(this.messages, new Comparator<Message>() { Collections.sort(this.messages, new Comparator<Message>() {

View file

@ -8,7 +8,6 @@ import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession; import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.GeoHelper; import eu.siacs.conversations.utils.GeoHelper;
@ -411,18 +410,15 @@ public class Message extends AbstractEntity {
body = this.body; body = this.body;
otherBody = message.body; otherBody = message.body;
} }
final boolean matchingCounterpart = this.counterpart.equals(message.getCounterpart());
if (message.getRemoteMsgId() != null) { if (message.getRemoteMsgId() != null) {
final boolean hasUuid = CryptoHelper.UUID_PATTERN.matcher(message.getRemoteMsgId()).matches();
if (hasUuid && this.edited != null && matchingCounterpart && this.edited.equals(message.getRemoteMsgId())) {
return true;
}
return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid)) return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid))
&& matchingCounterpart && this.counterpart.equals(message.getCounterpart())
&& (body.equals(otherBody) ||(message.getEncryption() == Message.ENCRYPTION_PGP && hasUuid)); && (body.equals(otherBody)
||(message.getEncryption() == Message.ENCRYPTION_PGP
&& CryptoHelper.UUID_PATTERN.matcher(message.getRemoteMsgId()).matches()));
} else { } else {
return this.remoteMsgId == null return this.remoteMsgId == null
&& matchingCounterpart && this.counterpart.equals(message.getCounterpart())
&& body.equals(otherBody) && body.equals(otherBody)
&& Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.MESSAGE_MERGE_WINDOW * 1000; && Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.MESSAGE_MERGE_WINDOW * 1000;
} }
@ -496,7 +492,7 @@ public class Message extends AbstractEntity {
!this.getBody().startsWith(ME_COMMAND) && !this.getBody().startsWith(ME_COMMAND) &&
!this.bodyIsHeart() && !this.bodyIsHeart() &&
!message.bodyIsHeart() && !message.bodyIsHeart() &&
((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint())) this.isTrusted() == message.isTrusted()
); );
} }
@ -815,8 +811,8 @@ public class Message extends AbstractEntity {
} }
public boolean isTrusted() { public boolean isTrusted() {
FingerprintStatus s = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint); XmppAxolotlSession.Trust t = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint);
return s != null && s.isTrusted(); return t != null && t.trusted();
} }
private int getPreviousEncryption() { private int getPreviousEncryption() {

View file

@ -3,6 +3,7 @@ package eu.siacs.conversations.entities;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@ -394,20 +395,10 @@ public class MucOptions {
if (user != null) { if (user != null) {
synchronized (users) { synchronized (users) {
users.remove(user); users.remove(user);
boolean realJidInMuc = false; if (membersOnly() &&
for (User u : users) { nonanonymous() &&
if (user.realJid != null && user.realJid.equals(u.realJid)) { user.affiliation.ranks(Affiliation.MEMBER) &&
realJidInMuc = true; user.realJid != null) {
break;
}
}
boolean self = user.realJid != null && user.realJid.equals(account.getJid().toBareJid());
if (membersOnly()
&& nonanonymous()
&& user.affiliation.ranks(Affiliation.MEMBER)
&& user.realJid != null
&& !realJidInMuc
&& !self) {
user.role = Role.NONE; user.role = Role.NONE;
user.avatar = null; user.avatar = null;
user.fullJid = null; user.fullJid = null;
@ -418,7 +409,7 @@ public class MucOptions {
return user; return user;
} }
public void updateUser(User user) { public void addUser(User user) {
User old; User old;
if (user.fullJid == null && user.realJid != null) { if (user.fullJid == null && user.realJid != null) {
old = findUserByRealJid(user.realJid); old = findUserByRealJid(user.realJid);
@ -444,10 +435,7 @@ public class MucOptions {
if (old != null) { if (old != null) {
users.remove(old); users.remove(old);
} }
if ((!membersOnly() || user.getAffiliation().ranks(Affiliation.MEMBER)) this.users.add(user);
&& user.getAffiliation().outranks(Affiliation.OUTCAST)){
this.users.add(user);
}
} }
} }
@ -517,20 +505,8 @@ public class MucOptions {
} }
public List<User> getUsers(int max) { public List<User> getUsers(int max) {
ArrayList<User> subset = new ArrayList<>(); ArrayList<User> users = getUsers();
HashSet<Jid> jids = new HashSet<>(); return users.subList(0, Math.min(max, users.size()));
jids.add(account.getJid().toBareJid());
synchronized (users) {
for(User user : users) {
if (user.getRealJid() == null || jids.add(user.getRealJid())) {
subset.add(user);
}
if (subset.size() >= max) {
break;
}
}
}
return subset;
} }
public int getUserCount() { public int getUserCount() {

View file

@ -10,15 +10,12 @@ import org.whispersystems.libaxolotl.ecc.ECPublicKey;
import org.whispersystems.libaxolotl.state.PreKeyRecord; import org.whispersystems.libaxolotl.state.PreKeyRecord;
import org.whispersystems.libaxolotl.state.SignedPreKeyRecord; import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.TimeZone; import java.util.TimeZone;
import java.util.UUID;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.R; import eu.siacs.conversations.R;
@ -318,7 +315,7 @@ public class IqGenerator extends AbstractGenerator {
IqPacket packet = new IqPacket(IqPacket.TYPE.GET); IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
packet.setTo(host); packet.setTo(host);
Element request = packet.addChild("request", Xmlns.HTTP_UPLOAD); Element request = packet.addChild("request", Xmlns.HTTP_UPLOAD);
request.addChild("filename").setContent(convertFilename(file.getName())); request.addChild("filename").setContent(file.getName());
request.addChild("size").setContent(String.valueOf(file.getExpectedSize())); request.addChild("size").setContent(String.valueOf(file.getExpectedSize()));
if (mime != null) { if (mime != null) {
request.addChild("content-type").setContent(mime); request.addChild("content-type").setContent(mime);
@ -326,23 +323,6 @@ public class IqGenerator extends AbstractGenerator {
return packet; return packet;
} }
private static String convertFilename(String name) {
int pos = name.indexOf('.');
if (pos != -1) {
try {
UUID uuid = UUID.fromString(name.substring(0, pos));
ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
bb.putLong(uuid.getMostSignificantBits());
bb.putLong(uuid.getLeastSignificantBits());
return Base64.encodeToString(bb.array(), Base64.URL_SAFE) + name.substring(pos, name.length());
} catch (Exception e) {
return name;
}
} else {
return name;
}
}
public IqPacket generateCreateAccountWithCaptcha(Account account, String id, Data data) { public IqPacket generateCreateAccountWithCaptcha(Account account, String id, Data data) {
final IqPacket register = new IqPacket(IqPacket.TYPE.SET); final IqPacket register = new IqPacket(IqPacket.TYPE.SET);
register.setFrom(account.getJid().toBareJid()); register.setFrom(account.getJid().toBareJid());

View file

@ -79,9 +79,6 @@ public class MessageGenerator extends AbstractGenerator {
packet.setBody(OMEMO_FALLBACK_MESSAGE); packet.setBody(OMEMO_FALLBACK_MESSAGE);
} }
packet.addChild("store", "urn:xmpp:hints"); packet.addChild("store", "urn:xmpp:hints");
packet.addChild("encryption","urn:xmpp:eme:0")
.setAttribute("name","OMEMO")
.setAttribute("namespace",AxolotlService.PEP_PREFIX);
return packet; return packet;
} }
@ -112,8 +109,6 @@ public class MessageGenerator extends AbstractGenerator {
content = message.getBody(); content = message.getBody();
} }
packet.setBody(otrSession.transformSending(content)[0]); packet.setBody(otrSession.transformSending(content)[0]);
packet.addChild("encryption","urn:xmpp:eme:0")
.setAttribute("namespace","urn:xmpp:otr:0");
return packet; return packet;
} catch (OtrException e) { } catch (OtrException e) {
return null; return null;
@ -144,8 +139,6 @@ public class MessageGenerator extends AbstractGenerator {
} else if (message.getEncryption() == Message.ENCRYPTION_PGP) { } else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
packet.addChild("x", "jabber:x:encrypted").setContent(message.getBody()); packet.addChild("x", "jabber:x:encrypted").setContent(message.getBody());
} }
packet.addChild("encryption","urn:xmpp:eme:0")
.setAttribute("namespace","jabber:x:encrypted");
return packet; return packet;
} }

View file

@ -24,7 +24,6 @@ import eu.siacs.conversations.services.AbstractConnectionManager;
import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.SSLSocketHelper; import eu.siacs.conversations.utils.SSLSocketHelper;
import eu.siacs.conversations.utils.TLSSocketFactory;
public class HttpConnectionManager extends AbstractConnectionManager { public class HttpConnectionManager extends AbstractConnectionManager {
@ -65,7 +64,7 @@ public class HttpConnectionManager extends AbstractConnectionManager {
final X509TrustManager trustManager; final X509TrustManager trustManager;
final HostnameVerifier hostnameVerifier; final HostnameVerifier hostnameVerifier;
if (interactive) { if (interactive) {
trustManager = mXmppConnectionService.getMemorizingTrustManager().getInteractive(); trustManager = mXmppConnectionService.getMemorizingTrustManager();
hostnameVerifier = mXmppConnectionService hostnameVerifier = mXmppConnectionService
.getMemorizingTrustManager().wrapHostnameVerifier( .getMemorizingTrustManager().wrapHostnameVerifier(
new StrictHostnameVerifier()); new StrictHostnameVerifier());
@ -78,7 +77,18 @@ public class HttpConnectionManager extends AbstractConnectionManager {
new StrictHostnameVerifier()); new StrictHostnameVerifier());
} }
try { try {
final SSLSocketFactory sf = new TLSSocketFactory(new X509TrustManager[]{trustManager}, mXmppConnectionService.getRNG()); final SSLContext sc = SSLSocketHelper.getSSLContext();
sc.init(null, new X509TrustManager[]{trustManager},
mXmppConnectionService.getRNG());
final SSLSocketFactory sf = sc.getSocketFactory();
final String[] cipherSuites = CryptoHelper.getOrderedCipherSuites(
sf.getSupportedCipherSuites());
if (cipherSuites.length > 0) {
sc.getDefaultSSLParameters().setCipherSuites(cipherSuites);
}
connection.setSSLSocketFactory(sf); connection.setSSLSocketFactory(sf);
connection.setHostnameVerifier(hostnameVerifier); connection.setHostnameVerifier(hostnameVerifier);
} catch (final KeyManagementException | NoSuchAlgorithmException ignored) { } catch (final KeyManagementException | NoSuchAlgorithmException ignored) {
@ -86,6 +96,6 @@ public class HttpConnectionManager extends AbstractConnectionManager {
} }
public Proxy getProxy() throws IOException { public Proxy getProxy() throws IOException {
return new Proxy(Proxy.Type.HTTP, new InetSocketAddress(InetAddress.getByAddress(new byte[]{127,0,0,1}), 8118)); return new Proxy(Proxy.Type.HTTP, new InetSocketAddress(InetAddress.getLocalHost(), 8118));
} }
} }

View file

@ -25,7 +25,6 @@ import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.AbstractConnectionManager; import eu.siacs.conversations.services.AbstractConnectionManager;
import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.FileWriterException;
public class HttpDownloadConnection implements Transferable { public class HttpDownloadConnection implements Transferable {
@ -120,7 +119,7 @@ public class HttpDownloadConnection implements Transferable {
} else { } else {
message.setTransferable(null); message.setTransferable(null);
} }
mHttpConnectionManager.updateConversationUi(true); mXmppConnectionService.updateConversationUi();
} }
private void finish() { private void finish() {
@ -131,7 +130,7 @@ public class HttpDownloadConnection implements Transferable {
if (message.getEncryption() == Message.ENCRYPTION_PGP) { if (message.getEncryption() == Message.ENCRYPTION_PGP) {
notify = message.getConversation().getAccount().getPgpDecryptionService().decrypt(message, notify); notify = message.getConversation().getAccount().getPgpDecryptionService().decrypt(message, notify);
} }
mHttpConnectionManager.updateConversationUi(true); mXmppConnectionService.updateConversationUi();
if (notify) { if (notify) {
mXmppConnectionService.getNotificationService().push(message); mXmppConnectionService.getNotificationService().push(message);
} }
@ -139,7 +138,11 @@ public class HttpDownloadConnection implements Transferable {
private void changeStatus(int status) { private void changeStatus(int status) {
this.mStatus = status; this.mStatus = status;
mHttpConnectionManager.updateConversationUi(true); mXmppConnectionService.updateConversationUi();
}
private class WriteException extends IOException {
} }
private void showToastForException(Exception e) { private void showToastForException(Exception e) {
@ -147,7 +150,7 @@ public class HttpDownloadConnection implements Transferable {
mXmppConnectionService.showErrorToastInUi(R.string.download_failed_server_not_found); mXmppConnectionService.showErrorToastInUi(R.string.download_failed_server_not_found);
} else if (e instanceof java.net.ConnectException) { } else if (e instanceof java.net.ConnectException) {
mXmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_connect); mXmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_connect);
} else if (e instanceof FileWriterException) { } else if (e instanceof WriteException) {
mXmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_write_file); mXmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_write_file);
} else if (!(e instanceof CancellationException)) { } else if (!(e instanceof CancellationException)) {
mXmppConnectionService.showErrorToastInUi(R.string.download_failed_file_not_found); mXmppConnectionService.showErrorToastInUi(R.string.download_failed_file_not_found);
@ -273,18 +276,16 @@ public class HttpDownloadConnection implements Transferable {
} }
connection.setRequestProperty("User-Agent",mXmppConnectionService.getIqGenerator().getIdentityName()); connection.setRequestProperty("User-Agent",mXmppConnectionService.getIqGenerator().getIdentityName());
final boolean tryResume = file.exists() && file.getKey() == null; final boolean tryResume = file.exists() && file.getKey() == null;
long resumeSize = 0;
if (tryResume) { if (tryResume) {
Log.d(Config.LOGTAG,"http download trying resume"); Log.d(Config.LOGTAG,"http download trying resume");
resumeSize = file.getSize(); long size = file.getSize();
connection.setRequestProperty("Range", "bytes="+resumeSize+"-"); connection.setRequestProperty("Range", "bytes="+size+"-");
} }
connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000); connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000); connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000);
connection.connect(); connection.connect();
is = new BufferedInputStream(connection.getInputStream()); is = new BufferedInputStream(connection.getInputStream());
final String contentRange = connection.getHeaderField("Content-Range"); boolean serverResumed = "bytes".equals(connection.getHeaderField("Accept-Ranges"));
boolean serverResumed = tryResume && contentRange != null && contentRange.startsWith("bytes "+resumeSize+"-");
long transmitted = 0; long transmitted = 0;
long expected = file.getExpectedSize(); long expected = file.getExpectedSize();
if (tryResume && serverResumed) { if (tryResume && serverResumed) {
@ -292,14 +293,9 @@ public class HttpDownloadConnection implements Transferable {
transmitted = file.getSize(); transmitted = file.getSize();
updateProgress((int) ((((double) transmitted) / expected) * 100)); updateProgress((int) ((((double) transmitted) / expected) * 100));
os = AbstractConnectionManager.createAppendedOutputStream(file); os = AbstractConnectionManager.createAppendedOutputStream(file);
if (os == null) {
throw new FileWriterException();
}
} else { } else {
file.getParentFile().mkdirs(); file.getParentFile().mkdirs();
if (!file.exists() && !file.createNewFile()) { file.createNewFile();
throw new FileWriterException();
}
os = AbstractConnectionManager.createOutputStream(file, true); os = AbstractConnectionManager.createOutputStream(file, true);
} }
int count; int count;
@ -309,7 +305,7 @@ public class HttpDownloadConnection implements Transferable {
try { try {
os.write(buffer, 0, count); os.write(buffer, 0, count);
} catch (IOException e) { } catch (IOException e) {
throw new FileWriterException(); throw new WriteException();
} }
updateProgress((int) ((((double) transmitted) / expected) * 100)); updateProgress((int) ((((double) transmitted) / expected) * 100));
if (canceled) { if (canceled) {
@ -319,7 +315,7 @@ public class HttpDownloadConnection implements Transferable {
try { try {
os.flush(); os.flush();
} catch (IOException e) { } catch (IOException e) {
throw new FileWriterException(); throw new WriteException();
} }
} catch (CancellationException | IOException e) { } catch (CancellationException | IOException e) {
throw e; throw e;
@ -340,7 +336,7 @@ public class HttpDownloadConnection implements Transferable {
public void updateProgress(int i) { public void updateProgress(int i) {
this.mProgress = i; this.mProgress = i;
mHttpConnectionManager.updateConversationUi(false); mXmppConnectionService.updateConversationUi();
} }
@Override @Override

View file

@ -182,7 +182,7 @@ public class HttpUploadConnection implements Transferable {
while (((count = mFileInputStream.read(buffer)) != -1) && !canceled) { while (((count = mFileInputStream.read(buffer)) != -1) && !canceled) {
transmitted += count; transmitted += count;
os.write(buffer, 0, count); os.write(buffer, 0, count);
mHttpConnectionManager.updateConversationUi(false); mXmppConnectionService.updateConversationUi();
} }
os.flush(); os.flush();
os.close(); os.close();

View file

@ -74,24 +74,19 @@ public abstract class AbstractParser {
} }
public static MucOptions.User parseItem(Conversation conference, Element item) { public static MucOptions.User parseItem(Conversation conference, Element item) {
return parseItem(conference,item, null);
}
public static MucOptions.User parseItem(Conversation conference, Element item, Jid fullJid) {
final String local = conference.getJid().getLocalpart(); final String local = conference.getJid().getLocalpart();
final String domain = conference.getJid().getDomainpart(); final String domain = conference.getJid().getDomainpart();
String affiliation = item.getAttribute("affiliation"); String affiliation = item.getAttribute("affiliation");
String role = item.getAttribute("role"); String role = item.getAttribute("role");
String nick = item.getAttribute("nick"); String nick = item.getAttribute("nick");
if (nick != null && fullJid == null) { Jid fullJid;
try { try {
fullJid = Jid.fromParts(local, domain, nick); fullJid = nick != null ? Jid.fromParts(local, domain, nick) : null;
} catch (InvalidJidException e) { } catch (InvalidJidException e) {
fullJid = null; fullJid = null;
}
} }
Jid realJid = item.getAttributeAsJid("jid"); Jid realJid = item.getAttributeAsJid("jid");
MucOptions.User user = new MucOptions.User(conference.getMucOptions(), fullJid); MucOptions.User user = new MucOptions.User(conference.getMucOptions(), nick == null ? null : fullJid);
user.setRealJid(realJid); user.setRealJid(realJid);
user.setAffiliation(affiliation); user.setAffiliation(affiliation);
user.setRole(role); user.setRole(role);

View file

@ -26,7 +26,6 @@ import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.Xmlns; import eu.siacs.conversations.utils.Xmlns;
import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Element;
@ -320,14 +319,6 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
} }
} }
account.getBlocklist().addAll(jids); account.getBlocklist().addAll(jids);
if (packet.getType() == IqPacket.TYPE.SET) {
for(Jid jid : jids) {
Conversation conversation = mXmppConnectionService.find(account,jid);
if (conversation != null) {
mXmppConnectionService.markRead(conversation);
}
}
}
} }
// Update the UI // Update the UI
mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED); mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
@ -358,8 +349,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
final IqPacket response = packet.generateResponse(IqPacket.TYPE.RESULT); final IqPacket response = packet.generateResponse(IqPacket.TYPE.RESULT);
mXmppConnectionService.sendIqPacket(account, response, null); mXmppConnectionService.sendIqPacket(account, response, null);
} else if (packet.hasChild("open", "http://jabber.org/protocol/ibb") } else if (packet.hasChild("open", "http://jabber.org/protocol/ibb")
|| packet.hasChild("data", "http://jabber.org/protocol/ibb") || packet.hasChild("data", "http://jabber.org/protocol/ibb")) {
|| packet.hasChild("close","http://jabber.org/protocol/ibb")) {
mXmppConnectionService.getJingleConnectionManager() mXmppConnectionService.getJingleConnectionManager()
.deliverIbbPacket(account, packet); .deliverIbbPacket(account, packet);
} else if (packet.hasChild("query", "http://jabber.org/protocol/disco#info")) { } else if (packet.hasChild("query", "http://jabber.org/protocol/disco#info")) {

View file

@ -31,7 +31,6 @@ import eu.siacs.conversations.http.HttpConnectionManager;
import eu.siacs.conversations.services.MessageArchiveService; import eu.siacs.conversations.services.MessageArchiveService;
import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.Xmlns;
import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.OnMessagePacketReceived; import eu.siacs.conversations.xmpp.OnMessagePacketReceived;
import eu.siacs.conversations.xmpp.chatstate.ChatState; import eu.siacs.conversations.xmpp.chatstate.ChatState;
@ -167,13 +166,11 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
} }
private class Invite { private class Invite {
final Jid jid; Jid jid;
final String password; String password;
final Contact inviter; Invite(Jid jid, String password) {
Invite(Jid jid, String password, Contact inviter) {
this.jid = jid; this.jid = jid;
this.password = password; this.password = password;
this.inviter = inviter;
} }
public boolean execute(Account account) { public boolean execute(Account account) {
@ -182,7 +179,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
if (!conversation.getMucOptions().online()) { if (!conversation.getMucOptions().online()) {
conversation.getMucOptions().setPassword(password); conversation.getMucOptions().setPassword(password);
mXmppConnectionService.databaseBackend.updateConversation(conversation); mXmppConnectionService.databaseBackend.updateConversation(conversation);
mXmppConnectionService.joinMuc(conversation, inviter != null && inviter.mutualPresenceSubscription()); mXmppConnectionService.joinMuc(conversation);
mXmppConnectionService.updateConversationUi(); mXmppConnectionService.updateConversationUi();
} }
return true; return true;
@ -191,22 +188,18 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
} }
} }
private Invite extractInvite(Account account, Element message) { private Invite extractInvite(Element message) {
Element x = message.findChild("x", "http://jabber.org/protocol/muc#user"); Element x = message.findChild("x", "http://jabber.org/protocol/muc#user");
if (x != null) { if (x != null) {
Element invite = x.findChild("invite"); Element invite = x.findChild("invite");
if (invite != null) { if (invite != null) {
Element pw = x.findChild("password"); Element pw = x.findChild("password");
Jid from = invite.getAttributeAsJid("from"); return new Invite(message.getAttributeAsJid("from"), pw != null ? pw.getContent(): null);
Contact contact = from == null ? null : account.getRoster().getContact(from);
return new Invite(message.getAttributeAsJid("from"), pw != null ? pw.getContent(): null, contact);
} }
} else { } else {
x = message.findChild("x","jabber:x:conference"); x = message.findChild("x","jabber:x:conference");
if (x != null) { if (x != null) {
Jid from = message.getAttributeAsJid("from"); return new Invite(x.getAttributeAsJid("jid"),x.getAttribute("password"));
Contact contact = from == null ? null : account.getRoster().getContact(from);
return new Invite(x.getAttributeAsJid("jid"),x.getAttribute("password"),contact);
} }
} }
return null; return null;
@ -215,7 +208,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
private static String extractStanzaId(Element packet, Jid by) { private static String extractStanzaId(Element packet, Jid by) {
for(Element child : packet.getChildren()) { for(Element child : packet.getChildren()) {
if (child.getName().equals("stanza-id") if (child.getName().equals("stanza-id")
&& Xmlns.STANZA_IDS.equals(child.getNamespace()) && "urn:xmpp:sid:0".equals(child.getNamespace())
&& by.equals(child.getAttributeAsJid("by"))) { && by.equals(child.getAttributeAsJid("by"))) {
return child.getAttribute("id"); return child.getAttribute("id");
} }
@ -260,10 +253,9 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
mXmppConnectionService.updateAccountUi(); mXmppConnectionService.updateAccountUi();
} }
} else if (AxolotlService.PEP_DEVICE_LIST.equals(node)) { } else if (AxolotlService.PEP_DEVICE_LIST.equals(node)) {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Received PEP device list update from "+ from + ", processing...");
Element item = items.findChild("item"); Element item = items.findChild("item");
Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item);
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Received PEP device list ("+deviceIds+") update from "+ from + ", processing...");
AxolotlService axolotlService = account.getAxolotlService(); AxolotlService axolotlService = account.getAxolotlService();
axolotlService.registerDevices(from, deviceIds); axolotlService.registerDevices(from, deviceIds);
mXmppConnectionService.updateAccountUi(); mXmppConnectionService.updateAccountUi();
@ -370,7 +362,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
counterpart = from; counterpart = from;
} }
Invite invite = extractInvite(account, packet); Invite invite = extractInvite(packet);
if (invite != null && invite.execute(account)) { if (invite != null && invite.execute(account)) {
return; return;
} }
@ -438,18 +430,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
} }
if (serverMsgId == null) { if (serverMsgId == null) {
final Jid by; serverMsgId = extractStanzaId(packet, isTypeGroupChat ? conversation.getJid().toBareJid() : account.getServer());
final boolean safeToExtract;
if (isTypeGroupChat) {
by = conversation.getJid().toBareJid();
safeToExtract = true; //conversation.getMucOptions().hasFeature(Xmlns.STANZA_IDS);
} else {
by = account.getJid().toBareJid();
safeToExtract = true; //account.getXmppConnection().getFeatures().stanzaIds();
}
if (safeToExtract) {
serverMsgId = extractStanzaId(packet, by);
}
} }
message.setCounterpart(counterpart); message.setCounterpart(counterpart);
@ -491,8 +472,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|| replacedMessage.getFingerprint().equals(message.getFingerprint()); || replacedMessage.getFingerprint().equals(message.getFingerprint());
final boolean trueCountersMatch = replacedMessage.getTrueCounterpart() != null final boolean trueCountersMatch = replacedMessage.getTrueCounterpart() != null
&& replacedMessage.getTrueCounterpart().equals(message.getTrueCounterpart()); && replacedMessage.getTrueCounterpart().equals(message.getTrueCounterpart());
final boolean duplicate = conversation.hasDuplicateMessage(message); if (fingerprintsMatch && (trueCountersMatch || !conversationMultiMode)) {
if (fingerprintsMatch && (trueCountersMatch || !conversationMultiMode) && !duplicate) {
Log.d(Config.LOGTAG, "replaced message '" + replacedMessage.getBody() + "' with '" + message.getBody() + "'"); Log.d(Config.LOGTAG, "replaced message '" + replacedMessage.getBody() + "' with '" + message.getBody() + "'");
synchronized (replacedMessage) { synchronized (replacedMessage) {
final String uuid = replacedMessage.getUuid(); final String uuid = replacedMessage.getUuid();
@ -521,12 +501,6 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
} }
} }
long deletionDate = mXmppConnectionService.getAutomaticMessageDeletionDate();
if (deletionDate != 0 && message.getTimeSent() < deletionDate) {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": skipping message from "+message.getCounterpart().toString()+" because it was sent prior to our deletion date");
return;
}
boolean checkForDuplicates = query != null boolean checkForDuplicates = query != null
|| (isTypeGroupChat && packet.hasChild("delay","urn:xmpp:delay")) || (isTypeGroupChat && packet.hasChild("delay","urn:xmpp:delay"))
|| message.getType() == Message.TYPE_PRIVATE; || message.getType() == Message.TYPE_PRIVATE;
@ -576,7 +550,9 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
conversation.endOtrIfNeeded(); conversation.endOtrIfNeeded();
} }
mXmppConnectionService.databaseBackend.createMessage(message); if (message.getEncryption() == Message.ENCRYPTION_NONE || mXmppConnectionService.saveEncryptedMessages()) {
mXmppConnectionService.databaseBackend.createMessage(message);
}
final HttpConnectionManager manager = this.mXmppConnectionService.getHttpConnectionManager(); final HttpConnectionManager manager = this.mXmppConnectionService.getHttpConnectionManager();
if (message.trusted() && message.treatAsDownloadable() != Message.Decision.NEVER && manager.getAutoAcceptFileSize() > 0) { if (message.trusted() && message.treatAsDownloadable() != Message.Decision.NEVER && manager.getAutoAcceptFileSize() > 0) {
manager.createNewDownloadConnection(message); manager.createNewDownloadConnection(message);
@ -624,19 +600,10 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
+user.getRealJid()+" to "+user.getAffiliation()+" in " +user.getRealJid()+" to "+user.getAffiliation()+" in "
+conversation.getJid().toBareJid()); +conversation.getJid().toBareJid());
if (!user.realJidMatchesAccount()) { if (!user.realJidMatchesAccount()) {
conversation.getMucOptions().updateUser(user); conversation.getMucOptions().addUser(user);
mXmppConnectionService.getAvatarService().clear(conversation); mXmppConnectionService.getAvatarService().clear(conversation);
mXmppConnectionService.updateMucRosterUi(); mXmppConnectionService.updateMucRosterUi();
mXmppConnectionService.updateConversationUi(); mXmppConnectionService.updateConversationUi();
if (!user.getAffiliation().ranks(MucOptions.Affiliation.MEMBER)) {
Jid jid = user.getRealJid();
List<Jid> cryptoTargets = conversation.getAcceptedCryptoTargets();
if (cryptoTargets.remove(user.getRealJid())) {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": removed "+jid+" from crypto targets of "+conversation.getName());
conversation.setAcceptedCryptoTargets(cryptoTargets);
mXmppConnectionService.updateConversation(conversation);
}
}
} }
} }
} }
@ -671,9 +638,9 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
} }
} }
Element event = original.findChild("event", "http://jabber.org/protocol/pubsub#event"); Element event = packet.findChild("event", "http://jabber.org/protocol/pubsub#event");
if (event != null) { if (event != null) {
parseEvent(event, original.getFrom(), account); parseEvent(event, from, account);
} }
String nick = packet.findChildContent("nick", "http://jabber.org/protocol/nick"); String nick = packet.findChildContent("nick", "http://jabber.org/protocol/nick");

View file

@ -38,7 +38,7 @@ public class PresenceParser extends AbstractParser implements
boolean before = mucOptions.online(); boolean before = mucOptions.online();
int count = mucOptions.getUserCount(); int count = mucOptions.getUserCount();
final List<MucOptions.User> tileUserBefore = mucOptions.getUsers(5); final List<MucOptions.User> tileUserBefore = mucOptions.getUsers(5);
processConferencePresence(packet, conversation); processConferencePresence(packet, mucOptions);
final List<MucOptions.User> tileUserAfter = mucOptions.getUsers(5); final List<MucOptions.User> tileUserAfter = mucOptions.getUsers(5);
if (!tileUserAfter.equals(tileUserBefore)) { if (!tileUserAfter.equals(tileUserBefore)) {
mXmppConnectionService.getAvatarService().clear(mucOptions); mXmppConnectionService.getAvatarService().clear(mucOptions);
@ -51,8 +51,7 @@ public class PresenceParser extends AbstractParser implements
} }
} }
private void processConferencePresence(PresencePacket packet, Conversation conversation) { private void processConferencePresence(PresencePacket packet, MucOptions mucOptions) {
MucOptions mucOptions = conversation.getMucOptions();
final Jid from = packet.getFrom(); final Jid from = packet.getFrom();
if (!from.isBareJid()) { if (!from.isBareJid()) {
final String type = packet.getAttribute("type"); final String type = packet.getAttribute("type");
@ -64,7 +63,10 @@ public class PresenceParser extends AbstractParser implements
Element item = x.findChild("item"); Element item = x.findChild("item");
if (item != null && !from.isBareJid()) { if (item != null && !from.isBareJid()) {
mucOptions.setError(MucOptions.Error.NONE); mucOptions.setError(MucOptions.Error.NONE);
MucOptions.User user = parseItem(conversation, item, from); MucOptions.User user = new MucOptions.User(mucOptions, from);
user.setAffiliation(item.getAttribute("affiliation"));
user.setRole(item.getAttribute("role"));
user.setRealJid(item.getAttributeAsJid("jid"));
if (codes.contains(MucOptions.STATUS_CODE_SELF_PRESENCE) || packet.getFrom().equals(mucOptions.getConversation().getJid())) { if (codes.contains(MucOptions.STATUS_CODE_SELF_PRESENCE) || packet.getFrom().equals(mucOptions.getConversation().getJid())) {
mucOptions.setOnline(); mucOptions.setOnline();
mucOptions.setSelf(user); mucOptions.setSelf(user);
@ -75,7 +77,7 @@ public class PresenceParser extends AbstractParser implements
mucOptions.mNickChangingInProgress = false; mucOptions.mNickChangingInProgress = false;
} }
} else { } else {
mucOptions.updateUser(user); mucOptions.addUser(user);
} }
if (codes.contains(MucOptions.STATUS_CODE_ROOM_CREATED) && mucOptions.autoPushConfiguration()) { if (codes.contains(MucOptions.STATUS_CODE_ROOM_CREATED) && mucOptions.autoPushConfiguration()) {
Log.d(Config.LOGTAG,mucOptions.getAccount().getJid().toBareJid() Log.d(Config.LOGTAG,mucOptions.getAccount().getJid().toBareJid()
@ -129,10 +131,6 @@ public class PresenceParser extends AbstractParser implements
Log.d(Config.LOGTAG, "unknown error in conference: " + packet); Log.d(Config.LOGTAG, "unknown error in conference: " + packet);
} }
} else if (!from.isBareJid()){ } else if (!from.isBareJid()){
Element item = x.findChild("item");
if (item != null) {
mucOptions.updateUser(parseItem(conversation, item, from));
}
MucOptions.User user = mucOptions.deleteUser(from); MucOptions.User user = mucOptions.deleteUser(from);
if (user != null) { if (user != null) {
mXmppConnectionService.getAvatarService().clear(user); mXmppConnectionService.getAvatarService().clear(user);

View file

@ -7,7 +7,6 @@ import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteCantOpenDatabaseException; import android.database.sqlite.SQLiteCantOpenDatabaseException;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteOpenHelper;
import android.os.Environment;
import android.util.Base64; import android.util.Base64;
import android.util.Log; import android.util.Log;
import android.util.Pair; import android.util.Pair;
@ -22,26 +21,23 @@ import org.whispersystems.libaxolotl.state.SessionRecord;
import org.whispersystems.libaxolotl.state.SignedPreKeyRecord; import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException; import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory; import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import org.json.JSONException; import org.json.JSONException;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
import eu.siacs.conversations.crypto.axolotl.SQLiteAxolotlStore; import eu.siacs.conversations.crypto.axolotl.SQLiteAxolotlStore;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Conversation;
@ -49,7 +45,7 @@ import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.PresenceTemplate; import eu.siacs.conversations.entities.PresenceTemplate;
import eu.siacs.conversations.entities.Roster; import eu.siacs.conversations.entities.Roster;
import eu.siacs.conversations.entities.ServiceDiscoveryResult; import eu.siacs.conversations.entities.ServiceDiscoveryResult;
import eu.siacs.conversations.utils.MimeUtils; import eu.siacs.conversations.generator.AbstractGenerator;
import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid; import eu.siacs.conversations.xmpp.jid.Jid;
@ -58,7 +54,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
private static DatabaseBackend instance = null; private static DatabaseBackend instance = null;
private static final String DATABASE_NAME = "history"; private static final String DATABASE_NAME = "history";
private static final int DATABASE_VERSION = 34; private static final int DATABASE_VERSION = 29;
private static String CREATE_CONTATCS_STATEMENT = "create table " private static String CREATE_CONTATCS_STATEMENT = "create table "
+ Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, " + Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, "
@ -133,9 +129,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
+ SQLiteAxolotlStore.OWN + " INTEGER, " + SQLiteAxolotlStore.OWN + " INTEGER, "
+ SQLiteAxolotlStore.FINGERPRINT + " TEXT, " + SQLiteAxolotlStore.FINGERPRINT + " TEXT, "
+ SQLiteAxolotlStore.CERTIFICATE + " BLOB, " + SQLiteAxolotlStore.CERTIFICATE + " BLOB, "
+ SQLiteAxolotlStore.TRUST + " TEXT, " + SQLiteAxolotlStore.TRUSTED + " INTEGER, "
+ SQLiteAxolotlStore.ACTIVE + " NUMBER, "
+ SQLiteAxolotlStore.LAST_ACTIVATION + " NUMBER,"
+ SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY(" + SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY("
+ SQLiteAxolotlStore.ACCOUNT + SQLiteAxolotlStore.ACCOUNT
+ ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, " + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, "
@ -145,12 +139,6 @@ public class DatabaseBackend extends SQLiteOpenHelper {
+ ") ON CONFLICT IGNORE" + ") ON CONFLICT IGNORE"
+ ");"; + ");";
private static String START_TIMES_TABLE = "start_times";
private static String CREATE_START_TIMES_TABLE = "create table "+START_TIMES_TABLE+" (timestamp NUMBER);";
private static String CREATE_MESSAGE_TIME_INDEX = "create INDEX message_time_index ON "+Message.TABLENAME+"("+Message.TIME_SENT+")";
private DatabaseBackend(Context context) { private DatabaseBackend(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION); super(context, DATABASE_NAME, null, DATABASE_VERSION);
} }
@ -198,7 +186,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
+ Message.CONVERSATION + ") REFERENCES " + Message.CONVERSATION + ") REFERENCES "
+ Conversation.TABLENAME + "(" + Conversation.UUID + Conversation.TABLENAME + "(" + Conversation.UUID
+ ") ON DELETE CASCADE);"); + ") ON DELETE CASCADE);");
db.execSQL(CREATE_MESSAGE_TIME_INDEX);
db.execSQL(CREATE_CONTATCS_STATEMENT); db.execSQL(CREATE_CONTATCS_STATEMENT);
db.execSQL(CREATE_DISCOVERY_RESULTS_STATEMENT); db.execSQL(CREATE_DISCOVERY_RESULTS_STATEMENT);
db.execSQL(CREATE_SESSIONS_STATEMENT); db.execSQL(CREATE_SESSIONS_STATEMENT);
@ -206,7 +194,6 @@ public class DatabaseBackend extends SQLiteOpenHelper {
db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT); db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT);
db.execSQL(CREATE_IDENTITIES_STATEMENT); db.execSQL(CREATE_IDENTITIES_STATEMENT);
db.execSQL(CREATE_PRESENCE_TEMPLATES_STATEMENT); db.execSQL(CREATE_PRESENCE_TEMPLATES_STATEMENT);
db.execSQL(CREATE_START_TIMES_TABLE);
} }
@Override @Override
@ -305,16 +292,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
deleteSession(db, account, ownAddress); deleteSession(db, account, ownAddress);
IdentityKeyPair identityKeyPair = loadOwnIdentityKeyPair(db, account); IdentityKeyPair identityKeyPair = loadOwnIdentityKeyPair(db, account);
if (identityKeyPair != null) { if (identityKeyPair != null) {
String[] selectionArgs = { setIdentityKeyTrust(db, account, identityKeyPair.getPublicKey().getFingerprint().replaceAll("\\s", ""), XmppAxolotlSession.Trust.TRUSTED);
account.getUuid(),
identityKeyPair.getPublicKey().getFingerprint().replaceAll("\\s", "")
};
ContentValues values = new ContentValues();
values.put(SQLiteAxolotlStore.TRUSTED, 2);
db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, values,
SQLiteAxolotlStore.ACCOUNT + " = ? AND "
+ SQLiteAxolotlStore.FINGERPRINT + " = ? ",
selectionArgs);
} else { } else {
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not load own identity key pair"); Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not load own identity key pair");
} }
@ -360,85 +338,6 @@ public class DatabaseBackend extends SQLiteOpenHelper {
if (oldVersion < 29 && newVersion >= 29) { if (oldVersion < 29 && newVersion >= 29) {
db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.ERROR_MESSAGE + " TEXT"); db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.ERROR_MESSAGE + " TEXT");
} }
if (oldVersion < 30 && newVersion >= 30) {
db.execSQL(CREATE_START_TIMES_TABLE);
}
if (oldVersion < 31 && newVersion >= 31) {
db.execSQL("ALTER TABLE "+ SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN "+SQLiteAxolotlStore.TRUST + " TEXT");
db.execSQL("ALTER TABLE "+ SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN "+SQLiteAxolotlStore.ACTIVE + " NUMBER");
HashMap<Integer,ContentValues> migration = new HashMap<>();
migration.put(0,createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED,true));
migration.put(1,createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED, true));
migration.put(2,createFingerprintStatusContentValues(FingerprintStatus.Trust.UNTRUSTED, true));
migration.put(3,createFingerprintStatusContentValues(FingerprintStatus.Trust.COMPROMISED, false));
migration.put(4,createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED, false));
migration.put(5,createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED, false));
migration.put(6,createFingerprintStatusContentValues(FingerprintStatus.Trust.UNTRUSTED, false));
migration.put(7,createFingerprintStatusContentValues(FingerprintStatus.Trust.VERIFIED_X509, true));
migration.put(8,createFingerprintStatusContentValues(FingerprintStatus.Trust.VERIFIED_X509, false));
for(Map.Entry<Integer,ContentValues> entry : migration.entrySet()) {
String whereClause = SQLiteAxolotlStore.TRUSTED+"=?";
String[] where = {String.valueOf(entry.getKey())};
db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME,entry.getValue(),whereClause,where);
}
}
if (oldVersion < 32 && newVersion >= 32) {
db.execSQL("ALTER TABLE "+ SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN "+SQLiteAxolotlStore.LAST_ACTIVATION + " NUMBER");
ContentValues defaults = new ContentValues();
defaults.put(SQLiteAxolotlStore.LAST_ACTIVATION,System.currentTimeMillis());
db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME,defaults,null,null);
}
if (oldVersion < 33 && newVersion >= 33) {
String whereClause = SQLiteAxolotlStore.OWN+"=1";
db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME,createFingerprintStatusContentValues(FingerprintStatus.Trust.VERIFIED,true),whereClause,null);
}
if (oldVersion < 34 && newVersion >= 34) {
db.execSQL(CREATE_MESSAGE_TIME_INDEX);
final File oldPicturesDirectory = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)+"/Conversations/");
final File oldFilesDirectory = new File(Environment.getExternalStorageDirectory() + "/Conversations/");
final File newFilesDirectory = new File(Environment.getExternalStorageDirectory() + "/Conversations/Media/Conversations Files/");
final File newVideosDirectory = new File(Environment.getExternalStorageDirectory() + "/Conversations/Media/Conversations Videos/");
if (oldPicturesDirectory.exists() && oldPicturesDirectory.isDirectory()) {
final File newPicturesDirectory = new File(Environment.getExternalStorageDirectory() + "/Conversations/Media/Conversations Images/");
newPicturesDirectory.getParentFile().mkdirs();
if (oldPicturesDirectory.renameTo(newPicturesDirectory)) {
Log.d(Config.LOGTAG,"moved "+oldPicturesDirectory.getAbsolutePath()+" to "+newPicturesDirectory.getAbsolutePath());
}
}
if (oldFilesDirectory.exists() && oldFilesDirectory.isDirectory()) {
newFilesDirectory.mkdirs();
newVideosDirectory.mkdirs();
for(File file : oldFilesDirectory.listFiles()) {
if (file.getName().equals(".nomedia")) {
if (file.delete()) {
Log.d(Config.LOGTAG,"deleted nomedia file in "+oldFilesDirectory.getAbsolutePath());
}
} else if (file.isFile()) {
final String name = file.getName();
boolean isVideo = false;
int start = name.lastIndexOf('.') + 1;
if (start < name.length()) {
String mime= MimeUtils.guessMimeTypeFromExtension(name.substring(start));
isVideo = mime != null && mime.startsWith("video/");
}
File dst = new File((isVideo ? newVideosDirectory : newFilesDirectory).getAbsolutePath()+"/"+file.getName());
if (file.renameTo(dst)) {
Log.d(Config.LOGTAG, "moved " + file + " to " + dst);
}
}
}
}
}
}
private static ContentValues createFingerprintStatusContentValues(FingerprintStatus.Trust trust, boolean active) {
ContentValues values = new ContentValues();
values.put(SQLiteAxolotlStore.TRUST,trust.toString());
values.put(SQLiteAxolotlStore.ACTIVE,active ? 1 : 0);
return values;
} }
private void canonicalizeJids(SQLiteDatabase db) { private void canonicalizeJids(SQLiteDatabase db) {
@ -813,13 +712,6 @@ public class DatabaseBackend extends SQLiteOpenHelper {
db.delete(Message.TABLENAME, Message.CONVERSATION + "=?", args); db.delete(Message.TABLENAME, Message.CONVERSATION + "=?", args);
} }
public boolean expireOldMessages(long timestamp) {
String where = Message.TIME_SENT+"<?";
String[] whereArgs = {String.valueOf(timestamp)};
SQLiteDatabase db = this.getReadableDatabase();
return db.delete(Message.TABLENAME,where,whereArgs) > 0;
}
public Pair<Long, String> getLastMessageReceived(Account account) { public Pair<Long, String> getLastMessageReceived(Account account) {
Cursor cursor = null; Cursor cursor = null;
try { try {
@ -842,20 +734,6 @@ public class DatabaseBackend extends SQLiteOpenHelper {
} }
} }
public long getLastTimeFingerprintUsed(Account account, String fingerprint) {
String SQL = "select messages.timeSent from accounts join conversations on accounts.uuid=conversations.accountUuid join messages on conversations.uuid=messages.conversationUuid where accounts.uuid=? and messages.axolotl_fingerprint=? order by messages.timesent desc limit 1";
String[] args = {account.getUuid(), fingerprint};
Cursor cursor = getReadableDatabase().rawQuery(SQL,args);
long time;
if (cursor.moveToFirst()) {
time = cursor.getLong(0);
} else {
time = 0;
}
cursor.close();
return time;
}
public Pair<Long,String> getLastClearDate(Account account) { public Pair<Long,String> getLastClearDate(Account account) {
SQLiteDatabase db = this.getReadableDatabase(); SQLiteDatabase db = this.getReadableDatabase();
String[] columns = {Conversation.ATTRIBUTES}; String[] columns = {Conversation.ATTRIBUTES};
@ -1120,9 +998,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
} }
private Cursor getIdentityKeyCursor(SQLiteDatabase db, Account account, String name, Boolean own, String fingerprint) { private Cursor getIdentityKeyCursor(SQLiteDatabase db, Account account, String name, Boolean own, String fingerprint) {
String[] columns = {SQLiteAxolotlStore.TRUST, String[] columns = {SQLiteAxolotlStore.TRUSTED,
SQLiteAxolotlStore.ACTIVE,
SQLiteAxolotlStore.LAST_ACTIVATION,
SQLiteAxolotlStore.KEY}; SQLiteAxolotlStore.KEY};
ArrayList<String> selectionArgs = new ArrayList<>(4); ArrayList<String> selectionArgs = new ArrayList<>(4);
selectionArgs.add(account.getUuid()); selectionArgs.add(account.getUuid());
@ -1174,21 +1050,18 @@ public class DatabaseBackend extends SQLiteOpenHelper {
return loadIdentityKeys(account, name, null); return loadIdentityKeys(account, name, null);
} }
public Set<IdentityKey> loadIdentityKeys(Account account, String name, FingerprintStatus status) { public Set<IdentityKey> loadIdentityKeys(Account account, String name, XmppAxolotlSession.Trust trust) {
Set<IdentityKey> identityKeys = new HashSet<>(); Set<IdentityKey> identityKeys = new HashSet<>();
Cursor cursor = getIdentityKeyCursor(account, name, false); Cursor cursor = getIdentityKeyCursor(account, name, false);
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
if (status != null && !FingerprintStatus.fromCursor(cursor).equals(status)) { if (trust != null &&
cursor.getInt(cursor.getColumnIndex(SQLiteAxolotlStore.TRUSTED))
!= trust.getCode()) {
continue; continue;
} }
try { try {
String key = cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)); identityKeys.add(new IdentityKey(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), Base64.DEFAULT), 0));
if (key != null) {
identityKeys.add(new IdentityKey(Base64.decode(key, Base64.DEFAULT), 0));
} else {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Missing key (possibly preverified) in database for account" + account.getJid().toBareJid() + ", address: " + name);
}
} catch (InvalidKeyException e) { } catch (InvalidKeyException e) {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Encountered invalid IdentityKey in database for account" + account.getJid().toBareJid() + ", address: " + name); Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Encountered invalid IdentityKey in database for account" + account.getJid().toBareJid() + ", address: " + name);
} }
@ -1203,20 +1076,22 @@ public class DatabaseBackend extends SQLiteOpenHelper {
String[] args = { String[] args = {
account.getUuid(), account.getUuid(),
name, name,
FingerprintStatus.Trust.TRUSTED.toString(), String.valueOf(XmppAxolotlSession.Trust.TRUSTED.getCode()),
FingerprintStatus.Trust.VERIFIED.toString(), String.valueOf(XmppAxolotlSession.Trust.TRUSTED_X509.getCode())
FingerprintStatus.Trust.VERIFIED_X509.toString()
}; };
return DatabaseUtils.queryNumEntries(db, SQLiteAxolotlStore.IDENTITIES_TABLENAME, return DatabaseUtils.queryNumEntries(db, SQLiteAxolotlStore.IDENTITIES_TABLENAME,
SQLiteAxolotlStore.ACCOUNT + " = ?" SQLiteAxolotlStore.ACCOUNT + " = ?"
+ " AND " + SQLiteAxolotlStore.NAME + " = ?" + " AND " + SQLiteAxolotlStore.NAME + " = ?"
+ " AND (" + SQLiteAxolotlStore.TRUST + " = ? OR " + SQLiteAxolotlStore.TRUST + " = ? OR " +SQLiteAxolotlStore.TRUST +" = ?)" + " AND (" + SQLiteAxolotlStore.TRUSTED + " = ? OR " + SQLiteAxolotlStore.TRUSTED + " = ?)",
+ " AND " +SQLiteAxolotlStore.ACTIVE + " > 0",
args args
); );
} }
private void storeIdentityKey(Account account, String name, boolean own, String fingerprint, String base64Serialized, FingerprintStatus status) { private void storeIdentityKey(Account account, String name, boolean own, String fingerprint, String base64Serialized) {
storeIdentityKey(account, name, own, fingerprint, base64Serialized, XmppAxolotlSession.Trust.UNDECIDED);
}
private void storeIdentityKey(Account account, String name, boolean own, String fingerprint, String base64Serialized, XmppAxolotlSession.Trust trusted) {
SQLiteDatabase db = this.getWritableDatabase(); SQLiteDatabase db = this.getWritableDatabase();
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid()); values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid());
@ -1224,50 +1099,35 @@ public class DatabaseBackend extends SQLiteOpenHelper {
values.put(SQLiteAxolotlStore.OWN, own ? 1 : 0); values.put(SQLiteAxolotlStore.OWN, own ? 1 : 0);
values.put(SQLiteAxolotlStore.FINGERPRINT, fingerprint); values.put(SQLiteAxolotlStore.FINGERPRINT, fingerprint);
values.put(SQLiteAxolotlStore.KEY, base64Serialized); values.put(SQLiteAxolotlStore.KEY, base64Serialized);
values.putAll(status.toContentValues()); values.put(SQLiteAxolotlStore.TRUSTED, trusted.getCode());
String where = SQLiteAxolotlStore.ACCOUNT+"=? AND "+SQLiteAxolotlStore.NAME+"=? AND "+SQLiteAxolotlStore.FINGERPRINT+" =?";
String[] whereArgs = {account.getUuid(),name,fingerprint};
int rows = db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME,values,where,whereArgs);
if (rows == 0) {
db.insert(SQLiteAxolotlStore.IDENTITIES_TABLENAME, null, values);
}
}
public void storePreVerification(Account account, String name, String fingerprint, FingerprintStatus status) {
SQLiteDatabase db = this.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid());
values.put(SQLiteAxolotlStore.NAME, name);
values.put(SQLiteAxolotlStore.OWN, 0);
values.put(SQLiteAxolotlStore.FINGERPRINT, fingerprint);
values.putAll(status.toContentValues());
db.insert(SQLiteAxolotlStore.IDENTITIES_TABLENAME, null, values); db.insert(SQLiteAxolotlStore.IDENTITIES_TABLENAME, null, values);
} }
public FingerprintStatus getFingerprintStatus(Account account, String fingerprint) { public XmppAxolotlSession.Trust isIdentityKeyTrusted(Account account, String fingerprint) {
Cursor cursor = getIdentityKeyCursor(account, fingerprint); Cursor cursor = getIdentityKeyCursor(account, fingerprint);
final FingerprintStatus status; XmppAxolotlSession.Trust trust = null;
if (cursor.getCount() > 0) { if (cursor.getCount() > 0) {
cursor.moveToFirst(); cursor.moveToFirst();
status = FingerprintStatus.fromCursor(cursor); int trustValue = cursor.getInt(cursor.getColumnIndex(SQLiteAxolotlStore.TRUSTED));
} else { trust = XmppAxolotlSession.Trust.fromCode(trustValue);
status = null;
} }
cursor.close(); cursor.close();
return status; return trust;
} }
public boolean setIdentityKeyTrust(Account account, String fingerprint, FingerprintStatus fingerprintStatus) { public boolean setIdentityKeyTrust(Account account, String fingerprint, XmppAxolotlSession.Trust trust) {
SQLiteDatabase db = this.getWritableDatabase(); SQLiteDatabase db = this.getWritableDatabase();
return setIdentityKeyTrust(db, account, fingerprint, fingerprintStatus); return setIdentityKeyTrust(db, account, fingerprint, trust);
} }
private boolean setIdentityKeyTrust(SQLiteDatabase db, Account account, String fingerprint, FingerprintStatus status) { private boolean setIdentityKeyTrust(SQLiteDatabase db, Account account, String fingerprint, XmppAxolotlSession.Trust trust) {
String[] selectionArgs = { String[] selectionArgs = {
account.getUuid(), account.getUuid(),
fingerprint fingerprint
}; };
int rows = db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, status.toContentValues(), ContentValues values = new ContentValues();
values.put(SQLiteAxolotlStore.TRUSTED, trust.getCode());
int rows = db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, values,
SQLiteAxolotlStore.ACCOUNT + " = ? AND " SQLiteAxolotlStore.ACCOUNT + " = ? AND "
+ SQLiteAxolotlStore.FINGERPRINT + " = ? ", + SQLiteAxolotlStore.FINGERPRINT + " = ? ",
selectionArgs); selectionArgs);
@ -1321,12 +1181,12 @@ public class DatabaseBackend extends SQLiteOpenHelper {
} }
} }
public void storeIdentityKey(Account account, String name, IdentityKey identityKey, FingerprintStatus status) { public void storeIdentityKey(Account account, String name, IdentityKey identityKey) {
storeIdentityKey(account, name, false, identityKey.getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT), status); storeIdentityKey(account, name, false, identityKey.getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT));
} }
public void storeOwnIdentityKeyPair(Account account, IdentityKeyPair identityKeyPair) { public void storeOwnIdentityKeyPair(Account account, IdentityKeyPair identityKeyPair) {
storeIdentityKey(account, account.getJid().toBareJid().toPreppedString(), true, identityKeyPair.getPublicKey().getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKeyPair.serialize(), Base64.DEFAULT), FingerprintStatus.createActiveVerified(false)); storeIdentityKey(account, account.getJid().toBareJid().toPreppedString(), true, identityKeyPair.getPublicKey().getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKeyPair.serialize(), Base64.DEFAULT), XmppAxolotlSession.Trust.TRUSTED);
} }
@ -1362,35 +1222,4 @@ public class DatabaseBackend extends SQLiteOpenHelper {
SQLiteAxolotlStore.ACCOUNT + " = ?", SQLiteAxolotlStore.ACCOUNT + " = ?",
deleteArgs); deleteArgs);
} }
public boolean startTimeCountExceedsThreshold() {
SQLiteDatabase db = this.getWritableDatabase();
long cleanBeforeTimestamp = System.currentTimeMillis() - Config.FREQUENT_RESTARTS_DETECTION_WINDOW;
db.execSQL("delete from "+START_TIMES_TABLE+" where timestamp < "+cleanBeforeTimestamp);
ContentValues values = new ContentValues();
values.put("timestamp",System.currentTimeMillis());
db.insert(START_TIMES_TABLE,null,values);
String[] columns = new String[]{"count(timestamp)"};
Cursor cursor = db.query(START_TIMES_TABLE,columns,null,null,null,null,null);
int count;
if (cursor.moveToFirst()) {
count = cursor.getInt(0);
} else {
count = 0;
}
cursor.close();
Log.d(Config.LOGTAG,"start time counter reached "+count);
return count >= Config.FREQUENT_RESTARTS_THRESHOLD;
}
public void clearStartTimeCounter(boolean justOne) {
SQLiteDatabase db = this.getWritableDatabase();
if (justOne) {
db.execSQL("delete from "+START_TIMES_TABLE+" where timestamp in (select timestamp from "+START_TIMES_TABLE+" order by timestamp desc limit 1)");
Log.d(Config.LOGTAG,"do not count start up after being swiped away");
} else {
Log.d(Config.LOGTAG,"resetting start time counter");
db.execSQL("delete from " + START_TIMES_TABLE);
}
}
} }

View file

@ -9,7 +9,6 @@ import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Matrix; import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.RectF; import android.graphics.RectF;
import android.media.MediaMetadataRetriever; import android.media.MediaMetadataRetriever;
import android.net.Uri; import android.net.Uri;
@ -55,14 +54,12 @@ import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.ExifHelper; import eu.siacs.conversations.utils.ExifHelper;
import eu.siacs.conversations.utils.FileUtils; import eu.siacs.conversations.utils.FileUtils;
import eu.siacs.conversations.utils.FileWriterException;
import eu.siacs.conversations.utils.MimeUtils;
import eu.siacs.conversations.xmpp.pep.Avatar; import eu.siacs.conversations.xmpp.pep.Avatar;
public class FileBackend { public class FileBackend {
private static final SimpleDateFormat IMAGE_DATE_FORMAT = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US); private static final SimpleDateFormat IMAGE_DATE_FORMAT = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US);
private static final String FILE_PROVIDER = ".files"; public static final String CONVERSATIONS_FILE_PROVIDER = "eu.siacs.conversations.files";
private XmppConnectionService mXmppConnectionService; private XmppConnectionService mXmppConnectionService;
@ -71,7 +68,7 @@ public class FileBackend {
} }
private void createNoMedia() { private void createNoMedia() {
final File nomedia = new File(getConversationsDirectory("Files")+".nomedia"); final File nomedia = new File(getConversationsFileDirectory()+".nomedia");
if (!nomedia.exists()) { if (!nomedia.exists()) {
try { try {
nomedia.createNewFile(); nomedia.createNewFile();
@ -82,8 +79,7 @@ public class FileBackend {
} }
public void updateMediaScanner(File file) { public void updateMediaScanner(File file) {
String path = file.getAbsolutePath(); if (file.getAbsolutePath().startsWith(getConversationsImageDirectory())) {
if (!path.startsWith(getConversationsDirectory("Files"))) {
Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
intent.setData(Uri.fromFile(file)); intent.setData(Uri.fromFile(file));
mXmppConnectionService.sendBroadcast(intent); mXmppConnectionService.sendBroadcast(intent);
@ -119,16 +115,14 @@ public class FileBackend {
file = new DownloadableFile(path); file = new DownloadableFile(path);
} else { } else {
String mime = message.getMimeType(); String mime = message.getMimeType();
if (mime != null && mime.startsWith("image/")) { if (mime != null && mime.startsWith("image")) {
file = new DownloadableFile(getConversationsDirectory("Images") + path); file = new DownloadableFile(getConversationsImageDirectory() + path);
} else if (mime != null && mime.startsWith("video/")) {
file = new DownloadableFile(getConversationsDirectory("Videos") + path);
} else { } else {
file = new DownloadableFile(getConversationsDirectory("Files") + path); file = new DownloadableFile(getConversationsFileDirectory() + path);
} }
} }
if (encrypted) { if (encrypted) {
return new DownloadableFile(getConversationsDirectory("Files") + file.getName() + ".pgp"); return new DownloadableFile(getConversationsFileDirectory() + file.getName() + ".pgp");
} else { } else {
return file; return file;
} }
@ -157,16 +151,14 @@ public class FileBackend {
return true; return true;
} }
public String getConversationsDirectory(final String type) { public static String getConversationsFileDirectory() {
if (Config.ONLY_INTERNAL_STORAGE) { return Environment.getExternalStorageDirectory().getAbsolutePath()+"/Conversations/";
return mXmppConnectionService.getFilesDir().getAbsolutePath()+"/"+type+"/";
} else {
return Environment.getExternalStorageDirectory() +"/Conversations/Media/Conversations "+type+"/";
}
} }
public static String getConversationsLogsDirectory() { public static String getConversationsImageDirectory() {
return Environment.getExternalStorageDirectory().getAbsolutePath()+"/Conversations/"; return Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES).getAbsolutePath()
+ "/Conversations/";
} }
public Bitmap resize(Bitmap originalBitmap, int size) { public Bitmap resize(Bitmap originalBitmap, int size) {
@ -246,21 +238,11 @@ public class FileBackend {
byte[] buffer = new byte[1024]; byte[] buffer = new byte[1024];
int length; int length;
while ((length = is.read(buffer)) > 0) { while ((length = is.read(buffer)) > 0) {
try { os.write(buffer, 0, length);
os.write(buffer, 0, length);
} catch (IOException e) {
throw new FileWriterException();
}
}
try {
os.flush();
} catch (IOException e) {
throw new FileWriterException();
} }
os.flush();
} catch(FileNotFoundException e) { } catch(FileNotFoundException e) {
throw new FileCopyException(R.string.error_file_not_found); throw new FileCopyException(R.string.error_file_not_found);
} catch(FileWriterException e) {
throw new FileCopyException(R.string.error_unable_to_create_temporary_file);
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
throw new FileCopyException(R.string.error_io_exception); throw new FileCopyException(R.string.error_io_exception);
@ -271,7 +253,7 @@ public class FileBackend {
} }
public void copyFileToPrivateStorage(Message message, Uri uri) throws FileCopyException { public void copyFileToPrivateStorage(Message message, Uri uri) throws FileCopyException {
String mime = MimeUtils.guessMimeTypeFromUri(mXmppConnectionService, uri); String mime = mXmppConnectionService.getContentResolver().getType(uri);
Log.d(Config.LOGTAG, "copy " + uri.toString() + " to private storage (mime="+mime+")"); Log.d(Config.LOGTAG, "copy " + uri.toString() + " to private storage (mime="+mime+")");
String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mime); String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mime);
if (extension == null) { if (extension == null) {
@ -305,13 +287,8 @@ public class FileBackend {
InputStream is = null; InputStream is = null;
OutputStream os = null; OutputStream os = null;
try { try {
if (!file.exists() && !file.createNewFile()) { file.createNewFile();
throw new FileCopyException(R.string.error_unable_to_create_temporary_file);
}
is = mXmppConnectionService.getContentResolver().openInputStream(image); is = mXmppConnectionService.getContentResolver().openInputStream(image);
if (is == null) {
throw new FileCopyException(R.string.error_not_an_image_file);
}
Bitmap originalBitmap; Bitmap originalBitmap;
BitmapFactory.Options options = new BitmapFactory.Options(); BitmapFactory.Options options = new BitmapFactory.Options();
int inSampleSize = (int) Math.pow(2, sampleSize); int inSampleSize = (int) Math.pow(2, sampleSize);
@ -338,6 +315,7 @@ public class FileBackend {
quality -= 5; quality -= 5;
} }
scaledBitmap.recycle(); scaledBitmap.recycle();
return;
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
throw new FileCopyException(R.string.error_file_not_found); throw new FileCopyException(R.string.error_file_not_found);
} catch (IOException e) { } catch (IOException e) {
@ -352,6 +330,8 @@ public class FileBackend {
} else { } else {
throw new FileCopyException(R.string.error_out_of_memory); throw new FileCopyException(R.string.error_out_of_memory);
} }
} catch (NullPointerException e) {
throw new FileCopyException(R.string.error_io_exception);
} finally { } finally {
close(os); close(os);
close(is); close(is);
@ -406,8 +386,7 @@ public class FileBackend {
return thumbnail; return thumbnail;
} }
DownloadableFile file = getFile(message); DownloadableFile file = getFile(message);
final String mime = file.getMimeType(); if (file.getMimeType().startsWith("video/")) {
if (mime.startsWith("video/")) {
thumbnail = getVideoPreview(file, size); thumbnail = getVideoPreview(file, size);
} else { } else {
Bitmap fullsize = getFullsizeImagePreview(file, size); Bitmap fullsize = getFullsizeImagePreview(file, size);
@ -416,12 +395,6 @@ public class FileBackend {
} }
thumbnail = resize(fullsize, size); thumbnail = resize(fullsize, size);
thumbnail = rotate(thumbnail, getRotation(file)); thumbnail = rotate(thumbnail, getRotation(file));
if (mime.equals("image/gif")) {
Bitmap withGifOverlay = thumbnail.copy(Bitmap.Config.ARGB_8888,true);
drawOverlay(withGifOverlay,R.drawable.play_gif,1.0f);
thumbnail.recycle();
thumbnail = withGifOverlay;
}
} }
this.mXmppConnectionService.getBitmapCache().put(uuid, thumbnail); this.mXmppConnectionService.getBitmapCache().put(uuid, thumbnail);
} }
@ -440,21 +413,6 @@ public class FileBackend {
} }
} }
private void drawOverlay(Bitmap bitmap, int resource, float factor) {
Bitmap overlay = BitmapFactory.decodeResource(mXmppConnectionService.getResources(), resource);
Canvas canvas = new Canvas(bitmap);
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setFilterBitmap(true);
paint.setDither(true);
float targetSize = Math.min(canvas.getWidth(),canvas.getHeight()) * factor;
Log.d(Config.LOGTAG,"target size overlay: "+targetSize+" overlay bitmap size was "+overlay.getHeight());
float left = (canvas.getWidth() - targetSize) / 2.0f;
float top = (canvas.getHeight() - targetSize) / 2.0f;
RectF dst = new RectF(left,top,left+targetSize-1,top+targetSize-1);
canvas.drawBitmap(overlay,null,dst,paint);
}
private Bitmap getVideoPreview(File file, int size) { private Bitmap getVideoPreview(File file, int size) {
MediaMetadataRetriever metadataRetriever = new MediaMetadataRetriever(); MediaMetadataRetriever metadataRetriever = new MediaMetadataRetriever();
Bitmap frame; Bitmap frame;
@ -467,7 +425,11 @@ public class FileBackend {
frame = Bitmap.createBitmap(size,size, Bitmap.Config.ARGB_8888); frame = Bitmap.createBitmap(size,size, Bitmap.Config.ARGB_8888);
frame.eraseColor(0xff000000); frame.eraseColor(0xff000000);
} }
drawOverlay(frame,R.drawable.play_video,0.75f); Canvas canvas = new Canvas(frame);
Bitmap play = BitmapFactory.decodeResource(mXmppConnectionService.getResources(), R.drawable.play_video);
float x = (frame.getWidth() - play.getWidth()) / 2.0f;
float y = (frame.getHeight() - play.getHeight()) / 2.0f;
canvas.drawBitmap(play,x,y,null);
return frame; return frame;
} }
@ -476,35 +438,17 @@ public class FileBackend {
} }
public Uri getTakePhotoUri() { public Uri getTakePhotoUri() {
File file; File file = new File(getTakePhotoPath()+"IMG_" + this.IMAGE_DATE_FORMAT.format(new Date()) + ".jpg");
if (Config.ONLY_INTERNAL_STORAGE) {
file = new File(mXmppConnectionService.getCacheDir().getAbsolutePath(), "Camera/IMG_" + this.IMAGE_DATE_FORMAT.format(new Date()) + ".jpg");
} else {
file = new File(getTakePhotoPath() + "IMG_" + this.IMAGE_DATE_FORMAT.format(new Date()) + ".jpg");
}
file.getParentFile().mkdirs(); file.getParentFile().mkdirs();
return getUriForFile(mXmppConnectionService,file); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
} return FileProvider.getUriForFile(mXmppConnectionService, CONVERSATIONS_FILE_PROVIDER, file);
public static Uri getUriForFile(Context context, File file) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N || Config.ONLY_INTERNAL_STORAGE) {
try {
String packageId = context.getPackageName();
return FileProvider.getUriForFile(context, packageId + FILE_PROVIDER, file);
} catch(IllegalArgumentException e) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
throw new SecurityException();
} else {
return Uri.fromFile(file);
}
}
} else { } else {
return Uri.fromFile(file); return Uri.fromFile(file);
} }
} }
public static Uri getIndexableTakePhotoUri(Uri original) { public static Uri getIndexableTakePhotoUri(Uri original) {
if (Config.ONLY_INTERNAL_STORAGE || "file".equals(original.getScheme())) { if ("file".equals(original.getScheme())) {
return original; return original;
} else { } else {
List<String> segments = original.getPathSegments(); List<String> segments = original.getPathSegments();
@ -548,7 +492,6 @@ public class FileBackend {
File file = new File(getAvatarPath(hash)); File file = new File(getAvatarPath(hash));
FileInputStream is = null; FileInputStream is = null;
try { try {
avatar.size = file.length();
BitmapFactory.Options options = new BitmapFactory.Options(); BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true; options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(file.getAbsolutePath(), options); BitmapFactory.decodeFile(file.getAbsolutePath(), options);
@ -568,7 +511,6 @@ public class FileBackend {
avatar.image = new String(mByteArrayOutputStream.toByteArray()); avatar.image = new String(mByteArrayOutputStream.toByteArray());
avatar.height = options.outHeight; avatar.height = options.outHeight;
avatar.width = options.outWidth; avatar.width = options.outWidth;
avatar.type = options.outMimeType;
return avatar; return avatar;
} catch (IOException e) { } catch (IOException e) {
return null; return null;
@ -588,7 +530,6 @@ public class FileBackend {
File file; File file;
if (isAvatarCached(avatar)) { if (isAvatarCached(avatar)) {
file = new File(getAvatarPath(avatar.getFilename())); file = new File(getAvatarPath(avatar.getFilename()));
avatar.size = file.length();
} else { } else {
String filename = getAvatarPath(avatar.getFilename()); String filename = getAvatarPath(avatar.getFilename());
file = new File(filename + ".tmp"); file = new File(filename + ".tmp");
@ -600,8 +541,7 @@ public class FileBackend {
MessageDigest digest = MessageDigest.getInstance("SHA-1"); MessageDigest digest = MessageDigest.getInstance("SHA-1");
digest.reset(); digest.reset();
DigestOutputStream mDigestOutputStream = new DigestOutputStream(os, digest); DigestOutputStream mDigestOutputStream = new DigestOutputStream(os, digest);
final byte[] bytes = avatar.getImageAsBytes(); mDigestOutputStream.write(avatar.getImageAsBytes());
mDigestOutputStream.write(bytes);
mDigestOutputStream.flush(); mDigestOutputStream.flush();
mDigestOutputStream.close(); mDigestOutputStream.close();
String sha1sum = CryptoHelper.bytesToHex(digest.digest()); String sha1sum = CryptoHelper.bytesToHex(digest.digest());
@ -612,13 +552,13 @@ public class FileBackend {
file.delete(); file.delete();
return false; return false;
} }
avatar.size = bytes.length;
} catch (IllegalArgumentException | IOException | NoSuchAlgorithmException e) { } catch (IllegalArgumentException | IOException | NoSuchAlgorithmException e) {
return false; return false;
} finally { } finally {
close(os); close(os);
} }
} }
avatar.size = file.length();
return true; return true;
} }
@ -688,7 +628,7 @@ public class FileBackend {
Bitmap dest = Bitmap.createBitmap(newWidth, newHeight, Bitmap.Config.ARGB_8888); Bitmap dest = Bitmap.createBitmap(newWidth, newHeight, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(dest); Canvas canvas = new Canvas(dest);
canvas.drawBitmap(source, null, targetRect, null); canvas.drawBitmap(source, null, targetRect, null);
if (source.isRecycled()) { if (source != null && !source.isRecycled()) {
source.recycle(); source.recycle();
} }
return dest; return dest;
@ -753,6 +693,11 @@ public class FileBackend {
return inSampleSize; return inSampleSize;
} }
public Uri getJingleFileUri(Message message) {
File file = getFile(message);
return Uri.parse("file://" + file.getAbsolutePath());
}
public void updateFileParams(Message message) { public void updateFileParams(Message message) {
updateFileParams(message,null); updateFileParams(message,null);
} }

View file

@ -5,7 +5,6 @@ import android.content.Context;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.os.Build; import android.os.Build;
import android.os.PowerManager; import android.os.PowerManager;
import android.os.SystemClock;
import android.util.Log; import android.util.Log;
import android.util.Pair; import android.util.Pair;
@ -23,7 +22,6 @@ import java.io.OutputStream;
import java.security.InvalidAlgorithmParameterException; import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException; import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.concurrent.atomic.AtomicLong;
import javax.crypto.Cipher; import javax.crypto.Cipher;
import javax.crypto.CipherInputStream; import javax.crypto.CipherInputStream;
@ -38,9 +36,6 @@ import eu.siacs.conversations.entities.DownloadableFile;
public class AbstractConnectionManager { public class AbstractConnectionManager {
protected XmppConnectionService mXmppConnectionService; protected XmppConnectionService mXmppConnectionService;
private static final int UI_REFRESH_THRESHOLD = 250;
private static final AtomicLong LAST_UI_UPDATE_CALL = new AtomicLong(0);
public AbstractConnectionManager(XmppConnectionService service) { public AbstractConnectionManager(XmppConnectionService service) {
this.mXmppConnectionService = service; this.mXmppConnectionService = service;
} }
@ -60,7 +55,7 @@ public class AbstractConnectionManager {
} }
public boolean hasStoragePermission() { public boolean hasStoragePermission() {
if (!Config.ONLY_INTERNAL_STORAGE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return mXmppConnectionService.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; return mXmppConnectionService.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
} else { } else {
return true; return true;
@ -141,15 +136,6 @@ public class AbstractConnectionManager {
} }
} }
public void updateConversationUi(boolean force) {
synchronized (LAST_UI_UPDATE_CALL) {
if (force || SystemClock.elapsedRealtime() - LAST_UI_UPDATE_CALL.get() >= UI_REFRESH_THRESHOLD) {
LAST_UI_UPDATE_CALL.set(SystemClock.elapsedRealtime());
mXmppConnectionService.updateConversationUi();
}
}
}
public PowerManager.WakeLock createWakeLock(String name) { public PowerManager.WakeLock createWakeLock(String name) {
PowerManager powerManager = (PowerManager) mXmppConnectionService.getSystemService(Context.POWER_SERVICE); PowerManager powerManager = (PowerManager) mXmppConnectionService.getSystemService(Context.POWER_SERVICE);
return powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,name); return powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,name);

View file

@ -44,9 +44,6 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
} }
private Bitmap get(final Contact contact, final int size, boolean cachedOnly) { private Bitmap get(final Contact contact, final int size, boolean cachedOnly) {
if (contact.isSelf()) {
return get(contact.getAccount(),size,cachedOnly);
}
final String KEY = key(contact, size); final String KEY = key(contact, size);
Bitmap avatar = this.mXmppConnectionService.getBitmapCache().get(KEY); Bitmap avatar = this.mXmppConnectionService.getBitmapCache().get(KEY);
if (avatar != null || cachedOnly) { if (avatar != null || cachedOnly) {
@ -172,7 +169,7 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
if (bitmap != null || cachedOnly) { if (bitmap != null || cachedOnly) {
return bitmap; return bitmap;
} }
final List<MucOptions.User> users = mucOptions.getUsers(5); final List<MucOptions.User> users = mucOptions.getUsers();
int count = users.size(); int count = users.size();
bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap); Canvas canvas = new Canvas(bitmap);
@ -321,18 +318,16 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
letter = letter.toUpperCase(Locale.getDefault()); letter = letter.toUpperCase(Locale.getDefault());
Paint tilePaint = new Paint(), textPaint = new Paint(); Paint tilePaint = new Paint(), textPaint = new Paint();
tilePaint.setColor(tileColor); tilePaint.setColor(tileColor);
textPaint.setFlags(Paint.ANTI_ALIAS_FLAG); //textPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
textPaint.setColor(FG_COLOR); //textPaint.setColor(FG_COLOR);
textPaint.setTypeface(Typeface.create("sans-serif-light", //textPaint.setTypeface(Typeface.create("sans-serif-light",Typeface.NORMAL));
Typeface.NORMAL)); //textPaint.setTextSize((float) ((right - left) * 0.8));
textPaint.setTextSize((float) ((right - left) * 0.8));
Rect rect = new Rect(); Rect rect = new Rect();
canvas.drawRect(new Rect(left, top, right, bottom), tilePaint); canvas.drawRect(new Rect(left, top, right, bottom), tilePaint);
textPaint.getTextBounds(letter, 0, 1, rect); //textPaint.getTextBounds(letter, 0, 1, rect);
float width = textPaint.measureText(letter); //float width = textPaint.measureText(letter);
canvas.drawText(letter, (right + left) / 2 - width / 2, (top + bottom) //canvas.drawText(letter, (right + left) / 2 - width / 2, (top + bottom) / 2 + rect.height() / 2, textPaint);
/ 2 + rect.height() / 2, textPaint);
return true; return true;
} }

View file

@ -1,206 +0,0 @@
package eu.siacs.conversations.services;
import android.content.ComponentName;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.net.Uri;
import android.os.CancellationSignal;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.support.annotation.Nullable;
import android.util.Log;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.aztec.AztecWriter;
import com.google.zxing.common.BitMatrix;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.util.Hashtable;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.xmpp.jid.Jid;
public class BarcodeProvider extends ContentProvider implements ServiceConnection {
private static final String AUTHORITY = ".barcodes";
private final Object lock = new Object();
private XmppConnectionService mXmppConnectionService;
private boolean mBindingInProcess = false;
@Override
public boolean onCreate() {
File barcodeDirectory = new File(getContext().getCacheDir().getAbsolutePath() + "/barcodes/");
if (barcodeDirectory.exists() && barcodeDirectory.isDirectory()) {
for (File file : barcodeDirectory.listFiles()) {
if (file.isFile() && !file.isHidden()) {
Log.d(Config.LOGTAG, "deleting old barcode file " + file.getAbsolutePath());
file.delete();
}
}
}
return true;
}
@Nullable
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
return null;
}
@Nullable
@Override
public String getType(Uri uri) {
return "image/png";
}
@Nullable
@Override
public Uri insert(Uri uri, ContentValues values) {
return null;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
return 0;
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
return 0;
}
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
return openFile(uri, mode, null);
}
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode, CancellationSignal signal) throws FileNotFoundException {
Log.d(Config.LOGTAG, "opening file with uri (normal): " + uri.toString());
String path = uri.getPath();
if (path != null && path.endsWith(".png") && path.length() >= 5) {
String jid = path.substring(1).substring(0, path.length() - 4);
Log.d(Config.LOGTAG, "account:" + jid);
if (connectAndWait()) {
Log.d(Config.LOGTAG, "connected to background service");
try {
Account account = mXmppConnectionService.findAccountByJid(Jid.fromString(jid));
if (account != null) {
String shareableUri = account.getShareableUri();
String hash = CryptoHelper.getFingerprint(shareableUri);
File file = new File(getContext().getCacheDir().getAbsolutePath() + "/barcodes/" + hash);
if (!file.exists()) {
file.getParentFile().mkdirs();
file.createNewFile();
Bitmap bitmap = createAztecBitmap(account.getShareableUri(), 1024);
OutputStream outputStream = new FileOutputStream(file);
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
outputStream.close();
outputStream.flush();
}
return ParcelFileDescriptor.open(file,ParcelFileDescriptor.MODE_READ_ONLY);
}
} catch (Exception e) {
throw new FileNotFoundException();
}
}
}
throw new FileNotFoundException();
}
private boolean connectAndWait() {
Intent intent = new Intent(getContext(), XmppConnectionService.class);
intent.setAction(this.getClass().getSimpleName());
Context context = getContext();
if (context != null) {
synchronized (this) {
if (mXmppConnectionService == null && !mBindingInProcess) {
Log.d(Config.LOGTAG,"calling to bind service");
context.startService(intent);
context.bindService(intent, this, Context.BIND_AUTO_CREATE);
this.mBindingInProcess = true;
}
}
try {
waitForService();
return true;
} catch (InterruptedException e) {
return false;
}
} else {
Log.d(Config.LOGTAG, "context was null");
return false;
}
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
synchronized (this) {
XmppConnectionService.XmppConnectionBinder binder = (XmppConnectionService.XmppConnectionBinder) service;
mXmppConnectionService = binder.getService();
mBindingInProcess = false;
synchronized (this.lock) {
lock.notifyAll();
}
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
synchronized (this) {
mXmppConnectionService = null;
}
}
private void waitForService() throws InterruptedException {
if (mXmppConnectionService == null) {
synchronized (this.lock) {
lock.wait();
}
} else {
Log.d(Config.LOGTAG,"not waiting for service because already initialized");
}
}
public static Uri getUriForAccount(Context context, Account account) {
final String packageId = context.getPackageName();
return Uri.parse("content://" + packageId + AUTHORITY + "/" + account.getJid().toBareJid() + ".png");
}
public static Bitmap createAztecBitmap(String input, int size) {
try {
final AztecWriter AZTEC_WRITER = new AztecWriter();
final Hashtable<EncodeHintType, Object> hints = new Hashtable<>();
hints.put(EncodeHintType.ERROR_CORRECTION, 10);
final BitMatrix result = AZTEC_WRITER.encode(input, BarcodeFormat.AZTEC, size, size, hints);
final int width = result.getWidth();
final int height = result.getHeight();
final int[] pixels = new int[width * height];
for (int y = 0; y < height; y++) {
final int offset = y * width;
for (int x = 0; x < width; x++) {
pixels[offset + x] = result.get(x, y) ? Color.BLACK : Color.WHITE;
}
}
final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
return bitmap;
} catch (final Exception e) {
return null;
}
}
}

View file

@ -19,6 +19,9 @@ public class EventReceiver extends BroadcastReceiver {
mIntentForService.setAction("other"); mIntentForService.setAction("other");
} }
final String action = intent.getAction(); final String action = intent.getAction();
if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION) && Config.PUSH_MODE) {
return;
}
if (action.equals("ui") || DatabaseBackend.getInstance(context).hasEnabledAccounts()) { if (action.equals("ui") || DatabaseBackend.getInstance(context).hasEnabledAccounts()) {
context.startService(mIntentForService); context.startService(mIntentForService);
} }

View file

@ -26,7 +26,7 @@ import eu.siacs.conversations.xmpp.jid.Jid;
public class ExportLogsService extends Service { public class ExportLogsService extends Service {
private static final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); private static final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
private static final String DIRECTORY_STRING_FORMAT = FileBackend.getConversationsLogsDirectory() + "/logs/%s"; private static final String DIRECTORY_STRING_FORMAT = FileBackend.getConversationsFileDirectory() + "/logs/%s";
private static final String MESSAGE_STRING_FORMAT = "(%s) %s: %s\n"; private static final String MESSAGE_STRING_FORMAT = "(%s) %s: %s\n";
private static final int NOTIFICATION_ID = 1; private static final int NOTIFICATION_ID = 1;
private static AtomicBoolean running = new AtomicBoolean(false); private static AtomicBoolean running = new AtomicBoolean(false);

View file

@ -56,7 +56,6 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
startCatchup = lastClearDate.first; startCatchup = lastClearDate.first;
reference = null; reference = null;
} }
startCatchup = Math.max(startCatchup,mXmppConnectionService.getAutomaticMessageDeletionDate());
long endCatchup = account.getXmppConnection().getLastSessionEstablished(); long endCatchup = account.getXmppConnection().getLastSessionEstablished();
final Query query; final Query query;
if (startCatchup == 0) { if (startCatchup == 0) {
@ -108,15 +107,11 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
public Query query(Conversation conversation, long start, long end) { public Query query(Conversation conversation, long start, long end) {
synchronized (this.queries) { synchronized (this.queries) {
final Query query = new Query(conversation, start, end,PagingOrder.REVERSE);
if (start==0) {
query.reference = conversation.getFirstMamReference();
Log.d(Config.LOGTAG,"setting mam reference");
}
query.start = Math.max(start,mXmppConnectionService.getAutomaticMessageDeletionDate());
if (start > end) { if (start > end) {
return null; return null;
} }
final Query query = new Query(conversation, start, end,PagingOrder.REVERSE);
query.reference = conversation.getFirstMamReference();
this.queries.add(query); this.queries.add(query);
this.execute(query); this.execute(query);
return query; return query;
@ -225,11 +220,11 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
query.getConversation().setFirstMamReference(first == null ? null : first.getContent()); query.getConversation().setFirstMamReference(first == null ? null : first.getContent());
} }
if (complete || relevant == null || abort) { if (complete || relevant == null || abort) {
final boolean done = (complete || query.getMessageCount() == 0) && query.getStart() <= mXmppConnectionService.getAutomaticMessageDeletionDate(); final boolean done = (complete || query.getMessageCount() == 0) && query.getStart() == 0;
this.finalizeQuery(query, done); this.finalizeQuery(query, done);
Log.d(Config.LOGTAG,query.getAccount().getJid().toBareJid()+": finished mam after "+query.getTotalCount()+" messages. messages left="+Boolean.toString(!done)); Log.d(Config.LOGTAG,query.getAccount().getJid().toBareJid()+": finished mam after "+query.getTotalCount()+" messages. messages left="+Boolean.toString(!done));
if (query.getWith() == null && query.getMessageCount() > 0) { if (query.getWith() == null && query.getMessageCount() > 0) {
mXmppConnectionService.getNotificationService().finishBacklog(true,query.getAccount()); mXmppConnectionService.getNotificationService().finishBacklog(true);
} }
} else { } else {
final Query nextQuery; final Query nextQuery;

View file

@ -34,7 +34,6 @@ import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.ui.ConversationActivity; import eu.siacs.conversations.ui.ConversationActivity;
import eu.siacs.conversations.ui.ManageAccountActivity; import eu.siacs.conversations.ui.ManageAccountActivity;
import eu.siacs.conversations.ui.SettingsActivity;
import eu.siacs.conversations.ui.TimePreference; import eu.siacs.conversations.ui.TimePreference;
import eu.siacs.conversations.utils.GeoHelper; import eu.siacs.conversations.utils.GeoHelper;
import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.utils.UIHelper;
@ -100,28 +99,13 @@ public class NotificationService {
} }
} }
public void finishBacklog(boolean notify, Account account) { public void finishBacklog(boolean notify) {
synchronized (notifications) { synchronized (notifications) {
mXmppConnectionService.updateUnreadCountBadge(); mXmppConnectionService.updateUnreadCountBadge();
if (account == null || !notify) { updateNotification(notify);
updateNotification(notify);
} else {
boolean hasPendingMessages = false;
for(ArrayList<Message> messages : notifications.values()) {
if (messages.size() > 0 && messages.get(0).getConversation().getAccount() == account) {
hasPendingMessages = true;
break;
}
}
updateNotification(hasPendingMessages);
}
} }
} }
public void finishBacklog(boolean notify) {
finishBacklog(notify,null);
}
private void pushToStack(final Message message) { private void pushToStack(final Message message) {
final String conversationUuid = message.getConversationUuid(); final String conversationUuid = message.getConversationUuid();
if (notifications.containsKey(conversationUuid)) { if (notifications.containsKey(conversationUuid)) {
@ -522,7 +506,7 @@ public class NotificationService {
return (m.find() || message.getType() == Message.TYPE_PRIVATE); return (m.find() || message.getType() == Message.TYPE_PRIVATE);
} }
public static Pattern generateNickHighlightPattern(final String nick) { private static Pattern generateNickHighlightPattern(final String nick) {
// We expect a word boundary, i.e. space or start of string, followed by // We expect a word boundary, i.e. space or start of string, followed by
// the // the
// nick (matched in case-insensitive manner), followed by optional // nick (matched in case-insensitive manner), followed by optional
@ -607,7 +591,7 @@ public class NotificationService {
errors.add(account); errors.add(account);
} }
} }
if (mXmppConnectionService.getPreferences().getBoolean(SettingsActivity.KEEP_FOREGROUND_SERVICE, false)) { if (mXmppConnectionService.getPreferences().getBoolean("keep_foreground_service", false)) {
notificationManager.notify(FOREGROUND_NOTIFICATION_ID, createForegroundNotification()); notificationManager.notify(FOREGROUND_NOTIFICATION_ID, createForegroundNotification());
} }
final NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService); final NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService);

View file

@ -20,7 +20,6 @@ import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Environment; import android.os.Environment;
import android.os.IBinder; import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.PowerManager; import android.os.PowerManager;
import android.os.PowerManager.WakeLock; import android.os.PowerManager.WakeLock;
import android.os.SystemClock; import android.os.SystemClock;
@ -38,15 +37,11 @@ import net.java.otr4j.session.Session;
import net.java.otr4j.session.SessionID; import net.java.otr4j.session.SessionID;
import net.java.otr4j.session.SessionImpl; import net.java.otr4j.session.SessionImpl;
import net.java.otr4j.session.SessionStatus; import net.java.otr4j.session.SessionStatus;
import net.ypresto.androidtranscoder.MediaTranscoder;
import net.ypresto.androidtranscoder.format.MediaFormatStrategyPresets;
import org.openintents.openpgp.IOpenPgpService2; import org.openintents.openpgp.IOpenPgpService2;
import org.openintents.openpgp.util.OpenPgpApi; import org.openintents.openpgp.util.OpenPgpApi;
import org.openintents.openpgp.util.OpenPgpServiceConnection; import org.openintents.openpgp.util.OpenPgpServiceConnection;
import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.math.BigInteger; import java.math.BigInteger;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.security.cert.CertificateException; import java.security.cert.CertificateException;
@ -60,12 +55,9 @@ import java.util.HashSet;
import java.util.Hashtable; import java.util.Hashtable;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.ListIterator;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicLong;
import de.duenndns.ssl.MemorizingTrustManager; import de.duenndns.ssl.MemorizingTrustManager;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
@ -73,7 +65,6 @@ import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.PgpDecryptionService; import eu.siacs.conversations.crypto.PgpDecryptionService;
import eu.siacs.conversations.crypto.PgpEngine; import eu.siacs.conversations.crypto.PgpEngine;
import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage; import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Blockable; import eu.siacs.conversations.entities.Blockable;
@ -101,20 +92,16 @@ import eu.siacs.conversations.parser.MessageParser;
import eu.siacs.conversations.parser.PresenceParser; import eu.siacs.conversations.parser.PresenceParser;
import eu.siacs.conversations.persistance.DatabaseBackend; import eu.siacs.conversations.persistance.DatabaseBackend;
import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.ui.SettingsActivity;
import eu.siacs.conversations.ui.UiCallback; import eu.siacs.conversations.ui.UiCallback;
import eu.siacs.conversations.ui.UiInformableCallback;
import eu.siacs.conversations.utils.ConversationsFileObserver; import eu.siacs.conversations.utils.ConversationsFileObserver;
import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.ExceptionHelper; import eu.siacs.conversations.utils.ExceptionHelper;
import eu.siacs.conversations.utils.MimeUtils;
import eu.siacs.conversations.utils.OnPhoneContactsLoadedListener; import eu.siacs.conversations.utils.OnPhoneContactsLoadedListener;
import eu.siacs.conversations.utils.PRNGFixes; import eu.siacs.conversations.utils.PRNGFixes;
import eu.siacs.conversations.utils.PhoneHelper; import eu.siacs.conversations.utils.PhoneHelper;
import eu.siacs.conversations.utils.ReplacingSerialSingleThreadExecutor; import eu.siacs.conversations.utils.ReplacingSerialSingleThreadExecutor;
import eu.siacs.conversations.utils.SerialSingleThreadExecutor; import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
import eu.siacs.conversations.utils.Xmlns; import eu.siacs.conversations.utils.Xmlns;
import eu.siacs.conversations.utils.XmppUri;
import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.OnBindListener; import eu.siacs.conversations.xmpp.OnBindListener;
import eu.siacs.conversations.xmpp.OnContactStatusChanged; import eu.siacs.conversations.xmpp.OnContactStatusChanged;
@ -273,7 +260,6 @@ public class XmppConnectionService extends Service {
private int mucRosterChangedListenerCount = 0; private int mucRosterChangedListenerCount = 0;
private OnKeyStatusUpdated mOnKeyStatusUpdated = null; private OnKeyStatusUpdated mOnKeyStatusUpdated = null;
private int keyStatusUpdatedListenerCount = 0; private int keyStatusUpdatedListenerCount = 0;
private AtomicLong mLastExpiryRun = new AtomicLong(0);
private SecureRandom mRandom; private SecureRandom mRandom;
private LruCache<Pair<String,String>,ServiceDiscoveryResult> discoCache = new LruCache<>(20); private LruCache<Pair<String,String>,ServiceDiscoveryResult> discoCache = new LruCache<>(20);
private final OnBindListener mOnBindListener = new OnBindListener() { private final OnBindListener mOnBindListener = new OnBindListener() {
@ -309,11 +295,6 @@ public class XmppConnectionService extends Service {
mOnAccountUpdate.onAccountUpdate(); mOnAccountUpdate.onAccountUpdate();
} }
if (account.getStatus() == Account.State.ONLINE) { if (account.getStatus() == Account.State.ONLINE) {
synchronized (mLowPingTimeoutMode) {
if (mLowPingTimeoutMode.remove(account.getJid().toBareJid())) {
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": leaving low ping timeout mode");
}
}
if (account.setShowErrorNotification(true)) { if (account.setShowErrorNotification(true)) {
databaseBackend.updateAccount(account); databaseBackend.updateAccount(account);
} }
@ -345,36 +326,38 @@ public class XmppConnectionService extends Service {
joinMuc(conversation); joinMuc(conversation);
} }
account.pendingConferenceJoins.clear(); account.pendingConferenceJoins.clear();
scheduleWakeUpCall(Config.PING_MAX_INTERVAL, account.getUuid().hashCode()); scheduleWakeUpCall(Config.PUSH_MODE ? Config.PING_MIN_INTERVAL : Config.PING_MAX_INTERVAL, account.getUuid().hashCode());
} else { } else if (account.getStatus() == Account.State.OFFLINE || account.getStatus() == Account.State.DISABLED) {
if (account.getStatus() == Account.State.OFFLINE || account.getStatus() == Account.State.DISABLED) { resetSendingToWaiting(account);
resetSendingToWaiting(account); final boolean disabled = account.isOptionSet(Account.OPTION_DISABLED);
if (!account.isOptionSet(Account.OPTION_DISABLED)) { final boolean listeners = checkListeners();
synchronized (mLowPingTimeoutMode) { final boolean pushMode = Config.PUSH_MODE
if (mLowPingTimeoutMode.contains(account.getJid().toBareJid())) { && mPushManagementService.available(account)
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": went into offline state during low ping mode. reconnecting now"); && listeners;
reconnectAccount(account, true, false); Log.d(Config.LOGTAG,account.getJid().toBareJid()+": push mode="+Boolean.toString(pushMode)+" listeners="+Boolean.toString(listeners));
} else { if (!disabled && !pushMode) {
int timeToReconnect = mRandom.nextInt(10) + 2; if (mLowPingTimeoutMode.contains(account.getJid().toBareJid())) {
scheduleWakeUpCall(timeToReconnect, account.getUuid().hashCode()); Log.d(Config.LOGTAG,account.getJid().toBareJid()+": went into offline state during low ping mode. reconnecting now");
} reconnectAccount(account,true,false);
} } else {
} int timeToReconnect = mRandom.nextInt(20) + 10;
} else if (account.getStatus() == Account.State.REGISTRATION_SUCCESSFUL) { scheduleWakeUpCall(timeToReconnect, account.getUuid().hashCode());
databaseBackend.updateAccount(account);
reconnectAccount(account, true, false);
} else if ((account.getStatus() != Account.State.CONNECTING)
&& (account.getStatus() != Account.State.NO_INTERNET)) {
resetSendingToWaiting(account);
if (connection != null) {
int next = connection.getTimeToNextAttempt();
Log.d(Config.LOGTAG, account.getJid().toBareJid()
+ ": error connecting account. try again in "
+ next + "s for the "
+ (connection.getAttempt() + 1) + " time");
scheduleWakeUpCall(next, account.getUuid().hashCode());
} }
} }
} else if (account.getStatus() == Account.State.REGISTRATION_SUCCESSFUL) {
databaseBackend.updateAccount(account);
reconnectAccount(account, true, false);
} else if ((account.getStatus() != Account.State.CONNECTING)
&& (account.getStatus() != Account.State.NO_INTERNET)) {
resetSendingToWaiting(account);
if (connection != null) {
int next = connection.getTimeToNextAttempt();
Log.d(Config.LOGTAG, account.getJid().toBareJid()
+ ": error connecting account. try again in "
+ next + "s for the "
+ (connection.getAttempt() + 1) + " time");
scheduleWakeUpCall(next, account.getUuid().hashCode());
}
} }
getNotificationService().updateErrorNotification(); getNotificationService().updateErrorNotification();
} }
@ -464,10 +447,10 @@ public class XmppConnectionService extends Service {
} }
message.setCounterpart(conversation.getNextCounterpart()); message.setCounterpart(conversation.getNextCounterpart());
message.setType(Message.TYPE_FILE); message.setType(Message.TYPE_FILE);
final String path = getFileBackend().getOriginalPath(uri);
mFileAddingExecutor.execute(new Runnable() { mFileAddingExecutor.execute(new Runnable() {
@Override
private void processAsFile() { public void run() {
final String path = getFileBackend().getOriginalPath(uri);
if (path != null) { if (path != null) {
message.setRelativeFilePath(path); message.setRelativeFilePath(path);
getFileBackend().updateFileParams(message); getFileBackend().updateFileParams(message);
@ -495,72 +478,6 @@ public class XmppConnectionService extends Service {
} }
} }
} }
private void processAsVideo() throws FileNotFoundException {
Log.d(Config.LOGTAG,"processing file as video");
message.setRelativeFilePath(message.getUuid() + ".mp4");
final DownloadableFile file = getFileBackend().getFile(message);
file.getParentFile().mkdirs();
ParcelFileDescriptor parcelFileDescriptor = getContentResolver().openFileDescriptor(uri, "r");
FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
final ArrayList<Integer> progressTracker = new ArrayList<>();
final UiInformableCallback<Message> informableCallback;
if (callback instanceof UiInformableCallback) {
informableCallback = (UiInformableCallback<Message>) callback;
} else {
informableCallback = null;
}
MediaTranscoder.Listener listener = new MediaTranscoder.Listener() {
@Override
public void onTranscodeProgress(double progress) {
int p = ((int) Math.round(progress * 100) / 20) * 20;
if (!progressTracker.contains(p) && p != 100 && p != 0) {
progressTracker.add(p);
if (informableCallback != null) {
informableCallback.inform(getString(R.string.transcoding_video_progress, p));
}
}
}
@Override
public void onTranscodeCompleted() {
if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
getPgpEngine().encrypt(message, callback);
} else {
callback.success(message);
}
}
@Override
public void onTranscodeCanceled() {
processAsFile();
}
@Override
public void onTranscodeFailed(Exception e) {
Log.d(Config.LOGTAG,"video transcoding failed "+e.getMessage());
processAsFile();
}
};
MediaTranscoder.getInstance().transcodeVideo(fileDescriptor, file.getAbsolutePath(),
MediaFormatStrategyPresets.createAndroid720pStrategy(), listener);
}
@Override
public void run() {
final String mimeType = MimeUtils.guessMimeTypeFromUri(XmppConnectionService.this, uri);
if (mimeType != null && mimeType.startsWith("video/") && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
try {
processAsVideo();
} catch (Throwable e) {
processAsFile();
}
} else {
processAsFile();
}
}
}); });
} }
@ -570,13 +487,9 @@ public class XmppConnectionService extends Service {
callback.error(R.string.security_error_invalid_file_access, null); callback.error(R.string.security_error_invalid_file_access, null);
return; return;
} }
final String mimeType = MimeUtils.guessMimeTypeFromUri(this, uri);
final String compressPictures = getCompressPicturesPreference(); final String compressPictures = getCompressPicturesPreference();
if ("never".equals(compressPictures) if ("never".equals(compressPictures)
|| ("auto".equals(compressPictures) && getFileBackend().useImageAsIs(uri)) || ("auto".equals(compressPictures) && getFileBackend().useImageAsIs(uri))) {
|| (mimeType != null && mimeType.endsWith("/gif"))) {
Log.d(Config.LOGTAG,conversation.getAccount().getJid().toBareJid()+ ": not compressing picture. sending as file"); Log.d(Config.LOGTAG,conversation.getAccount().getJid().toBareJid()+ ": not compressing picture. sending as file");
attachFileToConversation(conversation, uri, callback); attachFileToConversation(conversation, uri, callback);
return; return;
@ -630,7 +543,7 @@ public class XmppConnectionService extends Service {
switch (action) { switch (action) {
case ConnectivityManager.CONNECTIVITY_ACTION: case ConnectivityManager.CONNECTIVITY_ACTION:
if (hasInternetConnection() && Config.RESET_ATTEMPT_COUNT_ON_NETWORK_CHANGE) { if (hasInternetConnection() && Config.RESET_ATTEMPT_COUNT_ON_NETWORK_CHANGE) {
resetAllAttemptCounts(true, false); resetAllAttemptCounts(true);
} }
break; break;
case ACTION_MERGE_PHONE_CONTACTS: case ACTION_MERGE_PHONE_CONTACTS:
@ -649,14 +562,14 @@ public class XmppConnectionService extends Service {
} }
break; break;
case ACTION_DISABLE_FOREGROUND: case ACTION_DISABLE_FOREGROUND:
getPreferences().edit().putBoolean(SettingsActivity.KEEP_FOREGROUND_SERVICE, false).commit(); getPreferences().edit().putBoolean("keep_foreground_service", false).commit();
toggleForegroundService(); toggleForegroundService();
break; break;
case ACTION_DISMISS_ERROR_NOTIFICATIONS: case ACTION_DISMISS_ERROR_NOTIFICATIONS:
dismissErrorNotifications(); dismissErrorNotifications();
break; break;
case ACTION_TRY_AGAIN: case ACTION_TRY_AGAIN:
resetAllAttemptCounts(false, true); resetAllAttemptCounts(false);
interactive = true; interactive = true;
break; break;
case ACTION_REPLY_TO_CONVERSATION: case ACTION_REPLY_TO_CONVERSATION:
@ -684,7 +597,8 @@ public class XmppConnectionService extends Service {
refreshAllGcmTokens(); refreshAllGcmTokens();
break; break;
case ACTION_IDLE_PING: case ACTION_IDLE_PING:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& !Config.PUSH_MODE) {
scheduleNextIdlePing(); scheduleNextIdlePing();
} }
break; break;
@ -694,58 +608,29 @@ public class XmppConnectionService extends Service {
break; break;
} }
} }
synchronized (this) { this.wakeLock.acquire();
this.wakeLock.acquire();
boolean pingNow = ConnectivityManager.CONNECTIVITY_ACTION.equals(action);
HashSet<Account> pingCandidates = new HashSet<>();
for (Account account : accounts) {
pingNow |= processAccountState(account,
interactive,
"ui".equals(action),
CryptoHelper.getAccountFingerprint(account).equals(pushedAccountHash),
pingCandidates);
}
if (pingNow) {
for (Account account : pingCandidates) {
final boolean lowTimeout = mLowPingTimeoutMode.contains(account.getJid().toBareJid());
account.getXmppConnection().sendPing();
Log.d(Config.LOGTAG, account.getJid().toBareJid() + " send ping (action=" + action + ",lowTimeout=" + Boolean.toString(lowTimeout) + ")");
scheduleWakeUpCall(lowTimeout ? Config.LOW_PING_TIMEOUT : Config.PING_TIMEOUT, account.getUuid().hashCode());
}
}
if (wakeLock.isHeld()) {
try {
wakeLock.release();
} catch (final RuntimeException ignored) {
}
}
}
if (SystemClock.elapsedRealtime() - mLastExpiryRun.get() >= Config.EXPIRY_INTERVAL) {
expireOldMessages();
}
return START_STICKY;
}
private boolean processAccountState(Account account, boolean interactive, boolean isUiAction, boolean isAccountPushed, HashSet<Account> pingCandidates) {
boolean pingNow = false; boolean pingNow = false;
if (!account.isOptionSet(Account.OPTION_DISABLED)) { HashSet<Account> pingCandidates = new HashSet<>();
if (!hasInternetConnection()) {
account.setStatus(Account.State.NO_INTERNET); for (Account account : accounts) {
if (statusListener != null) { if (!account.isOptionSet(Account.OPTION_DISABLED)) {
statusListener.onStatusChanged(account); if (!hasInternetConnection()) {
} account.setStatus(Account.State.NO_INTERNET);
} else {
if (account.getStatus() == Account.State.NO_INTERNET) {
account.setStatus(Account.State.OFFLINE);
if (statusListener != null) { if (statusListener != null) {
statusListener.onStatusChanged(account); statusListener.onStatusChanged(account);
} }
} } else {
if (account.getStatus() == Account.State.ONLINE) { if (account.getStatus() == Account.State.NO_INTERNET) {
synchronized (mLowPingTimeoutMode) { account.setStatus(Account.State.OFFLINE);
if (statusListener != null) {
statusListener.onStatusChanged(account);
}
}
if (account.getStatus() == Account.State.ONLINE) {
long lastReceived = account.getXmppConnection().getLastPacketReceived(); long lastReceived = account.getXmppConnection().getLastPacketReceived();
long lastSent = account.getXmppConnection().getLastPingSent(); long lastSent = account.getXmppConnection().getLastPingSent();
long pingInterval = isUiAction ? Config.PING_MIN_INTERVAL * 1000 : Config.PING_MAX_INTERVAL * 1000; long pingInterval = (Config.PUSH_MODE || "ui".equals(action)) ? Config.PING_MIN_INTERVAL * 1000 : Config.PING_MAX_INTERVAL * 1000;
long msToNextPing = (Math.max(lastReceived, lastSent) + pingInterval) - SystemClock.elapsedRealtime(); long msToNextPing = (Math.max(lastReceived, lastSent) + pingInterval) - SystemClock.elapsedRealtime();
int pingTimeout = mLowPingTimeoutMode.contains(account.getJid().toBareJid()) ? Config.LOW_PING_TIMEOUT * 1000 : Config.PING_TIMEOUT * 1000; int pingTimeout = mLowPingTimeoutMode.contains(account.getJid().toBareJid()) ? Config.LOW_PING_TIMEOUT * 1000 : Config.PING_TIMEOUT * 1000;
long pingTimeoutIn = (lastSent + pingTimeout) - SystemClock.elapsedRealtime(); long pingTimeoutIn = (lastSent + pingTimeout) - SystemClock.elapsedRealtime();
@ -759,7 +644,7 @@ public class XmppConnectionService extends Service {
} }
} else { } else {
pingCandidates.add(account); pingCandidates.add(account);
if (isAccountPushed) { if (CryptoHelper.getAccountFingerprint(account).equals(pushedAccountHash)) {
pingNow = true; pingNow = true;
if (mLowPingTimeoutMode.add(account.getJid().toBareJid())) { if (mLowPingTimeoutMode.add(account.getJid().toBareJid())) {
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": entering low ping timeout mode"); Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": entering low ping timeout mode");
@ -769,36 +654,60 @@ public class XmppConnectionService extends Service {
} else { } else {
this.scheduleWakeUpCall((int) (msToNextPing / 1000), account.getUuid().hashCode()); this.scheduleWakeUpCall((int) (msToNextPing / 1000), account.getUuid().hashCode());
if (mLowPingTimeoutMode.remove(account.getJid().toBareJid())) { if (mLowPingTimeoutMode.remove(account.getJid().toBareJid())) {
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": leaving low ping timeout mode"); Log.d(Config.LOGTAG,account.getJid().toBareJid()+": leaving low ping timeout mode");
} }
} }
} }
} } else if (account.getStatus() == Account.State.OFFLINE) {
} else if (account.getStatus() == Account.State.OFFLINE) {
reconnectAccount(account, true, interactive);
} else if (account.getStatus() == Account.State.CONNECTING) {
long secondsSinceLastConnect = (SystemClock.elapsedRealtime() - account.getXmppConnection().getLastConnect()) / 1000;
long secondsSinceLastDisco = (SystemClock.elapsedRealtime() - account.getXmppConnection().getLastDiscoStarted()) / 1000;
long discoTimeout = Config.CONNECT_DISCO_TIMEOUT - secondsSinceLastDisco;
long timeout = Config.CONNECT_TIMEOUT - secondsSinceLastConnect;
if (timeout < 0) {
Log.d(Config.LOGTAG, account.getJid() + ": time out during connect reconnecting (secondsSinceLast="+secondsSinceLastConnect+")");
account.getXmppConnection().resetAttemptCount(false);
reconnectAccount(account, true, interactive); reconnectAccount(account, true, interactive);
} else if (discoTimeout < 0) { } else if (account.getStatus() == Account.State.CONNECTING) {
account.getXmppConnection().sendDiscoTimeout(); long secondsSinceLastConnect = (SystemClock.elapsedRealtime() - account.getXmppConnection().getLastConnect()) / 1000;
scheduleWakeUpCall((int) Math.min(timeout,discoTimeout), account.getUuid().hashCode()); long secondsSinceLastDisco = (SystemClock.elapsedRealtime() - account.getXmppConnection().getLastDiscoStarted()) / 1000;
long discoTimeout = Config.CONNECT_DISCO_TIMEOUT - secondsSinceLastDisco;
long timeout = Config.CONNECT_TIMEOUT - secondsSinceLastConnect;
if (timeout < 0) {
Log.d(Config.LOGTAG, account.getJid() + ": time out during connect reconnecting");
account.getXmppConnection().resetAttemptCount();
reconnectAccount(account, true, interactive);
} else if (discoTimeout < 0) {
account.getXmppConnection().sendDiscoTimeout();
scheduleWakeUpCall((int) Math.min(timeout,discoTimeout), account.getUuid().hashCode());
} else {
scheduleWakeUpCall((int) Math.min(timeout,discoTimeout), account.getUuid().hashCode());
}
} else { } else {
scheduleWakeUpCall((int) Math.min(timeout,discoTimeout), account.getUuid().hashCode()); if (account.getXmppConnection().getTimeToNextAttempt() <= 0) {
} reconnectAccount(account, true, interactive);
} else { }
if (account.getXmppConnection().getTimeToNextAttempt() <= 0) {
reconnectAccount(account, true, interactive);
} }
} }
if (mOnAccountUpdate != null) {
mOnAccountUpdate.onAccountUpdate();
}
} }
} }
return pingNow; if (pingNow) {
final boolean listeners = checkListeners();
for (Account account : pingCandidates) {
if (listeners
&& Config.PUSH_MODE
&& mPushManagementService.available(account)) {
account.getXmppConnection().waitForPush();
cancelWakeUpCall(account.getUuid().hashCode());
} else {
account.getXmppConnection().sendPing();
Log.d(Config.LOGTAG, account.getJid().toBareJid() + " send ping (action=" + action + ",listeners="+Boolean.toString(listeners)+")");
scheduleWakeUpCall(Config.PING_TIMEOUT, account.getUuid().hashCode());
}
}
}
if (wakeLock.isHeld()) {
try {
wakeLock.release();
} catch (final RuntimeException ignored) {
}
}
return START_STICKY;
} }
public boolean isDataSaverDisabled() { public boolean isDataSaverDisabled() {
@ -852,15 +761,15 @@ public class XmppConnectionService extends Service {
} }
private boolean manuallyChangePresence() { private boolean manuallyChangePresence() {
return getPreferences().getBoolean(SettingsActivity.MANUALLY_CHANGE_PRESENCE, false); return getPreferences().getBoolean("manually_change_presence", false);
} }
private boolean treatVibrateAsSilent() { private boolean treatVibrateAsSilent() {
return getPreferences().getBoolean(SettingsActivity.TREAT_VIBRATE_AS_SILENT, false); return getPreferences().getBoolean("treat_vibrate_as_silent", false);
} }
private boolean awayWhenScreenOff() { private boolean awayWhenScreenOff() {
return getPreferences().getBoolean(SettingsActivity.AWAY_WHEN_SCREEN_IS_OFF, false); return getPreferences().getBoolean("away_when_screen_off", false);
} }
private String getCompressPicturesPreference() { private String getCompressPicturesPreference() {
@ -905,13 +814,13 @@ public class XmppConnectionService extends Service {
} }
} }
private void resetAllAttemptCounts(boolean reallyAll, boolean retryImmediately) { private void resetAllAttemptCounts(boolean reallyAll) {
Log.d(Config.LOGTAG, "resetting all attempt counts"); Log.d(Config.LOGTAG, "resetting all attempt counts");
for (Account account : accounts) { for (Account account : accounts) {
if (account.hasErrorStatus() || reallyAll) { if (account.hasErrorStatus() || reallyAll) {
final XmppConnection connection = account.getXmppConnection(); final XmppConnection connection = account.getXmppConnection();
if (connection != null) { if (connection != null) {
connection.resetAttemptCount(retryImmediately); connection.resetAttemptCount();
} }
} }
if (account.setShowErrorNotification(true)) { if (account.setShowErrorNotification(true)) {
@ -932,33 +841,6 @@ public class XmppConnectionService extends Service {
} }
} }
private void expireOldMessages() {
expireOldMessages(false);
}
public void expireOldMessages(final boolean resetHasMessagesLeftOnServer) {
mLastExpiryRun.set(SystemClock.elapsedRealtime());
mDatabaseExecutor.execute(new Runnable() {
@Override
public void run() {
long timestamp = getAutomaticMessageDeletionDate();
if (timestamp > 0) {
databaseBackend.expireOldMessages(timestamp);
synchronized (XmppConnectionService.this.conversations) {
for (Conversation conversation : XmppConnectionService.this.conversations) {
conversation.expireOldMessages(timestamp);
if (resetHasMessagesLeftOnServer) {
conversation.messagesLoaded.set(true);
conversation.setHasMessagesLeftOnServer(true);
}
}
}
updateConversationUi();
}
}
});
}
public boolean hasInternetConnection() { public boolean hasInternetConnection() {
ConnectivityManager cm = (ConnectivityManager) getApplicationContext() ConnectivityManager cm = (ConnectivityManager) getApplicationContext()
.getSystemService(Context.CONNECTIVITY_SERVICE); .getSystemService(Context.CONNECTIVITY_SERVICE);
@ -985,14 +867,6 @@ public class XmppConnectionService extends Service {
this.databaseBackend = DatabaseBackend.getInstance(getApplicationContext()); this.databaseBackend = DatabaseBackend.getInstance(getApplicationContext());
this.accounts = databaseBackend.getAccounts(); this.accounts = databaseBackend.getAccounts();
if (Config.FREQUENT_RESTARTS_THRESHOLD != 0
&& Config.FREQUENT_RESTARTS_DETECTION_WINDOW != 0
&& !keepForegroundService()
&& databaseBackend.startTimeCountExceedsThreshold()) {
getPreferences().edit().putBoolean(SettingsActivity.KEEP_FOREGROUND_SERVICE,true).commit();
Log.d(Config.LOGTAG,"number of restarts exceeds threshold. enabling foreground service");
}
restoreFromDatabase(); restoreFromDatabase();
getContentResolver().registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, contactObserver); getContentResolver().registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, contactObserver);
@ -1023,11 +897,10 @@ public class XmppConnectionService extends Service {
this.pm = (PowerManager) getSystemService(Context.POWER_SERVICE); this.pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "XmppConnectionService"); this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "XmppConnectionService");
toggleForegroundService(); toggleForegroundService();
updateUnreadCountBadge(); updateUnreadCountBadge();
toggleScreenEventReceiver(); toggleScreenEventReceiver();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Config.PUSH_MODE) {
scheduleNextIdlePing(); scheduleNextIdlePing();
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
@ -1070,21 +943,17 @@ public class XmppConnectionService extends Service {
} }
public void toggleForegroundService() { public void toggleForegroundService() {
if (keepForegroundService()) { if (getPreferences().getBoolean("keep_foreground_service", false)) {
startForeground(NotificationService.FOREGROUND_NOTIFICATION_ID, this.mNotificationService.createForegroundNotification()); startForeground(NotificationService.FOREGROUND_NOTIFICATION_ID, this.mNotificationService.createForegroundNotification());
} else { } else {
stopForeground(true); stopForeground(true);
} }
} }
private boolean keepForegroundService() {
return getPreferences().getBoolean(SettingsActivity.KEEP_FOREGROUND_SERVICE,false);
}
@Override @Override
public void onTaskRemoved(final Intent rootIntent) { public void onTaskRemoved(final Intent rootIntent) {
super.onTaskRemoved(rootIntent); super.onTaskRemoved(rootIntent);
if (!keepForegroundService()) { if (!getPreferences().getBoolean("keep_foreground_service", false)) {
this.logoutAndSave(false); this.logoutAndSave(false);
} else { } else {
Log.d(Config.LOGTAG,"ignoring onTaskRemoved because foreground service is activated"); Log.d(Config.LOGTAG,"ignoring onTaskRemoved because foreground service is activated");
@ -1093,7 +962,6 @@ public class XmppConnectionService extends Service {
private void logoutAndSave(boolean stop) { private void logoutAndSave(boolean stop) {
int activeAccounts = 0; int activeAccounts = 0;
databaseBackend.clearStartTimeCounter(true); // regular swipes don't count towards restart counter
for (final Account account : accounts) { for (final Account account : accounts) {
if (account.getStatus() != Account.State.DISABLED) { if (account.getStatus() != Account.State.DISABLED) {
activeAccounts++; activeAccounts++;
@ -1114,6 +982,13 @@ public class XmppConnectionService extends Service {
} }
} }
private void cancelWakeUpCall(int requestCode) {
final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
final Intent intent = new Intent(this, EventReceiver.class);
intent.setAction("ping");
alarmManager.cancel(PendingIntent.getBroadcast(this, requestCode, intent, 0));
}
public void scheduleWakeUpCall(int seconds, int requestCode) { public void scheduleWakeUpCall(int seconds, int requestCode) {
final long timeToWake = SystemClock.elapsedRealtime() + (seconds < 0 ? 1 : seconds + 1) * 1000; final long timeToWake = SystemClock.elapsedRealtime() + (seconds < 0 ? 1 : seconds + 1) * 1000;
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
@ -1337,10 +1212,12 @@ public class XmppConnectionService extends Service {
if (addToConversation) { if (addToConversation) {
conversation.add(message); conversation.add(message);
} }
if (saveInDb) { if (message.getEncryption() == Message.ENCRYPTION_NONE || saveEncryptedMessages()) {
databaseBackend.createMessage(message); if (saveInDb) {
} else if (message.edited()) { databaseBackend.createMessage(message);
databaseBackend.updateMessage(message, message.getEditedId()); } else if (message.edited()) {
databaseBackend.updateMessage(message, message.getEditedId());
}
} }
updateConversationUi(); updateConversationUi();
} }
@ -1450,12 +1327,6 @@ public class XmppConnectionService extends Service {
Runnable runnable = new Runnable() { Runnable runnable = new Runnable() {
@Override @Override
public void run() { public void run() {
long deletionDate = getAutomaticMessageDeletionDate();
mLastExpiryRun.set(SystemClock.elapsedRealtime());
if (deletionDate > 0) {
Log.d(Config.LOGTAG, "deleting messages that are older than "+AbstractGenerator.getTimestamp(deletionDate));
databaseBackend.expireOldMessages(deletionDate);
}
Log.d(Config.LOGTAG, "restoring roster"); Log.d(Config.LOGTAG, "restoring roster");
for (Account account : accounts) { for (Account account : accounts) {
databaseBackend.readRoster(account.getRoster()); databaseBackend.readRoster(account.getRoster());
@ -1627,11 +1498,8 @@ public class XmppConnectionService extends Service {
MessageArchiveService.Query query = getMessageArchiveService().query(conversation, 0, timestamp); MessageArchiveService.Query query = getMessageArchiveService().query(conversation, 0, timestamp);
if (query != null) { if (query != null) {
query.setCallback(callback); query.setCallback(callback);
callback.informUser(R.string.fetching_history_from_server);
} else {
callback.informUser(R.string.not_fetching_history_retention_period);
} }
callback.informUser(R.string.fetching_history_from_server);
} }
} }
} }
@ -1787,7 +1655,7 @@ public class XmppConnectionService extends Service {
callback.onAccountCreated(account); callback.onAccountCreated(account);
if (Config.X509_VERIFICATION) { if (Config.X509_VERIFICATION) {
try { try {
getMemorizingTrustManager().getNonInteractive(account.getJid().getDomainpart()).checkClientTrusted(chain, "RSA"); getMemorizingTrustManager().getNonInteractive().checkClientTrusted(chain, "RSA");
} catch (CertificateException e) { } catch (CertificateException e) {
callback.informUser(R.string.certificate_chain_is_not_trusted); callback.informUser(R.string.certificate_chain_is_not_trusted);
} }
@ -1815,7 +1683,7 @@ public class XmppConnectionService extends Service {
databaseBackend.updateAccount(account); databaseBackend.updateAccount(account);
if (Config.X509_VERIFICATION) { if (Config.X509_VERIFICATION) {
try { try {
getMemorizingTrustManager().getNonInteractive(account.getJid().getDomainpart()).checkClientTrusted(chain, "RSA"); getMemorizingTrustManager().getNonInteractive().checkClientTrusted(chain, "RSA");
} catch (CertificateException e) { } catch (CertificateException e) {
showErrorToastInUi(R.string.certificate_chain_is_not_trusted); showErrorToastInUi(R.string.certificate_chain_is_not_trusted);
} }
@ -2143,6 +2011,10 @@ public class XmppConnectionService extends Service {
if (connection.getFeatures().csi()) { if (connection.getFeatures().csi()) {
connection.sendInactive(); connection.sendInactive();
} }
if (Config.PUSH_MODE && mPushManagementService.available(account)) {
connection.waitForPush();
cancelWakeUpCall(account.getUuid().hashCode());
}
} }
} }
} }
@ -2160,18 +2032,10 @@ public class XmppConnectionService extends Service {
} }
public void joinMuc(Conversation conversation) { public void joinMuc(Conversation conversation) {
joinMuc(conversation,null, false); joinMuc(conversation, null);
}
public void joinMuc(Conversation conversation, boolean followedInvite) {
joinMuc(conversation, null, followedInvite);
} }
private void joinMuc(Conversation conversation, final OnConferenceJoined onConferenceJoined) { private void joinMuc(Conversation conversation, final OnConferenceJoined onConferenceJoined) {
joinMuc(conversation,onConferenceJoined,false);
}
private void joinMuc(Conversation conversation, final OnConferenceJoined onConferenceJoined, final boolean followedInvite) {
Account account = conversation.getAccount(); Account account = conversation.getAccount();
account.pendingConferenceJoins.remove(conversation); account.pendingConferenceJoins.remove(conversation);
account.pendingConferenceLeaves.remove(conversation); account.pendingConferenceLeaves.remove(conversation);
@ -2216,9 +2080,6 @@ public class XmppConnectionService extends Service {
} }
if (mucOptions.membersOnly() && mucOptions.nonanonymous()) { if (mucOptions.membersOnly() && mucOptions.nonanonymous()) {
fetchConferenceMembers(conversation); fetchConferenceMembers(conversation);
if (followedInvite && conversation.getBookmark() == null) {
saveConversationAsBookmark(conversation,null);
}
} }
sendUnsentMessages(conversation); sendUnsentMessages(conversation);
} }
@ -2253,7 +2114,6 @@ public class XmppConnectionService extends Service {
OnIqPacketReceived callback = new OnIqPacketReceived() { OnIqPacketReceived callback = new OnIqPacketReceived() {
private int i = 0; private int i = 0;
private boolean success = true;
@Override @Override
public void onIqPacketReceived(Account account, IqPacket packet) { public void onIqPacketReceived(Account account, IqPacket packet) {
@ -2264,37 +2124,19 @@ public class XmppConnectionService extends Service {
if ("item".equals(child.getName())) { if ("item".equals(child.getName())) {
MucOptions.User user = AbstractParser.parseItem(conversation,child); MucOptions.User user = AbstractParser.parseItem(conversation,child);
if (!user.realJidMatchesAccount()) { if (!user.realJidMatchesAccount()) {
conversation.getMucOptions().updateUser(user); conversation.getMucOptions().addUser(user);
getAvatarService().clear(conversation);
updateMucRosterUi();
updateConversationUi();
} }
} }
} }
} else { } else {
success = false;
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could not request affiliation "+affiliations[i]+" in "+conversation.getJid().toBareJid()); Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could not request affiliation "+affiliations[i]+" in "+conversation.getJid().toBareJid());
} }
++i; ++i;
if (i >= affiliations.length) { if (i >= affiliations.length) {
List<Jid> members = conversation.getMucOptions().getMembers();
if (success) {
List<Jid> cryptoTargets = conversation.getAcceptedCryptoTargets();
boolean changed = false;
for(ListIterator<Jid> iterator = cryptoTargets.listIterator(); iterator.hasNext();) {
Jid jid = iterator.next();
if (!members.contains(jid)) {
iterator.remove();
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": removed "+jid+" from crypto targets of "+conversation.getName());
changed = true;
}
}
if (changed) {
conversation.setAcceptedCryptoTargets(cryptoTargets);
updateConversation(conversation);
}
}
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": retrieved members for "+conversation.getJid().toBareJid()+": "+conversation.getMucOptions().getMembers()); Log.d(Config.LOGTAG,account.getJid().toBareJid()+": retrieved members for "+conversation.getJid().toBareJid()+": "+conversation.getMucOptions().getMembers());
getAvatarService().clear(conversation);
updateMucRosterUi();
updateConversationUi();
} }
} }
}; };
@ -2771,13 +2613,14 @@ public class XmppConnectionService extends Service {
} }
public void publishAvatar(Account account, final Avatar avatar, final UiCallback<Avatar> callback) { public void publishAvatar(Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
IqPacket packet = this.mIqGenerator.publishAvatar(avatar); final IqPacket packet = this.mIqGenerator.publishAvatar(avatar);
this.sendIqPacket(account, packet, new OnIqPacketReceived() { this.sendIqPacket(account, packet, new OnIqPacketReceived() {
@Override @Override
public void onIqPacketReceived(Account account, IqPacket result) { public void onIqPacketReceived(Account account, IqPacket result) {
if (result.getType() == IqPacket.TYPE.RESULT) { if (result.getType() == IqPacket.TYPE.RESULT) {
final IqPacket packet = XmppConnectionService.this.mIqGenerator.publishAvatarMetadata(avatar); final IqPacket packet = XmppConnectionService.this.mIqGenerator
.publishAvatarMetadata(avatar);
sendIqPacket(account, packet, new OnIqPacketReceived() { sendIqPacket(account, packet, new OnIqPacketReceived() {
@Override @Override
public void onIqPacketReceived(Account account, IqPacket result) { public void onIqPacketReceived(Account account, IqPacket result) {
@ -2786,22 +2629,25 @@ public class XmppConnectionService extends Service {
getAvatarService().clear(account); getAvatarService().clear(account);
databaseBackend.updateAccount(account); databaseBackend.updateAccount(account);
} }
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": published avatar "+(avatar.size/1024)+"KiB");
if (callback != null) { if (callback != null) {
callback.success(avatar); callback.success(avatar);
} else {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": published avatar");
} }
} else { } else {
if (callback != null) { if (callback != null) {
callback.error(R.string.error_publish_avatar_server_reject,avatar); callback.error(
R.string.error_publish_avatar_server_reject,
avatar);
} }
} }
} }
}); });
} else { } else {
Element error = result.findChild("error");
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": server rejected avatar "+(avatar.size/1024)+"KiB "+(error!=null?error.toString():""));
if (callback != null) { if (callback != null) {
callback.error(R.string.error_publish_avatar_server_reject, avatar); callback.error(
R.string.error_publish_avatar_server_reject,
avatar);
} }
} }
} }
@ -3044,26 +2890,23 @@ public class XmppConnectionService extends Service {
if (connection == null) { if (connection == null) {
connection = createConnection(account); connection = createConnection(account);
account.setXmppConnection(connection); account.setXmppConnection(connection);
} else {
connection.interrupt();
} }
boolean hasInternet = hasInternetConnection(); if (!account.isOptionSet(Account.OPTION_DISABLED)) {
if (!account.isOptionSet(Account.OPTION_DISABLED) && hasInternet) {
if (!force) { if (!force) {
disconnect(account, false); disconnect(account, false);
} }
Thread thread = new Thread(connection); Thread thread = new Thread(connection);
connection.setInteractive(interactive); connection.setInteractive(interactive);
connection.prepareNewConnection(); connection.prepareNewConnection();
connection.interrupt();
thread.start(); thread.start();
scheduleWakeUpCall(Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode()); scheduleWakeUpCall(Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode());
} else { } else {
disconnect(account, force || account.getTrueStatus().isError() || !hasInternet); disconnect(account, force);
account.getRoster().clearPresences(); account.getRoster().clearPresences();
connection.resetEverything(); connection.resetEverything();
account.getAxolotlService().resetBrokenness(); account.getAxolotlService().resetBrokenness();
if (!hasInternet) {
account.setStatus(Account.State.NO_INTERNET);
}
} }
} }
} }
@ -3144,7 +2987,8 @@ public class XmppConnectionService extends Service {
public void markMessage(Message message, int status, String errorMessage) { public void markMessage(Message message, int status, String errorMessage) {
if (status == Message.STATUS_SEND_FAILED if (status == Message.STATUS_SEND_FAILED
&& (message.getStatus() == Message.STATUS_SEND_RECEIVED || message && (message.getStatus() == Message.STATUS_SEND_RECEIVED || message
.getStatus() == Message.STATUS_SEND_DISPLAYED)) { .getStatus() == Message.STATUS_SEND_DISPLAYED)
&& errorMessage == null) {
return; return;
} }
message.setErrorMessage(errorMessage); message.setErrorMessage(errorMessage);
@ -3158,15 +3002,6 @@ public class XmppConnectionService extends Service {
.getDefaultSharedPreferences(getApplicationContext()); .getDefaultSharedPreferences(getApplicationContext());
} }
public long getAutomaticMessageDeletionDate() {
try {
final long timeout = Long.parseLong(getPreferences().getString(SettingsActivity.AUTOMATIC_MESSAGE_DELETION, "0")) * 1000;
return timeout == 0 ? timeout : System.currentTimeMillis() - timeout;
} catch (NumberFormatException e) {
return 0;
}
}
public boolean confirmMessages() { public boolean confirmMessages() {
return getPreferences().getBoolean("confirm_messages", true); return getPreferences().getBoolean("confirm_messages", true);
} }
@ -3179,6 +3014,10 @@ public class XmppConnectionService extends Service {
return getPreferences().getBoolean("chat_states", false); return getPreferences().getBoolean("chat_states", false);
} }
public boolean saveEncryptedMessages() {
return !getPreferences().getBoolean("dont_save_encrypted", false);
}
private boolean respectAutojoin() { private boolean respectAutojoin() {
return getPreferences().getBoolean("autojoin", true); return getPreferences().getBoolean("autojoin", true);
} }
@ -3764,68 +3603,6 @@ public class XmppConnectionService extends Service {
conversation.setBookmark(bookmark); conversation.setBookmark(bookmark);
} }
public void clearStartTimeCounter() {
mDatabaseExecutor.execute(new Runnable() {
@Override
public void run() {
databaseBackend.clearStartTimeCounter(false);
}
});
}
public boolean verifyFingerprints(Contact contact, List<XmppUri.Fingerprint> fingerprints) {
boolean needsRosterWrite = false;
boolean performedVerification = false;
final AxolotlService axolotlService = contact.getAccount().getAxolotlService();
for(XmppUri.Fingerprint fp : fingerprints) {
if (fp.type == XmppUri.FingerprintType.OTR) {
performedVerification |= contact.addOtrFingerprint(fp.fingerprint);
needsRosterWrite |= performedVerification;
} else if (fp.type == XmppUri.FingerprintType.OMEMO) {
String fingerprint = "05"+fp.fingerprint.replaceAll("\\s","");
FingerprintStatus fingerprintStatus = axolotlService.getFingerprintTrust(fingerprint);
if (fingerprintStatus != null) {
if (!fingerprintStatus.isVerified()) {
performedVerification = true;
axolotlService.setFingerprintTrust(fingerprint,fingerprintStatus.toVerified());
}
} else {
axolotlService.preVerifyFingerprint(contact,fingerprint);
}
}
}
if (needsRosterWrite) {
syncRosterToDisk(contact.getAccount());
}
return performedVerification;
}
public boolean verifyFingerprints(Account account, List<XmppUri.Fingerprint> fingerprints) {
final AxolotlService axolotlService = account.getAxolotlService();
boolean verifiedSomething = false;
for(XmppUri.Fingerprint fp : fingerprints) {
if (fp.type == XmppUri.FingerprintType.OMEMO) {
String fingerprint = "05"+fp.fingerprint.replaceAll("\\s","");
Log.d(Config.LOGTAG,"trying to verify own fp="+fingerprint);
FingerprintStatus fingerprintStatus = axolotlService.getFingerprintTrust(fingerprint);
if (fingerprintStatus != null) {
if (!fingerprintStatus.isVerified()) {
axolotlService.setFingerprintTrust(fingerprint,fingerprintStatus.toVerified());
verifiedSomething = true;
}
} else {
axolotlService.preVerifyFingerprint(account,fingerprint);
verifiedSomething = true;
}
}
}
return verifiedSomething;
}
public boolean blindTrustBeforeVerification() {
return getPreferences().getBoolean(SettingsActivity.BLIND_TRUST_BEFORE_VERIFICATION, true);
}
public interface OnMamPreferencesFetched { public interface OnMamPreferencesFetched {
void onPreferencesFetched(Element prefs); void onPreferencesFetched(Element prefs);
void onPreferencesFetchFailed(); void onPreferencesFetchFailed();

View file

@ -37,6 +37,7 @@ public final class BlockContactDialog {
builder.setTitle(isBlocked ? R.string.action_unblock_domain : R.string.action_block_domain); builder.setTitle(isBlocked ? R.string.action_unblock_domain : R.string.action_block_domain);
value = blockable.getJid().toDomainJid().toString(); value = blockable.getJid().toDomainJid().toString();
spannable = new SpannableString(context.getString(isBlocked ? R.string.unblock_domain_text : R.string.block_domain_text, value)); spannable = new SpannableString(context.getString(isBlocked ? R.string.unblock_domain_text : R.string.block_domain_text, value));
message.setText(spannable);
} else { } else {
builder.setTitle(isBlocked ? R.string.action_unblock_contact : R.string.action_block_contact); builder.setTitle(isBlocked ? R.string.action_unblock_contact : R.string.action_block_contact);
value = blockable.getJid().toBareJid().toString(); value = blockable.getJid().toBareJid().toString();

View file

@ -2,8 +2,10 @@ package eu.siacs.conversations.ui;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.ActivityNotFoundException;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentSender.SendIntentException; import android.content.IntentSender.SendIntentException;
import android.os.Bundle; import android.os.Bundle;
import android.view.ContextMenu; import android.view.ContextMenu;
@ -42,9 +44,6 @@ import eu.siacs.conversations.xmpp.jid.Jid;
public class ConferenceDetailsActivity extends XmppActivity implements OnConversationUpdate, OnMucRosterUpdate, XmppConnectionService.OnAffiliationChanged, XmppConnectionService.OnRoleChanged, XmppConnectionService.OnConferenceOptionsPushed { public class ConferenceDetailsActivity extends XmppActivity implements OnConversationUpdate, OnMucRosterUpdate, XmppConnectionService.OnAffiliationChanged, XmppConnectionService.OnRoleChanged, XmppConnectionService.OnConferenceOptionsPushed {
public static final String ACTION_VIEW_MUC = "view_muc"; public static final String ACTION_VIEW_MUC = "view_muc";
private static final float INACTIVE_ALPHA = 0.4684f; //compromise between dark and light theme
private Conversation mConversation; private Conversation mConversation;
private OnClickListener inviteListener = new OnClickListener() { private OnClickListener inviteListener = new OnClickListener() {
@ -381,7 +380,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
MenuItem invite = menu.findItem(R.id.invite); MenuItem invite = menu.findItem(R.id.invite);
startConversation.setVisible(true); startConversation.setVisible(true);
if (contact != null) { if (contact != null) {
showContactDetails.setVisible(!contact.isSelf()); showContactDetails.setVisible(true);
} }
if (user.getRole() == MucOptions.Role.NONE) { if (user.getRole() == MucOptions.Role.NONE) {
invite.setVisible(true); invite.setVisible(true);
@ -512,7 +511,8 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
this.uuid = getIntent().getExtras().getString("uuid"); this.uuid = getIntent().getExtras().getString("uuid");
} }
if (uuid != null) { if (uuid != null) {
this.mConversation = xmppConnectionService.findConversationByUuid(uuid); this.mConversation = xmppConnectionService
.findConversationByUuid(uuid);
if (this.mConversation != null) { if (this.mConversation != null) {
updateView(); updateView();
} }
@ -520,7 +520,6 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
} }
private void updateView() { private void updateView() {
invalidateOptionsMenu();
final MucOptions mucOptions = mConversation.getMucOptions(); final MucOptions mucOptions = mConversation.getMucOptions();
final User self = mucOptions.getSelf(); final User self = mucOptions.getSelf();
String account; String account;
@ -622,12 +621,6 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
} }
ImageView iv = (ImageView) view.findViewById(R.id.contact_photo); ImageView iv = (ImageView) view.findViewById(R.id.contact_photo);
iv.setImageBitmap(avatarService().get(user, getPixel(48), false)); iv.setImageBitmap(avatarService().get(user, getPixel(48), false));
if (user.getRole() == MucOptions.Role.NONE) {
tvDisplayName.setAlpha(INACTIVE_ALPHA);
tvKey.setAlpha(INACTIVE_ALPHA);
tvStatus.setAlpha(INACTIVE_ALPHA);
iv.setAlpha(INACTIVE_ALPHA);
}
membersView.addView(view); membersView.addView(view);
if (mConversation.getMucOptions().canInvite()) { if (mConversation.getMucOptions().canInvite()) {
mInviteButton.setVisibility(View.VISIBLE); mInviteButton.setVisibility(View.VISIBLE);

View file

@ -32,29 +32,29 @@ import com.wefika.flowlayout.FlowLayout;
import org.openintents.openpgp.util.OpenPgpUtils; import org.openintents.openpgp.util.OpenPgpUtils;
import java.security.cert.X509Certificate;
import java.util.List; import java.util.List;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.R; import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.PgpEngine; import eu.siacs.conversations.crypto.PgpEngine;
import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession; import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.ListItem; import eu.siacs.conversations.entities.ListItem;
import eu.siacs.conversations.entities.Presence;
import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate; import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate; import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate;
import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.utils.XmppUri;
import eu.siacs.conversations.xmpp.OnKeyStatusUpdated; import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
import eu.siacs.conversations.xmpp.OnUpdateBlocklist; import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid; import eu.siacs.conversations.xmpp.jid.Jid;
public class ContactDetailsActivity extends OmemoActivity implements OnAccountUpdate, OnRosterUpdate, OnUpdateBlocklist, OnKeyStatusUpdated { public class ContactDetailsActivity extends XmppActivity implements OnAccountUpdate, OnRosterUpdate, OnUpdateBlocklist, OnKeyStatusUpdated {
public static final String ACTION_VIEW_CONTACT = "view_contact"; public static final String ACTION_VIEW_CONTACT = "view_contact";
private Contact contact; private Contact contact;
@ -113,14 +113,11 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
private CheckBox send; private CheckBox send;
private CheckBox receive; private CheckBox receive;
private Button addContactButton; private Button addContactButton;
private Button mShowInactiveDevicesButton;
private QuickContactBadge badge; private QuickContactBadge badge;
private LinearLayout keys; private LinearLayout keys;
private LinearLayout keysWrapper;
private FlowLayout tags; private FlowLayout tags;
private boolean showDynamicTags = false; private boolean showDynamicTags = false;
private boolean showLastSeen = false; private boolean showLastSeen = false;
private boolean showInactiveOmemo = false;
private String messageFingerprint; private String messageFingerprint;
private DialogInterface.OnClickListener addToPhonebook = new DialogInterface.OnClickListener() { private DialogInterface.OnClickListener addToPhonebook = new DialogInterface.OnClickListener() {
@ -192,7 +189,6 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
@Override @Override
protected void onCreate(final Bundle savedInstanceState) { protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
showInactiveOmemo = savedInstanceState != null && savedInstanceState.getBoolean("show_inactive_omemo",false);
if (getIntent().getAction().equals(ACTION_VIEW_CONTACT)) { if (getIntent().getAction().equals(ACTION_VIEW_CONTACT)) {
try { try {
this.accountJid = Jid.fromString(getIntent().getExtras().getString(EXTRA_ACCOUNT)); this.accountJid = Jid.fromString(getIntent().getExtras().getString(EXTRA_ACCOUNT));
@ -221,26 +217,11 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
} }
}); });
keys = (LinearLayout) findViewById(R.id.details_contact_keys); keys = (LinearLayout) findViewById(R.id.details_contact_keys);
keysWrapper = (LinearLayout) findViewById(R.id.keys_wrapper);
tags = (FlowLayout) findViewById(R.id.tags); tags = (FlowLayout) findViewById(R.id.tags);
mShowInactiveDevicesButton = (Button) findViewById(R.id.show_inactive_devices);
if (getActionBar() != null) { if (getActionBar() != null) {
getActionBar().setHomeButtonEnabled(true); getActionBar().setHomeButtonEnabled(true);
getActionBar().setDisplayHomeAsUpEnabled(true); getActionBar().setDisplayHomeAsUpEnabled(true);
} }
mShowInactiveDevicesButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
showInactiveOmemo = !showInactiveOmemo;
populateView();
}
});
}
@Override
public void onSaveInstanceState(final Bundle savedInstanceState) {
savedInstanceState.putBoolean("show_inactive_omemo",showInactiveOmemo);
super.onSaveInstanceState(savedInstanceState);
} }
@Override @Override
@ -464,32 +445,15 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
} }
} }
if (Config.supportOmemo()) { if (Config.supportOmemo()) {
boolean skippedInactive = false; for (final String fingerprint : contact.getAccount().getAxolotlService().getFingerprintsForContact(contact)) {
boolean showsInactive = false; boolean highlight = fingerprint.equals(messageFingerprint);
for (final XmppAxolotlSession session : contact.getAccount().getAxolotlService().findSessionsForContact(contact)) { hasKeys |= addFingerprintRow(keys, contact.getAccount(), fingerprint, highlight, new OnClickListener() {
final FingerprintStatus trust = session.getTrust(); @Override
if (!trust.isActive()) { public void onClick(View v) {
if (showInactiveOmemo) { onOmemoKeyClicked(contact.getAccount(), fingerprint);
showsInactive = true;
} else {
skippedInactive = true;
continue;
} }
} });
if (!trust.isCompromised()) {
boolean highlight = session.getFingerprint().equals(messageFingerprint);
hasKeys = true;
addFingerprintRow(keys, session, highlight);
}
} }
if (showsInactive || skippedInactive) {
mShowInactiveDevicesButton.setText(showsInactive ? R.string.hide_inactive_devices : R.string.show_inactive_devices);
mShowInactiveDevicesButton.setVisibility(View.VISIBLE);
} else {
mShowInactiveDevicesButton.setVisibility(View.GONE);
}
} else {
mShowInactiveDevicesButton.setVisibility(View.GONE);
} }
if (Config.supportOpenPgp() && contact.getPgpKeyId() != 0) { if (Config.supportOpenPgp() && contact.getPgpKeyId() != 0) {
hasKeys = true; hasKeys = true;
@ -523,7 +487,11 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
}); });
keys.addView(view); keys.addView(view);
} }
keysWrapper.setVisibility(hasKeys ? View.VISIBLE : View.GONE); if (hasKeys) {
keys.setVisibility(View.VISIBLE);
} else {
keys.setVisibility(View.GONE);
}
List<ListItem.Tag> tagList = contact.getTags(this); List<ListItem.Tag> tagList = contact.getTags(this);
if (tagList.size() == 0 || !this.showDynamicTags) { if (tagList.size() == 0 || !this.showDynamicTags) {
@ -540,6 +508,40 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
} }
} }
private void onOmemoKeyClicked(Account account, String fingerprint) {
final XmppAxolotlSession.Trust trust = account.getAxolotlService().getFingerprintTrust(fingerprint);
if (Config.X509_VERIFICATION && trust != null && trust == XmppAxolotlSession.Trust.TRUSTED_X509) {
X509Certificate x509Certificate = account.getAxolotlService().getFingerprintCertificate(fingerprint);
if (x509Certificate != null) {
showCertificateInformationDialog(CryptoHelper.extractCertificateInformation(x509Certificate));
} else {
Toast.makeText(this,R.string.certificate_not_found, Toast.LENGTH_SHORT).show();
}
}
}
private void showCertificateInformationDialog(Bundle bundle) {
View view = getLayoutInflater().inflate(R.layout.certificate_information, null);
final String not_available = getString(R.string.certicate_info_not_available);
TextView subject_cn = (TextView) view.findViewById(R.id.subject_cn);
TextView subject_o = (TextView) view.findViewById(R.id.subject_o);
TextView issuer_cn = (TextView) view.findViewById(R.id.issuer_cn);
TextView issuer_o = (TextView) view.findViewById(R.id.issuer_o);
TextView sha1 = (TextView) view.findViewById(R.id.sha1);
subject_cn.setText(bundle.getString("subject_cn", not_available));
subject_o.setText(bundle.getString("subject_o", not_available));
issuer_cn.setText(bundle.getString("issuer_cn", not_available));
issuer_o.setText(bundle.getString("issuer_o", not_available));
sha1.setText(bundle.getString("sha1", not_available));
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.certificate_information);
builder.setView(view);
builder.setPositiveButton(R.string.ok, null);
builder.create().show();
}
protected void confirmToDeleteFingerprint(final String fingerprint) { protected void confirmToDeleteFingerprint(final String fingerprint) {
AlertDialog.Builder builder = new AlertDialog.Builder(this); AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.delete_fingerprint); builder.setTitle(R.string.delete_fingerprint);
@ -560,17 +562,15 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
builder.create().show(); builder.create().show();
} }
@Override
public void onBackendConnected() { public void onBackendConnected() {
if (accountJid != null && contactJid != null) { if ((accountJid != null) && (contactJid != null)) {
Account account = xmppConnectionService.findAccountByJid(accountJid); Account account = xmppConnectionService
.findAccountByJid(accountJid);
if (account == null) { if (account == null) {
return; return;
} }
this.contact = account.getRoster().getContact(contactJid); this.contact = account.getRoster().getContact(contactJid);
if (mPendingFingerprintVerificationUri != null) {
processFingerprintVerification(mPendingFingerprintVerificationUri);
mPendingFingerprintVerificationUri = null;
}
populateView(); populateView();
} }
} }
@ -579,15 +579,4 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
public void onKeyStatusUpdated(AxolotlService.FetchStatus report) { public void onKeyStatusUpdated(AxolotlService.FetchStatus report) {
refreshUi(); refreshUi();
} }
@Override
protected void processFingerprintVerification(XmppUri uri) {
if (contact != null && contact.getJid().toBareJid().equals(uri.getJid()) && uri.hasFingerprints()) {
if (xmppConnectionService.verifyFingerprints(contact,uri.getFingerprints())) {
Toast.makeText(this,R.string.verified_fingerprints,Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(this,R.string.invalid_barcode,Toast.LENGTH_SHORT).show();
}
}
} }

View file

@ -49,7 +49,6 @@ import de.timroes.android.listview.EnhancedListView;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.R; import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession; import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Blockable; import eu.siacs.conversations.entities.Blockable;
@ -65,7 +64,6 @@ import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate;
import eu.siacs.conversations.ui.adapter.ConversationAdapter; import eu.siacs.conversations.ui.adapter.ConversationAdapter;
import eu.siacs.conversations.utils.ExceptionHelper; import eu.siacs.conversations.utils.ExceptionHelper;
import eu.siacs.conversations.xmpp.OnUpdateBlocklist; import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid; import eu.siacs.conversations.xmpp.jid.Jid;
@ -99,7 +97,6 @@ public class ConversationActivity extends XmppActivity
private String mOpenConversation = null; private String mOpenConversation = null;
private boolean mPanelOpen = true; private boolean mPanelOpen = true;
private AtomicBoolean mShouldPanelBeOpen = new AtomicBoolean(false);
private Pair<Integer,Integer> mScrollPosition = null; private Pair<Integer,Integer> mScrollPosition = null;
final private List<Uri> mPendingImageUris = new ArrayList<>(); final private List<Uri> mPendingImageUris = new ArrayList<>();
final private List<Uri> mPendingFileUris = new ArrayList<>(); final private List<Uri> mPendingFileUris = new ArrayList<>();
@ -122,7 +119,6 @@ public class ConversationActivity extends XmppActivity
private boolean mActivityPaused = false; private boolean mActivityPaused = false;
private AtomicBoolean mRedirected = new AtomicBoolean(false); private AtomicBoolean mRedirected = new AtomicBoolean(false);
private Pair<Integer, Intent> mPostponedActivityResult; private Pair<Integer, Intent> mPostponedActivityResult;
private boolean mUnprocessedNewIntent = false;
public Conversation getSelectedConversation() { public Conversation getSelectedConversation() {
return this.mSelectedConversation; return this.mSelectedConversation;
@ -135,7 +131,6 @@ public class ConversationActivity extends XmppActivity
public void showConversationsOverview() { public void showConversationsOverview() {
if (mContentView instanceof SlidingPaneLayout) { if (mContentView instanceof SlidingPaneLayout) {
SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView; SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView;
mShouldPanelBeOpen.set(true);
mSlidingPaneLayout.openPane(); mSlidingPaneLayout.openPane();
} }
} }
@ -153,7 +148,6 @@ public class ConversationActivity extends XmppActivity
public void hideConversationsOverview() { public void hideConversationsOverview() {
if (mContentView instanceof SlidingPaneLayout) { if (mContentView instanceof SlidingPaneLayout) {
SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView; SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView;
mShouldPanelBeOpen.set(false);
mSlidingPaneLayout.closePane(); mSlidingPaneLayout.closePane();
} }
} }
@ -164,7 +158,8 @@ public class ConversationActivity extends XmppActivity
public boolean isConversationsOverviewVisable() { public boolean isConversationsOverviewVisable() {
if (mContentView instanceof SlidingPaneLayout) { if (mContentView instanceof SlidingPaneLayout) {
return mShouldPanelBeOpen.get(); SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView;
return mSlidingPaneLayout.isOpen();
} else { } else {
return true; return true;
} }
@ -298,25 +293,26 @@ public class ConversationActivity extends XmppActivity
} }
if (mContentView instanceof SlidingPaneLayout) { if (mContentView instanceof SlidingPaneLayout) {
SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView; SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView;
mSlidingPaneLayout.setShadowResource(R.drawable.es_slidingpane_shadow); mSlidingPaneLayout.setParallaxDistance(150);
mSlidingPaneLayout
.setShadowResource(R.drawable.es_slidingpane_shadow);
mSlidingPaneLayout.setSliderFadeColor(0); mSlidingPaneLayout.setSliderFadeColor(0);
mSlidingPaneLayout.setPanelSlideListener(new PanelSlideListener() { mSlidingPaneLayout.setPanelSlideListener(new PanelSlideListener() {
@Override @Override
public void onPanelOpened(View arg0) { public void onPanelOpened(View arg0) {
mShouldPanelBeOpen.set(true);
updateActionBarTitle(); updateActionBarTitle();
invalidateOptionsMenu(); invalidateOptionsMenu();
hideKeyboard(); hideKeyboard();
if (xmppConnectionServiceBound) { if (xmppConnectionServiceBound) {
xmppConnectionService.getNotificationService().setOpenConversation(null); xmppConnectionService.getNotificationService()
.setOpenConversation(null);
} }
closeContextMenu(); closeContextMenu();
} }
@Override @Override
public void onPanelClosed(View arg0) { public void onPanelClosed(View arg0) {
mShouldPanelBeOpen.set(false);
listView.discardUndo(); listView.discardUndo();
openConversation(); openConversation();
} }
@ -378,7 +374,7 @@ public class ConversationActivity extends XmppActivity
} }
public void sendReadMarkerIfNecessary(final Conversation conversation) { public void sendReadMarkerIfNecessary(final Conversation conversation) {
if (!mActivityPaused && !mUnprocessedNewIntent && conversation != null) { if (!mActivityPaused && conversation != null) {
xmppConnectionService.sendReadMarker(conversation); xmppConnectionService.sendReadMarker(conversation);
} }
} }
@ -498,7 +494,6 @@ public class ConversationActivity extends XmppActivity
case ATTACHMENT_CHOICE_TAKE_PHOTO: case ATTACHMENT_CHOICE_TAKE_PHOTO:
Uri uri = xmppConnectionService.getFileBackend().getTakePhotoUri(); Uri uri = xmppConnectionService.getFileBackend().getTakePhotoUri();
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE); intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri); intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
mPendingImageUris.clear(); mPendingImageUris.clear();
@ -553,7 +548,7 @@ public class ConversationActivity extends XmppActivity
public void attachFile(final int attachmentChoice) { public void attachFile(final int attachmentChoice) {
if (attachmentChoice != ATTACHMENT_CHOICE_LOCATION) { if (attachmentChoice != ATTACHMENT_CHOICE_LOCATION) {
if (!Config.ONLY_INTERNAL_STORAGE && !hasStoragePermission(attachmentChoice)) { if (!hasStoragePermission(attachmentChoice)) {
return; return;
} }
} }
@ -650,7 +645,7 @@ public class ConversationActivity extends XmppActivity
} }
public void startDownloadable(Message message) { public void startDownloadable(Message message) {
if (!Config.ONLY_INTERNAL_STORAGE && !hasStoragePermission(ConversationActivity.REQUEST_START_DOWNLOAD)) { if (!hasStoragePermission(ConversationActivity.REQUEST_START_DOWNLOAD)) {
this.mPendingDownloadableMessage = message; this.mPendingDownloadableMessage = message;
return; return;
} }
@ -972,7 +967,7 @@ public class ConversationActivity extends XmppActivity
if (!isConversationsOverviewVisable()) { if (!isConversationsOverviewVisable()) {
showConversationsOverview(); showConversationsOverview();
} else { } else {
super.onBackPressed(); moveTaskToBack(true);
} }
} }
@ -994,7 +989,6 @@ public class ConversationActivity extends XmppActivity
upKey = KeyEvent.KEYCODE_DPAD_RIGHT; upKey = KeyEvent.KEYCODE_DPAD_RIGHT;
downKey = KeyEvent.KEYCODE_DPAD_LEFT; downKey = KeyEvent.KEYCODE_DPAD_LEFT;
break; break;
case Surface.ROTATION_0:
default: default:
upKey = KeyEvent.KEYCODE_DPAD_UP; upKey = KeyEvent.KEYCODE_DPAD_UP;
downKey = KeyEvent.KEYCODE_DPAD_DOWN; downKey = KeyEvent.KEYCODE_DPAD_DOWN;
@ -1092,7 +1086,6 @@ public class ConversationActivity extends XmppActivity
protected void onNewIntent(final Intent intent) { protected void onNewIntent(final Intent intent) {
if (intent != null && ACTION_VIEW_CONVERSATION.equals(intent.getAction())) { if (intent != null && ACTION_VIEW_CONVERSATION.equals(intent.getAction())) {
mOpenConversation = null; mOpenConversation = null;
mUnprocessedNewIntent = true;
if (xmppConnectionServiceBound) { if (xmppConnectionServiceBound) {
handleViewConversationIntent(intent); handleViewConversationIntent(intent);
intent.setAction(Intent.ACTION_MAIN); intent.setAction(Intent.ACTION_MAIN);
@ -1131,7 +1124,6 @@ public class ConversationActivity extends XmppActivity
} }
this.mActivityPaused = false; this.mActivityPaused = false;
if (!isConversationsOverviewVisable() || !isConversationsOverviewHideable()) { if (!isConversationsOverviewVisable() || !isConversationsOverviewHideable()) {
sendReadMarkerIfNecessary(getSelectedConversation()); sendReadMarkerIfNecessary(getSelectedConversation());
} }
@ -1270,11 +1262,6 @@ public class ConversationActivity extends XmppActivity
if (!ExceptionHelper.checkForCrash(this, this.xmppConnectionService)) { if (!ExceptionHelper.checkForCrash(this, this.xmppConnectionService)) {
openBatteryOptimizationDialogIfNeeded(); openBatteryOptimizationDialogIfNeeded();
} }
if (isConversationsOverviewVisable() && isConversationsOverviewHideable()) {
xmppConnectionService.getNotificationService().setOpenConversation(null);
} else {
xmppConnectionService.getNotificationService().setOpenConversation(getSelectedConversation());
}
} }
private void handleViewConversationIntent(final Intent intent) { private void handleViewConversationIntent(final Intent intent) {
@ -1301,7 +1288,6 @@ public class ConversationActivity extends XmppActivity
this.mConversationFragment.appendText(text); this.mConversationFragment.appendText(text);
} }
hideConversationsOverview(); hideConversationsOverview();
mUnprocessedNewIntent = false;
openConversation(); openConversation();
if (mContentView instanceof SlidingPaneLayout) { if (mContentView instanceof SlidingPaneLayout) {
updateActionBarTitle(true); //fixes bug where slp isn't properly closed yet updateActionBarTitle(true); //fixes bug where slp isn't properly closed yet
@ -1312,8 +1298,6 @@ public class ConversationActivity extends XmppActivity
startDownloadable(message); startDownloadable(message);
} }
} }
} else {
mUnprocessedNewIntent = false;
} }
} }
@ -1425,11 +1409,9 @@ public class ConversationActivity extends XmppActivity
attachImageToConversation(getSelectedConversation(), uri); attachImageToConversation(getSelectedConversation(), uri);
mPendingImageUris.clear(); mPendingImageUris.clear();
} }
if (!Config.ONLY_INTERNAL_STORAGE) { Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); intent.setData(uri);
intent.setData(uri); sendBroadcast(intent);
sendBroadcast(intent);
}
} else { } else {
mPendingImageUris.clear(); mPendingImageUris.clear();
} }
@ -1464,8 +1446,7 @@ public class ConversationActivity extends XmppActivity
} }
private long getMaxHttpUploadSize(Conversation conversation) { private long getMaxHttpUploadSize(Conversation conversation) {
final XmppConnection connection = conversation.getAccount().getXmppConnection(); return conversation.getAccount().getXmppConnection().getFeatures().getMaxHttpUploadSize();
return connection == null ? -1 : connection.getFeatures().getMaxHttpUploadSize();
} }
private void setNeverAskForBatteryOptimizationsAgain() { private void setNeverAskForBatteryOptimizationsAgain() {
@ -1507,7 +1488,7 @@ public class ConversationActivity extends XmppActivity
private boolean hasAccountWithoutPush() { private boolean hasAccountWithoutPush() {
for(Account account : xmppConnectionService.getAccounts()) { for(Account account : xmppConnectionService.getAccounts()) {
if (account.getStatus() != Account.State.DISABLED if (account.getStatus() != Account.State.DISABLED
&& !xmppConnectionService.getPushManagementService().availableAndUseful(account)) { && !xmppConnectionService.getPushManagementService().available(account)) {
return true; return true;
} }
} }
@ -1543,26 +1524,9 @@ public class ConversationActivity extends XmppActivity
} }
final Toast prepareFileToast = Toast.makeText(getApplicationContext(),getText(R.string.preparing_file), Toast.LENGTH_LONG); final Toast prepareFileToast = Toast.makeText(getApplicationContext(),getText(R.string.preparing_file), Toast.LENGTH_LONG);
prepareFileToast.show(); prepareFileToast.show();
xmppConnectionService.attachFileToConversation(conversation, uri, new UiInformableCallback<Message>() { xmppConnectionService.attachFileToConversation(conversation, uri, new UiCallback<Message>() {
@Override
public void inform(final String text) {
hidePrepareFileToast(prepareFileToast);
runOnUiThread(new Runnable() {
@Override
public void run() {
replaceToast(text);
}
});
}
@Override @Override
public void success(Message message) { public void success(Message message) {
runOnUiThread(new Runnable() {
@Override
public void run() {
hideToast();
}
});
hidePrepareFileToast(prepareFileToast); hidePrepareFileToast(prepareFileToast);
xmppConnectionService.sendMessage(message); xmppConnectionService.sendMessage(message);
} }
@ -1586,10 +1550,6 @@ public class ConversationActivity extends XmppActivity
}); });
} }
public void attachImageToConversation(Uri uri) {
this.attachImageToConversation(getSelectedConversation(), uri);
}
private void attachImageToConversation(Conversation conversation, Uri uri) { private void attachImageToConversation(Conversation conversation, Uri uri) {
if (conversation == null) { if (conversation == null) {
return; return;
@ -1709,8 +1669,8 @@ public class ConversationActivity extends XmppActivity
AxolotlService axolotlService = mSelectedConversation.getAccount().getAxolotlService(); AxolotlService axolotlService = mSelectedConversation.getAccount().getAxolotlService();
final List<Jid> targets = axolotlService.getCryptoTargets(mSelectedConversation); final List<Jid> targets = axolotlService.getCryptoTargets(mSelectedConversation);
boolean hasUnaccepted = !mSelectedConversation.getAcceptedCryptoTargets().containsAll(targets); boolean hasUnaccepted = !mSelectedConversation.getAcceptedCryptoTargets().containsAll(targets);
boolean hasUndecidedOwn = !axolotlService.getKeysWithTrust(FingerprintStatus.createActiveUndecided()).isEmpty(); boolean hasUndecidedOwn = !axolotlService.getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED).isEmpty();
boolean hasUndecidedContacts = !axolotlService.getKeysWithTrust(FingerprintStatus.createActiveUndecided(), targets).isEmpty(); boolean hasUndecidedContacts = !axolotlService.getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED, targets).isEmpty();
boolean hasPendingKeys = !axolotlService.findDevicesWithoutSession(mSelectedConversation).isEmpty(); boolean hasPendingKeys = !axolotlService.findDevicesWithoutSession(mSelectedConversation).isEmpty();
boolean hasNoTrustedKeys = axolotlService.anyTargetHasNoTrustedKeys(targets); boolean hasNoTrustedKeys = axolotlService.anyTargetHasNoTrustedKeys(targets);
if(hasUndecidedOwn || hasUndecidedContacts || hasPendingKeys || hasNoTrustedKeys || hasUnaccepted) { if(hasUndecidedOwn || hasUndecidedContacts || hasPendingKeys || hasNoTrustedKeys || hasUnaccepted) {
@ -1787,4 +1747,11 @@ public class ConversationActivity extends XmppActivity
public boolean highlightSelectedConversations() { public boolean highlightSelectedConversations() {
return !isConversationsOverviewHideable() || this.conversationWasSelectedByKeyboard; return !isConversationsOverviewHideable() || this.conversationWasSelectedByKeyboard;
} }
public void setMessagesLoaded() {
if (mConversationFragment != null) {
mConversationFragment.setMessagesLoaded();
mConversationFragment.updateMessages();
}
}
} }

View file

@ -9,13 +9,8 @@ import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.IntentSender.SendIntentException; import android.content.IntentSender.SendIntentException;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.support.v13.view.inputmethod.InputConnectionCompat;
import android.support.v13.view.inputmethod.InputContentInfoCompat;
import android.text.Editable;
import android.text.InputType; import android.text.InputType;
import android.util.Log; import android.util.Log;
import android.util.Pair; import android.util.Pair;
@ -34,7 +29,6 @@ import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener; import android.widget.AbsListView.OnScrollListener;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.EditText;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.ListView; import android.widget.ListView;
import android.widget.RelativeLayout; import android.widget.RelativeLayout;
@ -61,7 +55,6 @@ import eu.siacs.conversations.entities.Presence;
import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.TransferablePlaceholder; import eu.siacs.conversations.entities.TransferablePlaceholder;
import eu.siacs.conversations.http.HttpDownloadConnection; import eu.siacs.conversations.http.HttpDownloadConnection;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.MessageArchiveService; import eu.siacs.conversations.services.MessageArchiveService;
import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.XmppActivity.OnPresenceSelected; import eu.siacs.conversations.ui.XmppActivity.OnPresenceSelected;
@ -120,6 +113,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
private RelativeLayout snackbar; private RelativeLayout snackbar;
private TextView snackbarMessage; private TextView snackbarMessage;
private TextView snackbarAction; private TextView snackbarAction;
private boolean messagesLoaded = true;
private Toast messageLoaderToast; private Toast messageLoaderToast;
private OnScrollListener mOnScrollListener = new OnScrollListener() { private OnScrollListener mOnScrollListener = new OnScrollListener() {
@ -134,13 +128,14 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
public void onScroll(AbsListView view, int firstVisibleItem, public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) { int visibleItemCount, int totalItemCount) {
synchronized (ConversationFragment.this.messageList) { synchronized (ConversationFragment.this.messageList) {
if (firstVisibleItem < 5 && conversation != null && conversation.messagesLoaded.compareAndSet(true,false) && messageList.size() > 0) { if (firstVisibleItem < 5 && messagesLoaded && messageList.size() > 0) {
long timestamp; long timestamp;
if (messageList.get(0).getType() == Message.TYPE_STATUS && messageList.size() >= 2) { if (messageList.get(0).getType() == Message.TYPE_STATUS && messageList.size() >= 2) {
timestamp = messageList.get(1).getTimeSent(); timestamp = messageList.get(1).getTimeSent();
} else { } else {
timestamp = messageList.get(0).getTimeSent(); timestamp = messageList.get(0).getTimeSent();
} }
messagesLoaded = false;
activity.xmppConnectionService.loadMoreMessages(conversation, timestamp, new XmppConnectionService.OnMoreMessagesLoaded() { activity.xmppConnectionService.loadMoreMessages(conversation, timestamp, new XmppConnectionService.OnMoreMessagesLoaded() {
@Override @Override
public void onMoreMessagesLoaded(final int c, Conversation conversation) { public void onMoreMessagesLoaded(final int c, Conversation conversation) {
@ -169,6 +164,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
messageListAdapter.notifyDataSetChanged(); messageListAdapter.notifyDataSetChanged();
int pos = Math.max(getIndexOf(uuid,messageList),0); int pos = Math.max(getIndexOf(uuid,messageList),0);
messagesView.setSelectionFromTop(pos, pxOffset); messagesView.setSelectionFromTop(pos, pxOffset);
messagesLoaded = true;
if (messageLoaderToast != null) { if (messageLoaderToast != null) {
messageLoaderToast.cancel(); messageLoaderToast.cancel();
} }
@ -288,37 +284,6 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
} }
} }
}; };
private EditMessage.OnCommitContentListener mEditorContentListener = new EditMessage.OnCommitContentListener() {
@Override
public boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags, Bundle opts, String[] contentMimeTypes) {
// try to get permission to read the image, if applicable
if ((flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
try {
inputContentInfo.requestPermission();
} catch (Exception e) {
Log.e(Config.LOGTAG, "InputContentInfoCompat#requestPermission() failed.", e);
Toast.makeText(
activity,
activity.getString(R.string.no_permission_to_access_x, inputContentInfo.getDescription()),
Toast.LENGTH_LONG
).show();
return false;
}
}
// send the image
activity.attachImageToConversation(inputContentInfo.getContentUri());
// TODO: revoke permissions?
// since uploading an image is async its tough to wire a callback to when
// the image has finished uploading.
// According to the docs: "calling IC#releasePermission() is just to be a
// good citizen. Even if we failed to call that method, the system would eventually revoke
// the permission sometime after inputContentInfo object gets garbage-collected."
// See: https://developer.android.com/samples/CommitContentSampleApp/src/com.example.android.commitcontent.app/MainActivity.html#l164
return true;
}
};
private OnClickListener mSendButtonListener = new OnClickListener() { private OnClickListener mSendButtonListener = new OnClickListener() {
@Override @Override
@ -373,6 +338,10 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
private ConversationActivity activity; private ConversationActivity activity;
private Message selectedMessage; private Message selectedMessage;
public void setMessagesLoaded() {
this.messagesLoaded = true;
}
private void sendMessage() { private void sendMessage() {
final String body = mEditMessage.getText().toString(); final String body = mEditMessage.getText().toString();
if (body.length() == 0 || this.conversation == null) { if (body.length() == 0 || this.conversation == null) {
@ -446,8 +415,6 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
public View onCreateView(final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { public View onCreateView(final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.fragment_conversation, container, false); final View view = inflater.inflate(R.layout.fragment_conversation, container, false);
view.setOnClickListener(null); view.setOnClickListener(null);
String[] allImagesMimeType = {"image/*"};
mEditMessage = (EditMessage) view.findViewById(R.id.textinput); mEditMessage = (EditMessage) view.findViewById(R.id.textinput);
mEditMessage.setOnClickListener(new OnClickListener() { mEditMessage.setOnClickListener(new OnClickListener() {
@ -459,7 +426,6 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
} }
}); });
mEditMessage.setOnEditorActionListener(mEditorActionListener); mEditMessage.setOnEditorActionListener(mEditorActionListener);
mEditMessage.setRichContentListener(allImagesMimeType, mEditorContentListener);
mSendButton = (ImageButton) view.findViewById(R.id.textSendButton); mSendButton = (ImageButton) view.findViewById(R.id.textSendButton);
mSendButton.setOnClickListener(this.mSendButtonListener); mSendButton.setOnClickListener(this.mSendButtonListener);
@ -540,34 +506,6 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
} }
} }
}); });
messageListAdapter.setOnQuoteListener(new MessageAdapter.OnQuoteListener() {
@Override
public void onQuote(String text) {
if (mEditMessage.isEnabled()) {
text = text.replaceAll("(\n *){2,}", "\n").replaceAll("(^|\n)", "$1> ").replaceAll("\n$", "");
Editable editable = mEditMessage.getEditableText();
int position = mEditMessage.getSelectionEnd();
if (position == -1) position = editable.length();
if (position > 0 && editable.charAt(position - 1) != '\n') {
editable.insert(position++, "\n");
}
editable.insert(position, text);
position += text.length();
editable.insert(position++, "\n");
if (position < editable.length() && editable.charAt(position) != '\n') {
editable.insert(position, "\n");
}
mEditMessage.setSelection(position);
mEditMessage.requestFocus();
InputMethodManager inputMethodManager = (InputMethodManager) getActivity()
.getSystemService(Context.INPUT_METHOD_SERVICE);
if (inputMethodManager != null) {
inputMethodManager.showSoftInput(mEditMessage, InputMethodManager.SHOW_IMPLICIT);
}
}
}
});
messagesView.setAdapter(messageListAdapter); messagesView.setAdapter(messageListAdapter);
registerForContextMenu(messagesView); registerForContextMenu(messagesView);
@ -619,8 +557,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
retryDecryption.setVisible(true); retryDecryption.setVisible(true);
} }
if (relevantForCorrection.getType() == Message.TYPE_TEXT if (relevantForCorrection.getType() == Message.TYPE_TEXT
&& relevantForCorrection.isLastCorrectableMessage() && relevantForCorrection.isLastCorrectableMessage()) {
&& (m.getConversation().getMucOptions().nonanonymous() || m.getConversation().getMode() == Conversation.MODE_SINGLE)) {
correctMessage.setVisible(true); correctMessage.setVisible(true);
} }
if (treatAsFile || (GeoHelper.isGeoUri(m.getBody()))) { if (treatAsFile || (GeoHelper.isGeoUri(m.getBody()))) {
@ -715,13 +652,9 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
shareIntent.putExtra(Intent.EXTRA_TEXT, message.getBody()); shareIntent.putExtra(Intent.EXTRA_TEXT, message.getBody());
shareIntent.setType("text/plain"); shareIntent.setType("text/plain");
} else { } else {
final DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message); shareIntent.putExtra(Intent.EXTRA_STREAM,
try { activity.xmppConnectionService.getFileBackend()
shareIntent.putExtra(Intent.EXTRA_STREAM, FileBackend.getUriForFile(activity, file)); .getJingleFileUri(message));
} catch (SecurityException e) {
Toast.makeText(activity, activity.getString(R.string.no_permission_to_access_x, file.getAbsolutePath()), Toast.LENGTH_SHORT).show();
return;
}
shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
String mime = message.getMimeType(); String mime = message.getMimeType();
if (mime == null) { if (mime == null) {
@ -843,22 +776,16 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
} }
protected void highlightInConference(String nick) { protected void highlightInConference(String nick) {
final Editable editable = mEditMessage.getText(); String oldString = mEditMessage.getText().toString().trim();
String oldString = editable.toString().trim(); if (oldString.isEmpty() || mEditMessage.getSelectionStart() == 0) {
final int pos = mEditMessage.getSelectionStart();
if (oldString.isEmpty() || pos == 0) {
mEditMessage.getText().insert(0, nick + ": "); mEditMessage.getText().insert(0, nick + ": ");
} else { } else {
final char before = editable.charAt(pos - 1); if (mEditMessage.getText().charAt(
final char after = editable.length() > pos ? editable.charAt(pos) : '\0'; mEditMessage.getSelectionStart() - 1) != ' ') {
if (before == '\n') { nick = " " + nick;
editable.insert(pos, nick + ": ");
} else {
editable.insert(pos,(Character.isWhitespace(before)? "" : " ") + nick + (Character.isWhitespace(after) ? "" : " "));
if (Character.isWhitespace(after)) {
mEditMessage.setSelection(mEditMessage.getSelectionStart()+1);
}
} }
mEditMessage.getText().insert(mEditMessage.getSelectionStart(),
nick + " ");
} }
} }
@ -906,7 +833,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
messageListAdapter.updatePreferences(); messageListAdapter.updatePreferences();
this.messagesView.setAdapter(messageListAdapter); this.messagesView.setAdapter(messageListAdapter);
updateMessages(); updateMessages();
this.conversation.messagesLoaded.set(true); this.messagesLoaded = true;
synchronized (this.messageList) { synchronized (this.messageList) {
final Message first = conversation.getFirstUnreadMessage(); final Message first = conversation.getFirstUnreadMessage();
final int bottom = Math.max(0, this.messageList.size() - 1); final int bottom = Math.max(0, this.messageList.size() - 1);
@ -989,15 +916,15 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
private void updateSnackBar(final Conversation conversation) { private void updateSnackBar(final Conversation conversation) {
final Account account = conversation.getAccount(); final Account account = conversation.getAccount();
final Contact contact = conversation.getContact();
final int mode = conversation.getMode(); final int mode = conversation.getMode();
final Contact contact = mode == Conversation.MODE_SINGLE ? conversation.getContact() : null;
if (account.getStatus() == Account.State.DISABLED) { if (account.getStatus() == Account.State.DISABLED) {
showSnackbar(R.string.this_account_is_disabled, R.string.enable, this.mEnableAccountListener); showSnackbar(R.string.this_account_is_disabled, R.string.enable, this.mEnableAccountListener);
} else if (conversation.isBlocked()) { } else if (conversation.isBlocked()) {
showSnackbar(R.string.contact_blocked, R.string.unblock, this.mUnblockClickListener); showSnackbar(R.string.contact_blocked, R.string.unblock, this.mUnblockClickListener);
} else if (contact != null && !contact.showInRoster() && contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { } else if (!contact.showInRoster() && contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
showSnackbar(R.string.contact_added_you, R.string.add_back, this.mAddBackClickListener); showSnackbar(R.string.contact_added_you, R.string.add_back, this.mAddBackClickListener);
} else if (contact != null && contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { } else if (contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
showSnackbar(R.string.contact_asks_for_presence_subscription, R.string.allow, this.mAllowPresenceSubscription); showSnackbar(R.string.contact_asks_for_presence_subscription, R.string.allow, this.mAllowPresenceSubscription);
} else if (mode == Conversation.MODE_MULTI } else if (mode == Conversation.MODE_MULTI
&& !conversation.getMucOptions().online() && !conversation.getMucOptions().online()

View file

@ -35,6 +35,8 @@ import android.widget.TableRow;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import android.util.Log;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@ -43,19 +45,15 @@ import java.util.concurrent.atomic.AtomicInteger;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.R; import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.services.BarcodeProvider;
import eu.siacs.conversations.services.XmppConnectionService.OnCaptchaRequested; import eu.siacs.conversations.services.XmppConnectionService.OnCaptchaRequested;
import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate; import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
import eu.siacs.conversations.ui.adapter.KnownHostsAdapter; import eu.siacs.conversations.ui.adapter.KnownHostsAdapter;
import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.utils.XmppUri;
import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.OnKeyStatusUpdated; import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.XmppConnection.Features; import eu.siacs.conversations.xmpp.XmppConnection.Features;
import eu.siacs.conversations.xmpp.forms.Data; import eu.siacs.conversations.xmpp.forms.Data;
@ -63,23 +61,20 @@ import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid; import eu.siacs.conversations.xmpp.jid.Jid;
import eu.siacs.conversations.xmpp.pep.Avatar; import eu.siacs.conversations.xmpp.pep.Avatar;
public class EditAccountActivity extends OmemoActivity implements OnAccountUpdate, OnUpdateBlocklist, public class EditAccountActivity extends XmppActivity implements OnAccountUpdate,
OnKeyStatusUpdated, OnCaptchaRequested, KeyChainAliasCallback, XmppConnectionService.OnShowErrorToast, XmppConnectionService.OnMamPreferencesFetched { OnKeyStatusUpdated, OnCaptchaRequested, KeyChainAliasCallback, XmppConnectionService.OnShowErrorToast, XmppConnectionService.OnMamPreferencesFetched {
private static final int REQUEST_DATA_SAVER = 0x37af244;
private AutoCompleteTextView mAccountJid; private AutoCompleteTextView mAccountJid;
private EditText mPassword; private EditText mPassword;
private EditText mPasswordConfirm; private EditText mPasswordConfirm;
private CheckBox mRegisterNew; private CheckBox mRegisterNew;
private Button mCancelButton; private Button mCancelButton;
private Button mSaveButton; private Button mSaveButton;
private Button mDisableOsOptimizationsButton; private Button mDisableBatterOptimizations;
private TextView mDisableOsOptimizationsHeadline;
private TextView getmDisableOsOptimizationsBody;
private TableLayout mMoreTable; private TableLayout mMoreTable;
private LinearLayout mStats; private LinearLayout mStats;
private RelativeLayout mOsOptimizations; private RelativeLayout mBatteryOptimizations;
private TextView mServerInfoSm; private TextView mServerInfoSm;
private TextView mServerInfoRosterVersion; private TextView mServerInfoRosterVersion;
private TextView mServerInfoCarbons; private TextView mServerInfoCarbons;
@ -99,6 +94,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
private RelativeLayout mAxolotlFingerprintBox; private RelativeLayout mAxolotlFingerprintBox;
private ImageButton mOtrFingerprintToClipboardButton; private ImageButton mOtrFingerprintToClipboardButton;
private ImageButton mAxolotlFingerprintToClipboardButton; private ImageButton mAxolotlFingerprintToClipboardButton;
private ImageButton mRegenerateAxolotlKeyButton;
private LinearLayout keys; private LinearLayout keys;
private LinearLayout keysCard; private LinearLayout keysCard;
private LinearLayout mNamePort; private LinearLayout mNamePort;
@ -253,7 +249,6 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
private TableRow mPushRow; private TableRow mPushRow;
private String mSavedInstanceAccount; private String mSavedInstanceAccount;
private boolean mSavedInstanceInit = false; private boolean mSavedInstanceInit = false;
private Button mClearDevicesButton;
public void refreshUiReal() { public void refreshUiReal() {
invalidateOptionsMenu(); invalidateOptionsMenu();
@ -376,24 +371,13 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
} }
@Override @Override
public void onActivityResult(int requestCode, int resultCode, Intent data) { protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data); super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_BATTERY_OP || requestCode == REQUEST_DATA_SAVER) { if (requestCode == REQUEST_BATTERY_OP) {
updateAccountInformation(mAccount == null); updateAccountInformation(mAccount == null);
} }
} }
@Override
protected void processFingerprintVerification(XmppUri uri) {
if (mAccount != null && mAccount.getJid().toBareJid().equals(uri.getJid()) && uri.hasFingerprints()) {
if (xmppConnectionService.verifyFingerprints(mAccount,uri.getFingerprints())) {
Toast.makeText(this,R.string.verified_fingerprints,Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(this,R.string.invalid_barcode,Toast.LENGTH_SHORT).show();
}
}
protected void updateSaveButton() { protected void updateSaveButton() {
boolean accountInfoEdited = accountInfoEdited(); boolean accountInfoEdited = accountInfoEdited();
@ -488,10 +472,21 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
this.mAvatar.setOnClickListener(this.mAvatarClickListener); this.mAvatar.setOnClickListener(this.mAvatarClickListener);
this.mRegisterNew = (CheckBox) findViewById(R.id.account_register_new); this.mRegisterNew = (CheckBox) findViewById(R.id.account_register_new);
this.mStats = (LinearLayout) findViewById(R.id.stats); this.mStats = (LinearLayout) findViewById(R.id.stats);
this.mOsOptimizations = (RelativeLayout) findViewById(R.id.os_optimization); this.mBatteryOptimizations = (RelativeLayout) findViewById(R.id.battery_optimization);
this.mDisableOsOptimizationsButton = (Button) findViewById(R.id.os_optimization_disable); this.mDisableBatterOptimizations = (Button) findViewById(R.id.batt_op_disable);
this.mDisableOsOptimizationsHeadline = (TextView) findViewById(R.id.os_optimization_headline); this.mDisableBatterOptimizations.setOnClickListener(new OnClickListener() {
this.getmDisableOsOptimizationsBody = (TextView) findViewById(R.id.os_optimization_body); @Override
public void onClick(View v) {
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
Uri uri = Uri.parse("package:"+getPackageName());
intent.setData(uri);
try {
startActivityForResult(intent, REQUEST_BATTERY_OP);
} catch (ActivityNotFoundException e) {
Toast.makeText(EditAccountActivity.this, R.string.device_does_not_support_battery_op, Toast.LENGTH_SHORT).show();
}
}
});
this.mSessionEst = (TextView) findViewById(R.id.session_est); this.mSessionEst = (TextView) findViewById(R.id.session_est);
this.mServerInfoRosterVersion = (TextView) findViewById(R.id.server_info_roster_version); this.mServerInfoRosterVersion = (TextView) findViewById(R.id.server_info_roster_version);
this.mServerInfoCarbons = (TextView) findViewById(R.id.server_info_carbons); this.mServerInfoCarbons = (TextView) findViewById(R.id.server_info_carbons);
@ -509,19 +504,13 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
this.mAxolotlFingerprint = (TextView) findViewById(R.id.axolotl_fingerprint); this.mAxolotlFingerprint = (TextView) findViewById(R.id.axolotl_fingerprint);
this.mAxolotlFingerprintBox = (RelativeLayout) findViewById(R.id.axolotl_fingerprint_box); this.mAxolotlFingerprintBox = (RelativeLayout) findViewById(R.id.axolotl_fingerprint_box);
this.mAxolotlFingerprintToClipboardButton = (ImageButton) findViewById(R.id.action_copy_axolotl_to_clipboard); this.mAxolotlFingerprintToClipboardButton = (ImageButton) findViewById(R.id.action_copy_axolotl_to_clipboard);
this.mRegenerateAxolotlKeyButton = (ImageButton) findViewById(R.id.action_regenerate_axolotl_key);
this.mOwnFingerprintDesc = (TextView) findViewById(R.id.own_fingerprint_desc); this.mOwnFingerprintDesc = (TextView) findViewById(R.id.own_fingerprint_desc);
this.keysCard = (LinearLayout) findViewById(R.id.other_device_keys_card); this.keysCard = (LinearLayout) findViewById(R.id.other_device_keys_card);
this.keys = (LinearLayout) findViewById(R.id.other_device_keys); this.keys = (LinearLayout) findViewById(R.id.other_device_keys);
this.mNamePort = (LinearLayout) findViewById(R.id.name_port); this.mNamePort = (LinearLayout) findViewById(R.id.name_port);
this.mHostname = (EditText) findViewById(R.id.hostname); this.mHostname = (EditText) findViewById(R.id.hostname);
this.mHostname.addTextChangedListener(mTextWatcher); this.mHostname.addTextChangedListener(mTextWatcher);
this.mClearDevicesButton = (Button) findViewById(R.id.clear_devices);
this.mClearDevicesButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
showWipePepDialog();
}
});
this.mPort = (EditText) findViewById(R.id.port); this.mPort = (EditText) findViewById(R.id.port);
this.mPort.setText("5222"); this.mPort.setText("5222");
this.mPort.addTextChangedListener(mTextWatcher); this.mPort.addTextChangedListener(mTextWatcher);
@ -560,30 +549,31 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
final MenuItem showMoreInfo = menu.findItem(R.id.action_server_info_show_more); final MenuItem showMoreInfo = menu.findItem(R.id.action_server_info_show_more);
final MenuItem changePassword = menu.findItem(R.id.action_change_password_on_server); final MenuItem changePassword = menu.findItem(R.id.action_change_password_on_server);
final MenuItem showPassword = menu.findItem(R.id.action_show_password); final MenuItem showPassword = menu.findItem(R.id.action_show_password);
final MenuItem clearDevices = menu.findItem(R.id.action_clear_devices);
final MenuItem renewCertificate = menu.findItem(R.id.action_renew_certificate); final MenuItem renewCertificate = menu.findItem(R.id.action_renew_certificate);
final MenuItem mamPrefs = menu.findItem(R.id.action_mam_prefs); final MenuItem mamPrefs = menu.findItem(R.id.action_mam_prefs);
final MenuItem changePresence = menu.findItem(R.id.action_change_presence); final MenuItem changePresence = menu.findItem(R.id.action_change_presence);
final MenuItem share = menu.findItem(R.id.action_share);
renewCertificate.setVisible(mAccount != null && mAccount.getPrivateKeyAlias() != null); renewCertificate.setVisible(mAccount != null && mAccount.getPrivateKeyAlias() != null);
share.setVisible(mAccount != null && !mInitMode);
if (mAccount != null && mAccount.isOnlineAndConnected()) { if (mAccount != null && mAccount.isOnlineAndConnected()) {
if (!mAccount.getXmppConnection().getFeatures().blocking()) { if (!mAccount.getXmppConnection().getFeatures().blocking()) {
showBlocklist.setVisible(false); showBlocklist.setVisible(false);
} else {
showBlocklist.setEnabled(mAccount.getBlocklist().size() > 0);
} }
if (!mAccount.getXmppConnection().getFeatures().register()) { if (!mAccount.getXmppConnection().getFeatures().register()) {
changePassword.setVisible(false); changePassword.setVisible(false);
} }
mamPrefs.setVisible(mAccount.getXmppConnection().getFeatures().mam()); mamPrefs.setVisible(mAccount.getXmppConnection().getFeatures().mam());
Set<Integer> otherDevices = mAccount.getAxolotlService().getOwnDeviceIds();
if (otherDevices == null || otherDevices.isEmpty() || !Config.supportOmemo()) {
clearDevices.setVisible(false);
}
changePresence.setVisible(manuallyChangePresence()); changePresence.setVisible(manuallyChangePresence());
} else { } else {
showQrCode.setVisible(false); showQrCode.setVisible(false);
showBlocklist.setVisible(false); showBlocklist.setVisible(false);
showMoreInfo.setVisible(false); showMoreInfo.setVisible(false);
changePassword.setVisible(false); changePassword.setVisible(false);
clearDevices.setVisible(false);
mamPrefs.setVisible(false); mamPrefs.setVisible(false);
changePresence.setVisible(false); changePresence.setVisible(false);
} }
@ -655,6 +645,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
super.onSaveInstanceState(savedInstanceState); super.onSaveInstanceState(savedInstanceState);
} }
@Override
protected void onBackendConnected() { protected void onBackendConnected() {
boolean init = true; boolean init = true;
if (mSavedInstanceAccount != null) { if (mSavedInstanceAccount != null) {
@ -679,10 +670,6 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
this.mPassword.requestFocus(); this.mPassword.requestFocus();
} }
} }
if (mPendingFingerprintVerificationUri != null) {
processFingerprintVerification(mPendingFingerprintVerificationUri);
mPendingFingerprintVerificationUri = null;
}
updateAccountInformation(init); updateAccountInformation(init);
} }
@ -723,21 +710,15 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
case R.id.action_server_info_show_more: case R.id.action_server_info_show_more:
changeMoreTableVisibility(!item.isChecked()); changeMoreTableVisibility(!item.isChecked());
break; break;
case R.id.action_share_barcode:
shareBarcode();
break;
case R.id.action_share_http:
shareLink(true);
break;
case R.id.action_share_uri:
shareLink(false);
break;
case R.id.action_change_password_on_server: case R.id.action_change_password_on_server:
gotoChangePassword(null); gotoChangePassword(null);
break; break;
case R.id.action_mam_prefs: case R.id.action_mam_prefs:
editMamPrefs(); editMamPrefs();
break; break;
case R.id.action_clear_devices:
showWipePepDialog();
break;
case R.id.action_renew_certificate: case R.id.action_renew_certificate:
renewCertificate(); renewCertificate();
break; break;
@ -751,27 +732,6 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
private void shareLink(boolean http) {
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
String text;
if (http) {
text = mAccount.getShareableLink();
} else {
text = mAccount.getShareableUri();
}
intent.putExtra(Intent.EXTRA_TEXT,text);
startActivity(Intent.createChooser(intent, getText(R.string.share_with)));
}
private void shareBarcode() {
Intent intent = new Intent(Intent.ACTION_SEND);
intent.putExtra(Intent.EXTRA_STREAM,BarcodeProvider.getUriForAccount(this,mAccount));
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setType("image/png");
startActivity(Intent.createChooser(intent, getText(R.string.share_with)));
}
private void changeMoreTableVisibility(boolean visible) { private void changeMoreTableVisibility(boolean visible) {
mMoreTable.setVisibility(visible ? View.VISIBLE : View.GONE); mMoreTable.setVisibility(visible ? View.VISIBLE : View.GONE);
} }
@ -836,9 +796,8 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
if (this.mAccount.isOnlineAndConnected() && !this.mFetchingAvatar) { if (this.mAccount.isOnlineAndConnected() && !this.mFetchingAvatar) {
Features features = this.mAccount.getXmppConnection().getFeatures(); Features features = this.mAccount.getXmppConnection().getFeatures();
this.mStats.setVisibility(View.VISIBLE); this.mStats.setVisibility(View.VISIBLE);
boolean showBatteryWarning = !xmppConnectionService.getPushManagementService().availableAndUseful(mAccount) && isOptimizingBattery(); boolean showOptimizingWarning = !xmppConnectionService.getPushManagementService().available(mAccount) && isOptimizingBattery();
boolean showDataSaverWarning = isAffectedByDataSaver(); this.mBatteryOptimizations.setVisibility(showOptimizingWarning ? View.VISIBLE : View.GONE);
showOsOptimizationWarning(showBatteryWarning,showDataSaverWarning);
this.mSessionEst.setText(UIHelper.readableTimeDifferenceFull(this, this.mAccount.getXmppConnection() this.mSessionEst.setText(UIHelper.readableTimeDifferenceFull(this, this.mAccount.getXmppConnection()
.getLastSessionEstablished())); .getLastSessionEstablished()));
if (features.rosterVersioning()) { if (features.rosterVersioning()) {
@ -907,7 +866,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
@Override @Override
public void onClick(final View v) { public void onClick(final View v) {
if (copyTextToClipboard(CryptoHelper.prettifyFingerprint(otrFingerprint), R.string.otr_fingerprint)) { if (copyTextToClipboard(otrFingerprint, R.string.otr_fingerprint)) {
Toast.makeText( Toast.makeText(
EditAccountActivity.this, EditAccountActivity.this,
R.string.toast_message_otr_fingerprint, R.string.toast_message_otr_fingerprint,
@ -934,29 +893,41 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
@Override @Override
public void onClick(final View v) { public void onClick(final View v) {
copyOmemoFingerprint(ownAxolotlFingerprint);
if (copyTextToClipboard(ownAxolotlFingerprint.substring(2), R.string.omemo_fingerprint)) {
Toast.makeText(
EditAccountActivity.this,
R.string.toast_message_omemo_fingerprint,
Toast.LENGTH_SHORT).show();
}
} }
}); });
if (Config.SHOW_REGENERATE_AXOLOTL_KEYS_BUTTON) {
this.mRegenerateAxolotlKeyButton
.setVisibility(View.VISIBLE);
this.mRegenerateAxolotlKeyButton
.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(final View v) {
showRegenerateAxolotlKeyDialog();
}
});
}
} else { } else {
this.mAxolotlFingerprintBox.setVisibility(View.GONE); this.mAxolotlFingerprintBox.setVisibility(View.GONE);
} }
boolean hasKeys = false; boolean hasKeys = false;
keys.removeAllViews(); keys.removeAllViews();
for(XmppAxolotlSession session : mAccount.getAxolotlService().findOwnSessions()) { for (final String fingerprint : mAccount.getAxolotlService().getFingerprintsForOwnSessions()) {
if (!session.getTrust().isCompromised()) { if (ownAxolotlFingerprint.equals(fingerprint)) {
boolean highlight = session.getFingerprint().equals(messageFingerprint); continue;
addFingerprintRow(keys,session,highlight);
hasKeys = true;
} }
boolean highlight = fingerprint.equals(messageFingerprint);
hasKeys |= addFingerprintRow(keys, mAccount, fingerprint, highlight, null);
} }
if (hasKeys && Config.supportOmemo()) { if (hasKeys && Config.supportOmemo()) {
keysCard.setVisibility(View.VISIBLE); keysCard.setVisibility(View.VISIBLE);
Set<Integer> otherDevices = mAccount.getAxolotlService().getOwnDeviceIds();
if (otherDevices == null || otherDevices.isEmpty()) {
mClearDevicesButton.setVisibility(View.GONE);
} else {
mClearDevicesButton.setVisibility(View.VISIBLE);
}
} else { } else {
keysCard.setVisibility(View.GONE); keysCard.setVisibility(View.GONE);
} }
@ -985,43 +956,20 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
} }
} }
private void showOsOptimizationWarning(boolean showBatteryWarning, boolean showDataSaverWarning) { public void showRegenerateAxolotlKeyDialog() {
this.mOsOptimizations.setVisibility(showBatteryWarning || showDataSaverWarning ? View.VISIBLE : View.GONE); Builder builder = new Builder(this);
if (showDataSaverWarning) { builder.setTitle("Regenerate Key");
this.mDisableOsOptimizationsHeadline.setText(R.string.data_saver_enabled); builder.setIconAttribute(android.R.attr.alertDialogIcon);
this.getmDisableOsOptimizationsBody.setText(R.string.data_saver_enabled_explained); builder.setMessage("Are you sure you want to regenerate your Identity Key? (This will also wipe all established sessions and contact Identity Keys)");
this.mDisableOsOptimizationsButton.setText(R.string.allow); builder.setNegativeButton(getString(R.string.cancel), null);
this.mDisableOsOptimizationsButton.setOnClickListener(new OnClickListener() { builder.setPositiveButton("Yes",
@Override new DialogInterface.OnClickListener() {
public void onClick(View v) { @Override
Intent intent = new Intent(Settings.ACTION_IGNORE_BACKGROUND_DATA_RESTRICTIONS_SETTINGS); public void onClick(DialogInterface dialog, int which) {
Uri uri = Uri.parse("package:"+getPackageName()); mAccount.getAxolotlService().regenerateKeys(false);
intent.setData(uri);
try {
startActivityForResult(intent, REQUEST_DATA_SAVER);
} catch (ActivityNotFoundException e) {
Toast.makeText(EditAccountActivity.this, R.string.device_does_not_support_data_saver, Toast.LENGTH_SHORT).show();
} }
} });
}); builder.create().show();
} else if (showBatteryWarning) {
this.mDisableOsOptimizationsButton.setText(R.string.disable);
this.mDisableOsOptimizationsHeadline.setText(R.string.battery_optimizations_enabled);
this.getmDisableOsOptimizationsBody.setText(R.string.battery_optimizations_enabled_explained);
this.mDisableOsOptimizationsButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
Uri uri = Uri.parse("package:"+getPackageName());
intent.setData(uri);
try {
startActivityForResult(intent, REQUEST_BATTERY_OP);
} catch (ActivityNotFoundException e) {
Toast.makeText(EditAccountActivity.this, R.string.device_does_not_support_battery_op, Toast.LENGTH_SHORT).show();
}
}
});
}
} }
public void showWipePepDialog() { public void showWipePepDialog() {
@ -1171,9 +1119,4 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
} }
}); });
} }
@Override
public void OnUpdateBlocklist(Status status) {
refreshUi();
}
} }

View file

@ -1,33 +1,19 @@
package eu.siacs.conversations.ui; package eu.siacs.conversations.ui;
import android.support.v13.view.inputmethod.EditorInfoCompat;
import android.support.v13.view.inputmethod.InputConnectionCompat;
import android.support.v13.view.inputmethod.InputContentInfoCompat;
import android.content.Context; import android.content.Context;
import android.os.Build; import android.os.Build;
import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.text.Editable; import android.text.Editable;
import android.text.InputFilter; import android.text.InputFilter;
import android.text.Spanned; import android.text.Spanned;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.widget.EditText; import android.widget.EditText;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
public class EditMessage extends EditText { public class EditMessage extends EditText {
public interface OnCommitContentListener {
boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags, Bundle opts, String[] mimeTypes);
}
private OnCommitContentListener mCommitContentListener = null;
private String[] mimeTypes = null;
public EditMessage(Context context, AttributeSet attrs) { public EditMessage(Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
} }
@ -139,27 +125,4 @@ public class EditMessage extends EditText {
return super.onTextContextMenuItem(id); return super.onTextContextMenuItem(id);
} }
} }
public void setRichContentListener(String[] mimeTypes, OnCommitContentListener listener) {
this.mimeTypes = mimeTypes;
this.mCommitContentListener = listener;
}
@Override
public InputConnection onCreateInputConnection(EditorInfo editorInfo) {
final InputConnection ic = super.onCreateInputConnection(editorInfo);
if (mimeTypes != null && mCommitContentListener != null) {
EditorInfoCompat.setContentMimeTypes(editorInfo, mimeTypes);
return InputConnectionCompat.createWrapper(ic, editorInfo, new InputConnectionCompat.OnCommitContentListener() {
@Override
public boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags, Bundle opts) {
return EditMessage.this.mCommitContentListener.onCommitContent(inputContentInfo, flags, opts, mimeTypes);
}
});
}
else {
return ic;
}
}
} }

View file

@ -1,288 +0,0 @@
package eu.siacs.conversations.ui;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.view.ContextMenu;
import android.view.MenuItem;
import android.view.View;
import android.widget.CompoundButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.google.zxing.integration.android.IntentIntegrator;
import com.google.zxing.integration.android.IntentResult;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.ui.widget.Switch;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.XmppUri;
public abstract class OmemoActivity extends XmppActivity {
private Account mSelectedAccount;
private String mSelectedFingerprint;
protected XmppUri mPendingFingerprintVerificationUri = null;
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu,v,menuInfo);
Object account = v.getTag(R.id.TAG_ACCOUNT);
Object fingerprint = v.getTag(R.id.TAG_FINGERPRINT);
Object fingerprintStatus = v.getTag(R.id.TAG_FINGERPRINT_STATUS);;
if (account != null
&& fingerprint != null
&& account instanceof Account
&& fingerprintStatus != null
&& fingerprint instanceof String
&& fingerprintStatus instanceof FingerprintStatus) {
getMenuInflater().inflate(R.menu.omemo_key_context, menu);
MenuItem distrust = menu.findItem(R.id.distrust_key);
MenuItem verifyScan = menu.findItem(R.id.verify_scan);
if (this instanceof TrustKeysActivity) {
distrust.setVisible(false);
verifyScan.setVisible(false);
} else {
FingerprintStatus status = (FingerprintStatus) fingerprintStatus;
if (!status.isActive() || status.isVerified()) {
verifyScan.setVisible(false);
}
distrust.setVisible(status.isVerified());
}
this.mSelectedAccount = (Account) account;
this.mSelectedFingerprint = (String) fingerprint;
}
}
@Override
public boolean onContextItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.distrust_key:
showPurgeKeyDialog(mSelectedAccount,mSelectedFingerprint);
break;
case R.id.copy_omemo_key:
copyOmemoFingerprint(mSelectedFingerprint);
break;
case R.id.verify_scan:
new IntentIntegrator(this).initiateScan(Arrays.asList("AZTEC","QR_CODE"));
break;
}
return true;
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent intent) {
IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent);
if (scanResult != null && scanResult.getFormatName() != null) {
String data = scanResult.getContents();
XmppUri uri = new XmppUri(data);
if (xmppConnectionServiceBound) {
processFingerprintVerification(uri);
} else {
this.mPendingFingerprintVerificationUri =uri;
}
}
}
protected abstract void processFingerprintVerification(XmppUri uri);
protected void copyOmemoFingerprint(String fingerprint) {
if (copyTextToClipboard(CryptoHelper.prettifyFingerprint(fingerprint.substring(2)), R.string.omemo_fingerprint)) {
Toast.makeText(
this,
R.string.toast_message_omemo_fingerprint,
Toast.LENGTH_SHORT).show();
}
}
protected void addFingerprintRow(LinearLayout keys, final XmppAxolotlSession session, boolean highlight) {
final Account account = session.getAccount();
final String fingerprint = session.getFingerprint();
addFingerprintRowWithListeners(keys,
session.getAccount(),
session.getFingerprint(),
highlight,
session.getTrust(),
true,
true,
new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
account.getAxolotlService().setFingerprintTrust(fingerprint, FingerprintStatus.createActive(isChecked));
}
});
}
protected void addFingerprintRowWithListeners(LinearLayout keys, final Account account,
final String fingerprint,
boolean highlight,
FingerprintStatus status,
boolean showTag,
boolean undecidedNeedEnablement,
CompoundButton.OnCheckedChangeListener
onCheckedChangeListener) {
View view = getLayoutInflater().inflate(R.layout.contact_key, keys, false);
TextView key = (TextView) view.findViewById(R.id.key);
TextView keyType = (TextView) view.findViewById(R.id.key_type);
if (Config.X509_VERIFICATION && status.getTrust() == FingerprintStatus.Trust.VERIFIED_X509) {
View.OnClickListener listener = new View.OnClickListener() {
@Override
public void onClick(View v) {
showX509Certificate(account,fingerprint);
}
};
key.setOnClickListener(listener);
keyType.setOnClickListener(listener);
}
Switch trustToggle = (Switch) view.findViewById(R.id.tgl_trust);
ImageView verifiedFingerprintSymbol = (ImageView) view.findViewById(R.id.verified_fingerprint);
trustToggle.setVisibility(View.VISIBLE);
registerForContextMenu(view);
view.setTag(R.id.TAG_ACCOUNT,account);
view.setTag(R.id.TAG_FINGERPRINT,fingerprint);
view.setTag(R.id.TAG_FINGERPRINT_STATUS,status);
boolean x509 = Config.X509_VERIFICATION && status.getTrust() == FingerprintStatus.Trust.VERIFIED_X509;
final View.OnClickListener toast;
trustToggle.setChecked(status.isTrusted(), false);
if (status.isActive()){
key.setTextColor(getPrimaryTextColor());
keyType.setTextColor(getSecondaryTextColor());
if (status.isVerified()) {
verifiedFingerprintSymbol.setVisibility(View.VISIBLE);
verifiedFingerprintSymbol.setAlpha(1.0f);
trustToggle.setVisibility(View.GONE);
verifiedFingerprintSymbol.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
replaceToast(getString(R.string.this_device_has_been_verified), false);
}
});
toast = null;
} else {
verifiedFingerprintSymbol.setVisibility(View.GONE);
trustToggle.setVisibility(View.VISIBLE);
trustToggle.setOnCheckedChangeListener(onCheckedChangeListener);
if (status.getTrust() == FingerprintStatus.Trust.UNDECIDED && undecidedNeedEnablement) {
trustToggle.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
account.getAxolotlService().setFingerprintTrust(fingerprint,FingerprintStatus.createActive(false));
v.setEnabled(true);
v.setOnClickListener(null);
}
});
trustToggle.setEnabled(false);
} else {
trustToggle.setOnClickListener(null);
trustToggle.setEnabled(true);
}
toast = new View.OnClickListener() {
@Override
public void onClick(View v) {
hideToast();
}
};
}
} else {
key.setTextColor(getTertiaryTextColor());
keyType.setTextColor(getTertiaryTextColor());
toast = new View.OnClickListener() {
@Override
public void onClick(View v) {
replaceToast(getString(R.string.this_device_is_no_longer_in_use), false);
}
};
if (status.isVerified()) {
trustToggle.setVisibility(View.GONE);
verifiedFingerprintSymbol.setVisibility(View.VISIBLE);
verifiedFingerprintSymbol.setAlpha(0.4368f);
verifiedFingerprintSymbol.setOnClickListener(toast);
} else {
trustToggle.setVisibility(View.VISIBLE);
verifiedFingerprintSymbol.setVisibility(View.GONE);
trustToggle.setOnClickListener(null);
trustToggle.setEnabled(false);
trustToggle.setOnClickListener(toast);
}
}
view.setOnClickListener(toast);
key.setOnClickListener(toast);
keyType.setOnClickListener(toast);
if (showTag) {
keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509 : R.string.omemo_fingerprint));
} else {
keyType.setVisibility(View.GONE);
}
if (highlight) {
keyType.setTextColor(getResources().getColor(R.color.accent));
keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509_selected_message : R.string.omemo_fingerprint_selected_message));
} else {
keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509 : R.string.omemo_fingerprint));
}
key.setText(CryptoHelper.prettifyFingerprint(fingerprint.substring(2)));
keys.addView(view);
}
public void showPurgeKeyDialog(final Account account, final String fingerprint) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.distrust_omemo_key);
builder.setMessage(R.string.distrust_omemo_key_text);
builder.setNegativeButton(getString(R.string.cancel), null);
builder.setPositiveButton(R.string.confirm,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
account.getAxolotlService().distrustFingerprint(fingerprint);
refreshUi();
}
});
builder.create().show();
}
private void showX509Certificate(Account account, String fingerprint) {
X509Certificate x509Certificate = account.getAxolotlService().getFingerprintCertificate(fingerprint);
if (x509Certificate != null) {
showCertificateInformationDialog(CryptoHelper.extractCertificateInformation(x509Certificate));
} else {
Toast.makeText(this,R.string.certificate_not_found, Toast.LENGTH_SHORT).show();
}
}
private void showCertificateInformationDialog(Bundle bundle) {
View view = getLayoutInflater().inflate(R.layout.certificate_information, null);
final String not_available = getString(R.string.certicate_info_not_available);
TextView subject_cn = (TextView) view.findViewById(R.id.subject_cn);
TextView subject_o = (TextView) view.findViewById(R.id.subject_o);
TextView issuer_cn = (TextView) view.findViewById(R.id.issuer_cn);
TextView issuer_o = (TextView) view.findViewById(R.id.issuer_o);
TextView sha1 = (TextView) view.findViewById(R.id.sha1);
subject_cn.setText(bundle.getString("subject_cn", not_available));
subject_o.setText(bundle.getString("subject_o", not_available));
issuer_cn.setText(bundle.getString("issuer_cn", not_available));
issuer_o.setText(bundle.getString("issuer_o", not_available));
sha1.setText(bundle.getString("sha1", not_available));
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.certificate_information);
builder.setView(view);
builder.setPositiveButton(R.string.ok, null);
builder.create().show();
}
}

View file

@ -7,7 +7,6 @@ import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.preference.ListPreference; import android.preference.ListPreference;
@ -15,11 +14,8 @@ import android.preference.Preference;
import android.preference.PreferenceCategory; import android.preference.PreferenceCategory;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.preference.PreferenceScreen; import android.preference.PreferenceScreen;
import android.provider.MediaStore;
import android.util.Log;
import android.widget.Toast; import android.widget.Toast;
import java.io.File;
import java.security.KeyStoreException; import java.security.KeyStoreException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -39,13 +35,6 @@ import eu.siacs.conversations.xmpp.jid.Jid;
public class SettingsActivity extends XmppActivity implements public class SettingsActivity extends XmppActivity implements
OnSharedPreferenceChangeListener { OnSharedPreferenceChangeListener {
public static final String KEEP_FOREGROUND_SERVICE = "enable_foreground_service";
public static final String AWAY_WHEN_SCREEN_IS_OFF = "away_when_screen_off";
public static final String TREAT_VIBRATE_AS_SILENT = "treat_vibrate_as_silent";
public static final String MANUALLY_CHANGE_PRESENCE = "manually_change_presence";
public static final String BLIND_TRUST_BEFORE_VERIFICATION = "btbv";
public static final String AUTOMATIC_MESSAGE_DELETION = "automatic_message_deletion";
public static final int REQUEST_WRITE_LOGS = 0xbf8701; public static final int REQUEST_WRITE_LOGS = 0xbf8701;
private SettingsFragment mSettingsFragment; private SettingsFragment mSettingsFragment;
@ -94,27 +83,6 @@ public class SettingsActivity extends XmppActivity implements
} }
} }
boolean removeLocation = new Intent("eu.siacs.conversations.location.request").resolveActivity(getPackageManager()) == null;
boolean removeVoice = new Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION).resolveActivity(getPackageManager()) == null;
ListPreference quickAction = (ListPreference) mSettingsFragment.findPreference("quick_action");
if (quickAction != null && (removeLocation || removeVoice)) {
ArrayList<CharSequence> entries = new ArrayList<>(Arrays.asList(quickAction.getEntries()));
ArrayList<CharSequence> entryValues = new ArrayList<>(Arrays.asList(quickAction.getEntryValues()));
int index = entryValues.indexOf("location");
if (index > 0 && removeLocation) {
entries.remove(index);
entryValues.remove(index);
}
index = entryValues.indexOf("voice");
if (index > 0 && removeVoice) {
entries.remove(index);
entryValues.remove(index);
}
quickAction.setEntries(entries.toArray(new CharSequence[entries.size()]));
quickAction.setEntryValues(entryValues.toArray(new CharSequence[entryValues.size()]));
}
final Preference removeCertsPreference = mSettingsFragment.findPreference("remove_trusted_certificates"); final Preference removeCertsPreference = mSettingsFragment.findPreference("remove_trusted_certificates");
removeCertsPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { removeCertsPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override @Override
@ -188,26 +156,6 @@ public class SettingsActivity extends XmppActivity implements
} }
}); });
if (Config.ONLY_INTERNAL_STORAGE) {
final Preference cleanCachePreference = mSettingsFragment.findPreference("clean_cache");
cleanCachePreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
cleanCache();
return true;
}
});
final Preference cleanPrivateStoragePreference = mSettingsFragment.findPreference("clean_private_storage");
cleanPrivateStoragePreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
cleanPrivateStorage();
return true;
}
});
}
final Preference deleteOmemoPreference = mSettingsFragment.findPreference("delete_omemo_identities"); final Preference deleteOmemoPreference = mSettingsFragment.findPreference("delete_omemo_identities");
deleteOmemoPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { deleteOmemoPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override @Override
@ -218,57 +166,6 @@ public class SettingsActivity extends XmppActivity implements
}); });
} }
private void cleanCache() {
Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivity(intent);
}
private void cleanPrivateStorage() {
cleanPrivatePictures();
cleanPrivateFiles();
}
private void cleanPrivatePictures() {
try {
File dir = new File(getFilesDir().getAbsolutePath(), "/Pictures/");
File[] array = dir.listFiles();
if (array != null) {
for (int b = 0; b < array.length; b++) {
String name = array[b].getName().toLowerCase();
if (name.equals(".nomedia")) {
continue;
}
if (array[b].isFile()) {
array[b].delete();
}
}
}
} catch (Throwable e) {
Log.e("CleanCache", e.toString());
}
}
private void cleanPrivateFiles() {
try {
File dir = new File(getFilesDir().getAbsolutePath(), "/Files/");
File[] array = dir.listFiles();
if (array != null) {
for (int b = 0; b < array.length; b++) {
String name = array[b].getName().toLowerCase();
if (name.equals(".nomedia")) {
continue;
}
if (array[b].isFile()) {
array[b].delete();
}
}
}
} catch (Throwable e) {
Log.e("CleanCache", e.toString());
}
}
private void deleteOmemoIdentities() { private void deleteOmemoIdentities() {
AlertDialog.Builder builder = new AlertDialog.Builder(this); AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.pref_delete_omemo_identities); builder.setTitle(R.string.pref_delete_omemo_identities);
@ -330,10 +227,10 @@ public class SettingsActivity extends XmppActivity implements
final List<String> resendPresence = Arrays.asList( final List<String> resendPresence = Arrays.asList(
"confirm_messages", "confirm_messages",
"xa_on_silent_mode", "xa_on_silent_mode",
AWAY_WHEN_SCREEN_IS_OFF, "away_when_screen_off",
"allow_message_correction", "allow_message_correction",
TREAT_VIBRATE_AS_SILENT, "treat_vibrate_as_silent",
MANUALLY_CHANGE_PRESENCE, "manually_change_presence",
"last_activity"); "last_activity");
if (name.equals("resource")) { if (name.equals("resource")) {
String resource = preferences.getString("resource", "mobile") String resource = preferences.getString("resource", "mobile")
@ -351,18 +248,15 @@ public class SettingsActivity extends XmppActivity implements
} }
} }
} }
} else if (name.equals(KEEP_FOREGROUND_SERVICE)) { } else if (name.equals("keep_foreground_service")) {
boolean foreground_service = preferences.getBoolean(KEEP_FOREGROUND_SERVICE,false);
if (!foreground_service) {
xmppConnectionService.clearStartTimeCounter();
}
xmppConnectionService.toggleForegroundService(); xmppConnectionService.toggleForegroundService();
} else if (resendPresence.contains(name)) { } else if (resendPresence.contains(name)) {
if (xmppConnectionServiceBound) { if (xmppConnectionServiceBound) {
if (name.equals(AWAY_WHEN_SCREEN_IS_OFF) || name.equals(MANUALLY_CHANGE_PRESENCE)) { if (name.equals("away_when_screen_off")
|| name.equals("manually_change_presence")) {
xmppConnectionService.toggleScreenEventReceiver(); xmppConnectionService.toggleScreenEventReceiver();
} }
if (name.equals(MANUALLY_CHANGE_PRESENCE) && !noAccountUsesPgp()) { if (name.equals("manually_change_presence") && !noAccountUsesPgp()) {
Toast.makeText(this, R.string.republish_pgp_keys, Toast.LENGTH_LONG).show(); Toast.makeText(this, R.string.republish_pgp_keys, Toast.LENGTH_LONG).show();
} }
xmppConnectionService.refreshAllPresences(); xmppConnectionService.refreshAllPresences();
@ -372,8 +266,6 @@ public class SettingsActivity extends XmppActivity implements
reconnectAccounts(); reconnectAccounts();
} else if (name.equals("use_tor")) { } else if (name.equals("use_tor")) {
reconnectAccounts(); reconnectAccounts();
} else if (name.equals(AUTOMATIC_MESSAGE_DELETION)) {
xmppConnectionService.expireOldMessages(true);
} }
} }

View file

@ -3,7 +3,6 @@ package eu.siacs.conversations.ui;
import android.app.Dialog; import android.app.Dialog;
import android.os.Bundle; import android.os.Bundle;
import android.preference.Preference; import android.preference.Preference;
import android.preference.PreferenceCategory;
import android.preference.PreferenceFragment; import android.preference.PreferenceFragment;
import android.preference.PreferenceScreen; import android.preference.PreferenceScreen;
import android.view.View; import android.view.View;
@ -12,7 +11,6 @@ import android.view.ViewParent;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R; import eu.siacs.conversations.R;
public class SettingsFragment extends PreferenceFragment { public class SettingsFragment extends PreferenceFragment {
@ -54,16 +52,6 @@ public class SettingsFragment extends PreferenceFragment {
// Load the preferences from an XML resource // Load the preferences from an XML resource
addPreferencesFromResource(R.xml.preferences); addPreferencesFromResource(R.xml.preferences);
// Remove from standard preferences if the flag ONLY_INTERNAL_STORAGE is not true
if (!Config.ONLY_INTERNAL_STORAGE) {
PreferenceCategory mCategory = (PreferenceCategory) findPreference("security_options");
Preference mPref1 = findPreference("clean_cache");
Preference mPref2 = findPreference("clean_private_storage");
mCategory.removePreference(mPref1);
mCategory.removePreference(mPref2);
}
} }
@Override @Override

View file

@ -59,17 +59,7 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer
private Toast mToast; private Toast mToast;
private AtomicInteger attachmentCounter = new AtomicInteger(0); private AtomicInteger attachmentCounter = new AtomicInteger(0);
private UiInformableCallback<Message> attachFileCallback = new UiInformableCallback<Message>() { private UiCallback<Message> attachFileCallback = new UiCallback<Message>() {
@Override
public void inform(final String text) {
runOnUiThread(new Runnable() {
@Override
public void run() {
replaceToast(text);
}
});
}
@Override @Override
public void userInputRequried(PendingIntent pi, Message object) { public void userInputRequried(PendingIntent pi, Message object) {
@ -303,7 +293,8 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer
} else { } else {
replaceToast(getString(R.string.preparing_file)); replaceToast(getString(R.string.preparing_file));
ShareWithActivity.this.xmppConnectionService ShareWithActivity.this.xmppConnectionService
.attachFileToConversation(conversation, share.uris.get(0), attachFileCallback); .attachFileToConversation(conversation, share.uris.get(0),
attachFileCallback);
} }
} }
}; };
@ -319,62 +310,16 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer
} else { } else {
if (mReturnToPrevious && this.share.text != null && !this.share.text.isEmpty() ) { if (mReturnToPrevious && this.share.text != null && !this.share.text.isEmpty() ) {
final OnPresenceSelected callback = new OnPresenceSelected() { final OnPresenceSelected callback = new OnPresenceSelected() {
@Override
private void finishAndSend(Message message) { public void onPresenceSelected() {
Message message = new Message(conversation,share.text, conversation.getNextEncryption());
if (conversation.getNextEncryption() == Message.ENCRYPTION_OTR) {
message.setCounterpart(conversation.getNextCounterpart());
}
xmppConnectionService.sendMessage(message); xmppConnectionService.sendMessage(message);
replaceToast(getString(R.string.shared_text_with_x, conversation.getName())); replaceToast(getString(R.string.shared_text_with_x, conversation.getName()));
finish(); finish();
} }
private UiCallback<Message> messageEncryptionCallback = new UiCallback<Message>() {
@Override
public void success(final Message message) {
message.setEncryption(Message.ENCRYPTION_DECRYPTED);
runOnUiThread(new Runnable() {
@Override
public void run() {
finishAndSend(message);
}
});
}
@Override
public void error(final int errorCode, Message object) {
runOnUiThread(new Runnable() {
@Override
public void run() {
replaceToast(getString(errorCode));
finish();
}
});
}
@Override
public void userInputRequried(PendingIntent pi, Message object) {
finish();
}
};
@Override
public void onPresenceSelected() {
final int encryption = conversation.getNextEncryption();
Message message = new Message(conversation,share.text, encryption);
Log.d(Config.LOGTAG,"on presence selected encrpytion="+encryption);
if (encryption == Message.ENCRYPTION_PGP) {
replaceToast(getString(R.string.encrypting_message));
xmppConnectionService.getPgpEngine().encrypt(message,messageEncryptionCallback);
return;
}
if (encryption == Message.ENCRYPTION_OTR) {
message.setCounterpart(conversation.getNextCounterpart());
}
finishAndSend(message);
}
}; };
if (conversation.getNextEncryption() == Message.ENCRYPTION_OTR) { if (conversation.getNextEncryption() == Message.ENCRYPTION_OTR) {
selectPresence(conversation, callback); selectPresence(conversation, callback);

View file

@ -13,7 +13,6 @@ import android.app.FragmentManager;
import android.app.FragmentTransaction; import android.app.FragmentTransaction;
import android.app.ListFragment; import android.app.ListFragment;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.ActivityNotFoundException;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener; import android.content.DialogInterface.OnClickListener;
@ -26,13 +25,12 @@ import android.nfc.NfcAdapter;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Parcelable; import android.os.Parcelable;
import android.support.v13.app.FragmentPagerAdapter;
import android.support.v4.view.PagerAdapter; import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager; import android.support.v4.view.ViewPager;
import android.text.Editable; import android.text.Editable;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextWatcher; import android.text.TextWatcher;
import android.text.style.TypefaceSpan; import android.util.Log;
import android.util.Pair; import android.util.Pair;
import android.view.ContextMenu; import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo; import android.view.ContextMenu.ContextMenuInfo;
@ -67,6 +65,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.R; import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Blockable;
import eu.siacs.conversations.entities.Bookmark; import eu.siacs.conversations.entities.Bookmark;
import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Conversation;
@ -325,23 +324,6 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
openConversationsForBookmark(bookmark); openConversationsForBookmark(bookmark);
} }
protected void shareBookmarkUri() {
shareBookmarkUri(conference_context_id);
}
protected void shareBookmarkUri(int position) {
Bookmark bookmark = (Bookmark) conferences.get(position);
Intent shareIntent = new Intent();
shareIntent.setAction(Intent.ACTION_SEND);
shareIntent.putExtra(Intent.EXTRA_TEXT, "xmpp:"+bookmark.getJid().toBareJid().toString()+"?join");
shareIntent.setType("text/plain");
try {
startActivity(Intent.createChooser(shareIntent, getText(R.string.share_uri_with)));
} catch (ActivityNotFoundException e) {
Toast.makeText(this, R.string.no_application_to_share_uri, Toast.LENGTH_SHORT).show();
}
}
protected void openConversationsForBookmark(Bookmark bookmark) { protected void openConversationsForBookmark(Bookmark bookmark) {
Jid jid = bookmark.getJid(); Jid jid = bookmark.getJid();
if (jid == null) { if (jid == null) {
@ -415,11 +397,11 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
} }
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
protected void showCreateContactDialog(final String prefilledJid, final Invite invite) { protected void showCreateContactDialog(final String prefilledJid, final String fingerprint) {
EnterJidDialog dialog = new EnterJidDialog( EnterJidDialog dialog = new EnterJidDialog(
this, mKnownHosts, mActivatedAccounts, this, mKnownHosts, mActivatedAccounts,
getString(R.string.create_contact), getString(R.string.create), getString(R.string.create_contact), getString(R.string.create),
prefilledJid, null, invite == null || !invite.hasFingerprints() prefilledJid, null, fingerprint == null
); );
dialog.setOnEnterJidDialogPositiveListener(new EnterJidDialog.OnEnterJidDialogPositiveListener() { dialog.setOnEnterJidDialogPositiveListener(new EnterJidDialog.OnEnterJidDialogPositiveListener() {
@ -438,11 +420,9 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
if (contact.showInRoster()) { if (contact.showInRoster()) {
throw new EnterJidDialog.JidError(getString(R.string.contact_already_exists)); throw new EnterJidDialog.JidError(getString(R.string.contact_already_exists));
} else { } else {
contact.addOtrFingerprint(fingerprint);
xmppConnectionService.createContact(contact); xmppConnectionService.createContact(contact);
if (invite != null && invite.hasFingerprints()) { switchToConversation(contact);
xmppConnectionService.verifyFingerprints(contact,invite.getFingerprints());
}
switchToConversation(contact, invite == null ? null : invite.getBody());
return true; return true;
} }
} }
@ -581,11 +561,11 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
return xmppConnectionService.findAccountByJid(jid); return xmppConnectionService.findAccountByJid(jid);
} }
protected void switchToConversation(Contact contact, String body) { protected void switchToConversation(Contact contact) {
Conversation conversation = xmppConnectionService Conversation conversation = xmppConnectionService
.findOrCreateConversation(contact.getAccount(), .findOrCreateConversation(contact.getAccount(),
contact.getJid(), false); contact.getJid(), false);
switchToConversation(conversation, body, false); switchToConversation(conversation);
} }
public static void populateAccountSpinner(Context context, List<String> accounts, Spinner spinner) { public static void populateAccountSpinner(Context context, List<String> accounts, Spinner spinner) {
@ -644,7 +624,7 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
showCreateConferenceDialog(); showCreateConferenceDialog();
return true; return true;
case R.id.action_scan_qr_code: case R.id.action_scan_qr_code:
new IntentIntegrator(this).initiateScan(Arrays.asList("AZTEC","QR_CODE")); new IntentIntegrator(this).initiateScan();
return true; return true;
case R.id.action_hide_offline: case R.id.action_hide_offline:
mHideOfflineContacts = !item.isChecked(); mHideOfflineContacts = !item.isChecked();
@ -806,15 +786,12 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
if (this.mPendingInvite != null) { if (this.mPendingInvite != null) {
mPendingInvite.invite(); mPendingInvite.invite();
this.mPendingInvite = null; this.mPendingInvite = null;
filter(null);
} else if (!handleIntent(getIntent())) { } else if (!handleIntent(getIntent())) {
if (mSearchEditText != null) { if (mSearchEditText != null) {
filter(mSearchEditText.getText().toString()); filter(mSearchEditText.getText().toString());
} else { } else {
filter(null); filter(null);
} }
} else {
filter(null);
} }
setIntent(null); setIntent(null);
} }
@ -833,13 +810,15 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
case Intent.ACTION_VIEW: case Intent.ACTION_VIEW:
Uri uri = intent.getData(); Uri uri = intent.getData();
if (uri != null) { if (uri != null) {
return new Invite(intent.getData(),false).invite(); Log.d(Config.LOGTAG, "received uri=" + intent.getData());
return new Invite(intent.getData()).invite();
} else { } else {
return false; return false;
} }
case NfcAdapter.ACTION_NDEF_DISCOVERED: case NfcAdapter.ACTION_NDEF_DISCOVERED:
for (Parcelable message : getIntent().getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)) { for (Parcelable message : getIntent().getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)) {
if (message instanceof NdefMessage) { if (message instanceof NdefMessage) {
Log.d(Config.LOGTAG, "received message=" + message);
for (NdefRecord record : ((NdefMessage) message).getRecords()) { for (NdefRecord record : ((NdefMessage) message).getRecords()) {
switch (record.getTnf()) { switch (record.getTnf()) {
case NdefRecord.TNF_WELL_KNOWN: case NdefRecord.TNF_WELL_KNOWN:
@ -863,40 +842,28 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
} }
private boolean handleJid(Invite invite) { private boolean handleJid(Invite invite) {
Account account = xmppConnectionService.findAccountByJid(invite.getJid());
if (account != null && !account.isOptionSet(Account.OPTION_DISABLED)) {
if (invite.hasFingerprints() && xmppConnectionService.verifyFingerprints(account,invite.getFingerprints())) {
Toast.makeText(this,R.string.verified_fingerprints,Toast.LENGTH_SHORT).show();
}
switchToAccount(account);
finish();
return true;
}
List<Contact> contacts = xmppConnectionService.findContacts(invite.getJid()); List<Contact> contacts = xmppConnectionService.findContacts(invite.getJid());
if (invite.isMuc()) { if (invite.isMuc()) {
Conversation muc = xmppConnectionService.findFirstMuc(invite.getJid()); Conversation muc = xmppConnectionService.findFirstMuc(invite.getJid());
if (muc != null) { if (muc != null) {
switchToConversation(muc,invite.getBody(),false); switchToConversation(muc);
return true; return true;
} else { } else {
showJoinConferenceDialog(invite.getJid().toBareJid().toString()); showJoinConferenceDialog(invite.getJid().toBareJid().toString());
return false; return false;
} }
} else if (contacts.size() == 0) { } else if (contacts.size() == 0) {
showCreateContactDialog(invite.getJid().toString(), invite); showCreateContactDialog(invite.getJid().toString(), invite.getFingerprint());
return false; return false;
} else if (contacts.size() == 1) { } else if (contacts.size() == 1) {
Contact contact = contacts.get(0); Contact contact = contacts.get(0);
if (!invite.isSafeSource() && invite.hasFingerprints()) { if (invite.getFingerprint() != null) {
displayVerificationWarningDialog(contact,invite); if (contact.addOtrFingerprint(invite.getFingerprint())) {
} else { Log.d(Config.LOGTAG, "added new fingerprint");
if (invite.hasFingerprints()) { xmppConnectionService.syncRosterToDisk(contact.getAccount());
if(xmppConnectionService.verifyFingerprints(contact, invite.getFingerprints())) {
Toast.makeText(this,R.string.verified_fingerprints,Toast.LENGTH_SHORT).show();
}
} }
switchToConversation(contact, invite.getBody());
} }
switchToConversation(contact);
return true; return true;
} else { } else {
if (mMenuSearchView != null) { if (mMenuSearchView != null) {
@ -911,46 +878,6 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
} }
} }
private void displayVerificationWarningDialog(final Contact contact, final Invite invite) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.verify_omemo_keys);
View view = getLayoutInflater().inflate(R.layout.dialog_verify_fingerprints, null);
final CheckBox isTrustedSource = (CheckBox) view.findViewById(R.id.trusted_source);
TextView warning = (TextView) view.findViewById(R.id.warning);
String jid = contact.getJid().toBareJid().toString();
SpannableString spannable = new SpannableString(getString(R.string.verifying_omemo_keys_trusted_source,jid,contact.getDisplayName()));
int start = spannable.toString().indexOf(jid);
if (start >= 0) {
spannable.setSpan(new TypefaceSpan("monospace"),start,start + jid.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
warning.setText(spannable);
builder.setView(view);
builder.setPositiveButton(R.string.confirm, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (isTrustedSource.isChecked() && invite.hasFingerprints()) {
xmppConnectionService.verifyFingerprints(contact, invite.getFingerprints());
}
switchToConversation(contact, invite.getBody());
}
});
builder.setNegativeButton(R.string.cancel, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
StartConversationActivity.this.finish();
}
});
AlertDialog dialog = builder.create();
dialog.setCanceledOnTouchOutside(false);
dialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
StartConversationActivity.this.finish();
}
});
dialog.show();
}
protected void filter(String needle) { protected void filter(String needle) {
if (xmppConnectionServiceBound) { if (xmppConnectionServiceBound) {
this.filterContacts(needle); this.filterContacts(needle);
@ -1122,14 +1049,10 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
activity.conference_context_id = acmi.position; activity.conference_context_id = acmi.position;
} else if (mResContextMenu == R.menu.contact_context) { } else if (mResContextMenu == R.menu.contact_context) {
activity.contact_context_id = acmi.position; activity.contact_context_id = acmi.position;
final Contact contact = (Contact) activity.contacts.get(acmi.position); final Blockable contact = (Contact) activity.contacts.get(acmi.position);
final MenuItem blockUnblockItem = menu.findItem(R.id.context_contact_block_unblock); final MenuItem blockUnblockItem = menu.findItem(R.id.context_contact_block_unblock);
final MenuItem showContactDetailsItem = menu.findItem(R.id.context_contact_details);
if (contact.isSelf()) {
showContactDetailsItem.setVisible(false);
}
XmppConnection xmpp = contact.getAccount().getXmppConnection(); XmppConnection xmpp = contact.getAccount().getXmppConnection();
if (xmpp != null && xmpp.getFeatures().blocking() && !contact.isSelf()) { if (xmpp != null && xmpp.getFeatures().blocking()) {
if (contact.isBlocked()) { if (contact.isBlocked()) {
blockUnblockItem.setTitle(R.string.unblock_contact); blockUnblockItem.setTitle(R.string.unblock_contact);
} else { } else {
@ -1160,9 +1083,6 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
case R.id.context_join_conference: case R.id.context_join_conference:
activity.openConversationForBookmark(); activity.openConversationForBookmark();
break; break;
case R.id.context_share_uri:
activity.shareBookmarkUri();
break;
case R.id.context_delete_conference: case R.id.context_delete_conference:
activity.deleteConference(); activity.deleteConference();
} }
@ -1180,10 +1100,6 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
super(uri); super(uri);
} }
public Invite(Uri uri, boolean safeSource) {
super(uri,safeSource);
}
boolean invite() { boolean invite() {
if (getJid() != null) { if (getJid() != null) {
return handleJid(this); return handleJid(this);

View file

@ -1,12 +1,7 @@
package eu.siacs.conversations.ui; package eu.siacs.conversations.ui;
import android.app.ActionBar;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log;
import android.view.Gravity;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
import android.widget.Button; import android.widget.Button;
@ -15,29 +10,24 @@ import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import com.google.zxing.integration.android.IntentIntegrator;
import org.whispersystems.libaxolotl.IdentityKey; import org.whispersystems.libaxolotl.IdentityKey;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R; import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.FingerprintStatus; import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.utils.XmppUri;
import eu.siacs.conversations.xmpp.OnKeyStatusUpdated; import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid; import eu.siacs.conversations.xmpp.jid.Jid;
public class TrustKeysActivity extends OmemoActivity implements OnKeyStatusUpdated { public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdated {
private List<Jid> contactJids; private List<Jid> contactJids;
private Account mAccount; private Account mAccount;
@ -71,7 +61,6 @@ public class TrustKeysActivity extends OmemoActivity implements OnKeyStatusUpdat
finish(); finish();
} }
}; };
private Toast mUseCameraHintToast = null;
@Override @Override
protected void refreshUiReal() { protected void refreshUiReal() {
@ -110,61 +99,6 @@ public class TrustKeysActivity extends OmemoActivity implements OnKeyStatusUpdat
} }
} }
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.trust_keys, menu);
mUseCameraHintToast = Toast.makeText(this,R.string.use_camera_icon_to_scan_barcode,Toast.LENGTH_LONG);
ActionBar actionBar = getActionBar();
mUseCameraHintToast.setGravity(Gravity.TOP | Gravity.END, 0 ,actionBar == null ? 0 : actionBar.getHeight());
mUseCameraHintToast.show();
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_scan_qr_code:
if (hasPendingKeyFetches()) {
Toast.makeText(this, R.string.please_wait_for_keys_to_be_fetched, Toast.LENGTH_SHORT).show();
} else {
new IntentIntegrator(this).initiateScan(Arrays.asList("AZTEC","QR_CODE"));
return true;
}
}
return super.onOptionsItemSelected(item);
}
@Override
protected void onStop() {
super.onStop();
if (mUseCameraHintToast != null) {
mUseCameraHintToast.cancel();
}
}
@Override
protected void processFingerprintVerification(XmppUri uri) {
if (mConversation != null
&& mAccount != null
&& uri.hasFingerprints()
&& mAccount.getAxolotlService().getCryptoTargets(mConversation).contains(uri.getJid())) {
boolean performedVerification = xmppConnectionService.verifyFingerprints(mAccount.getRoster().getContact(uri.getJid()),uri.getFingerprints());
boolean keys = reloadFingerprints();
if (performedVerification && !keys && !hasNoOtherTrustedKeys() && !hasPendingKeyFetches()) {
Toast.makeText(this,R.string.all_omemo_keys_have_been_verified, Toast.LENGTH_SHORT).show();
finishOk();
return;
} else if (performedVerification) {
Toast.makeText(this,R.string.verified_fingerprints,Toast.LENGTH_SHORT).show();
}
} else {
reloadFingerprints();
Log.d(Config.LOGTAG,"xmpp uri was: "+uri.getJid()+" has Fingerprints: "+Boolean.toString(uri.hasFingerprints()));
Toast.makeText(this,R.string.barcode_does_not_contain_fingerprints_for_this_conversation,Toast.LENGTH_SHORT).show();
}
populateView();
}
private void populateView() { private void populateView() {
setTitle(getString(R.string.trust_omemo_fingerprints)); setTitle(getString(R.string.trust_omemo_fingerprints));
ownKeys.removeAllViews(); ownKeys.removeAllViews();
@ -174,14 +108,16 @@ public class TrustKeysActivity extends OmemoActivity implements OnKeyStatusUpdat
for(final String fingerprint : ownKeysToTrust.keySet()) { for(final String fingerprint : ownKeysToTrust.keySet()) {
hasOwnKeys = true; hasOwnKeys = true;
addFingerprintRowWithListeners(ownKeys, mAccount, fingerprint, false, addFingerprintRowWithListeners(ownKeys, mAccount, fingerprint, false,
FingerprintStatus.createActive(ownKeysToTrust.get(fingerprint)), false, false, XmppAxolotlSession.Trust.fromBoolean(ownKeysToTrust.get(fingerprint)), false,
new CompoundButton.OnCheckedChangeListener() { new CompoundButton.OnCheckedChangeListener() {
@Override @Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
ownKeysToTrust.put(fingerprint, isChecked); ownKeysToTrust.put(fingerprint, isChecked);
// own fingerprints have no impact on locked status. // own fingerprints have no impact on locked status.
} }
} },
null,
null
); );
} }
@ -197,14 +133,16 @@ public class TrustKeysActivity extends OmemoActivity implements OnKeyStatusUpdat
final Map<String, Boolean> fingerprints = entry.getValue(); final Map<String, Boolean> fingerprints = entry.getValue();
for (final String fingerprint : fingerprints.keySet()) { for (final String fingerprint : fingerprints.keySet()) {
addFingerprintRowWithListeners(keysContainer, mAccount, fingerprint, false, addFingerprintRowWithListeners(keysContainer, mAccount, fingerprint, false,
FingerprintStatus.createActive(fingerprints.get(fingerprint)), false, false, XmppAxolotlSession.Trust.fromBoolean(fingerprints.get(fingerprint)), false,
new CompoundButton.OnCheckedChangeListener() { new CompoundButton.OnCheckedChangeListener() {
@Override @Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
fingerprints.put(fingerprint, isChecked); fingerprints.put(fingerprint, isChecked);
lockOrUnlockAsNeeded(); lockOrUnlockAsNeeded();
} }
} },
null,
null
); );
} }
if (fingerprints.size() == 0) { if (fingerprints.size() == 0) {
@ -246,7 +184,7 @@ public class TrustKeysActivity extends OmemoActivity implements OnKeyStatusUpdat
List<Jid> acceptedTargets = mConversation == null ? new ArrayList<Jid>() : mConversation.getAcceptedCryptoTargets(); List<Jid> acceptedTargets = mConversation == null ? new ArrayList<Jid>() : mConversation.getAcceptedCryptoTargets();
ownKeysToTrust.clear(); ownKeysToTrust.clear();
AxolotlService service = this.mAccount.getAxolotlService(); AxolotlService service = this.mAccount.getAxolotlService();
Set<IdentityKey> ownKeysSet = service.getKeysWithTrust(FingerprintStatus.createActiveUndecided()); Set<IdentityKey> ownKeysSet = service.getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED);
for(final IdentityKey identityKey : ownKeysSet) { for(final IdentityKey identityKey : ownKeysSet) {
if(!ownKeysToTrust.containsKey(identityKey)) { if(!ownKeysToTrust.containsKey(identityKey)) {
ownKeysToTrust.put(identityKey.getFingerprint().replaceAll("\\s", ""), false); ownKeysToTrust.put(identityKey.getFingerprint().replaceAll("\\s", ""), false);
@ -255,9 +193,9 @@ public class TrustKeysActivity extends OmemoActivity implements OnKeyStatusUpdat
synchronized (this.foreignKeysToTrust) { synchronized (this.foreignKeysToTrust) {
foreignKeysToTrust.clear(); foreignKeysToTrust.clear();
for (Jid jid : contactJids) { for (Jid jid : contactJids) {
Set<IdentityKey> foreignKeysSet = service.getKeysWithTrust(FingerprintStatus.createActiveUndecided(), jid); Set<IdentityKey> foreignKeysSet = service.getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED, jid);
if (hasNoOtherTrustedKeys(jid) && ownKeysSet.size() == 0) { if (hasNoOtherTrustedKeys(jid) && ownKeysSet.size() == 0) {
foreignKeysSet.addAll(service.getKeysWithTrust(FingerprintStatus.createActive(false), jid)); foreignKeysSet.addAll(service.getKeysWithTrust(XmppAxolotlSession.Trust.UNTRUSTED, jid));
} }
Map<String, Boolean> foreignFingerprints = new HashMap<>(); Map<String, Boolean> foreignFingerprints = new HashMap<>();
for (final IdentityKey identityKey : foreignKeysSet) { for (final IdentityKey identityKey : foreignKeysSet) {
@ -273,19 +211,15 @@ public class TrustKeysActivity extends OmemoActivity implements OnKeyStatusUpdat
return ownKeysSet.size() + foreignKeysToTrust.size() > 0; return ownKeysSet.size() + foreignKeysToTrust.size() > 0;
} }
@Override
public void onBackendConnected() { public void onBackendConnected() {
Intent intent = getIntent(); Intent intent = getIntent();
this.mAccount = extractAccount(intent); this.mAccount = extractAccount(intent);
if (this.mAccount != null && intent != null) { if (this.mAccount != null && intent != null) {
String uuid = intent.getStringExtra("conversation"); String uuid = intent.getStringExtra("conversation");
this.mConversation = xmppConnectionService.findConversationByUuid(uuid); this.mConversation = xmppConnectionService.findConversationByUuid(uuid);
if (this.mPendingFingerprintVerificationUri != null) { reloadFingerprints();
processFingerprintVerification(this.mPendingFingerprintVerificationUri); populateView();
this.mPendingFingerprintVerificationUri = null;
} else {
reloadFingerprints();
populateView();
}
} }
} }
@ -304,32 +238,24 @@ public class TrustKeysActivity extends OmemoActivity implements OnKeyStatusUpdat
@Override @Override
public void onKeyStatusUpdated(final AxolotlService.FetchStatus report) { public void onKeyStatusUpdated(final AxolotlService.FetchStatus report) {
final boolean keysToTrust = reloadFingerprints();
if (report != null) { if (report != null) {
lastFetchReport = report; lastFetchReport = report;
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
if (mUseCameraHintToast != null && !keysToTrust) {
mUseCameraHintToast.cancel();
}
switch (report) { switch (report) {
case ERROR: case ERROR:
Toast.makeText(TrustKeysActivity.this,R.string.error_fetching_omemo_key,Toast.LENGTH_SHORT).show(); Toast.makeText(TrustKeysActivity.this,R.string.error_fetching_omemo_key,Toast.LENGTH_SHORT).show();
break; break;
case SUCCESS_TRUSTED:
Toast.makeText(TrustKeysActivity.this,R.string.blindly_trusted_omemo_keys,Toast.LENGTH_LONG).show();
break;
case SUCCESS_VERIFIED: case SUCCESS_VERIFIED:
Toast.makeText(TrustKeysActivity.this, Toast.makeText(TrustKeysActivity.this,R.string.verified_omemo_key_with_certificate,Toast.LENGTH_LONG).show();
Config.X509_VERIFICATION ? R.string.verified_omemo_key_with_certificate : R.string.all_omemo_keys_have_been_verified,
Toast.LENGTH_LONG).show();
break; break;
} }
} }
}); });
} }
boolean keysToTrust = reloadFingerprints();
if (keysToTrust || hasPendingKeyFetches() || hasNoOtherTrustedKeys()) { if (keysToTrust || hasPendingKeyFetches() || hasNoOtherTrustedKeys()) {
refreshUi(); refreshUi();
} else { } else {
@ -354,7 +280,7 @@ public class TrustKeysActivity extends OmemoActivity implements OnKeyStatusUpdat
for(final String fingerprint :ownKeysToTrust.keySet()) { for(final String fingerprint :ownKeysToTrust.keySet()) {
mAccount.getAxolotlService().setFingerprintTrust( mAccount.getAxolotlService().setFingerprintTrust(
fingerprint, fingerprint,
FingerprintStatus.createActive(ownKeysToTrust.get(fingerprint))); XmppAxolotlSession.Trust.fromBoolean(ownKeysToTrust.get(fingerprint)));
} }
List<Jid> acceptedTargets = mConversation == null ? new ArrayList<Jid>() : mConversation.getAcceptedCryptoTargets(); List<Jid> acceptedTargets = mConversation == null ? new ArrayList<Jid>() : mConversation.getAcceptedCryptoTargets();
synchronized (this.foreignKeysToTrust) { synchronized (this.foreignKeysToTrust) {
@ -367,7 +293,7 @@ public class TrustKeysActivity extends OmemoActivity implements OnKeyStatusUpdat
for (final String fingerprint : value.keySet()) { for (final String fingerprint : value.keySet()) {
mAccount.getAxolotlService().setFingerprintTrust( mAccount.getAxolotlService().setFingerprintTrust(
fingerprint, fingerprint,
FingerprintStatus.createActive(value.get(fingerprint))); XmppAxolotlSession.Trust.fromBoolean(value.get(fingerprint)));
} }
} }
} }

View file

@ -1,5 +0,0 @@
package eu.siacs.conversations.ui;
public interface UiInformableCallback<T> extends UiCallback<T> {
void inform(String text);
}

View file

@ -173,10 +173,11 @@ public class VerifyOTRActivity extends XmppActivity implements XmppConnectionSer
protected boolean verifyWithUri(XmppUri uri) { protected boolean verifyWithUri(XmppUri uri) {
Contact contact = mConversation.getContact(); Contact contact = mConversation.getContact();
if (this.mConversation.getContact().getJid().equals(uri.getJid()) && uri.hasFingerprints()) { if (this.mConversation.getContact().getJid().equals(uri.getJid()) && uri.getFingerprint() != null) {
xmppConnectionService.verifyFingerprints(contact,uri.getFingerprints()); contact.addOtrFingerprint(uri.getFingerprint());
Toast.makeText(this,R.string.verified,Toast.LENGTH_SHORT).show(); Toast.makeText(this,R.string.verified,Toast.LENGTH_SHORT).show();
updateView(); updateView();
xmppConnectionService.syncRosterToDisk(contact.getAccount());
return true; return true;
} else { } else {
Toast.makeText(this,R.string.could_not_verify_fingerprint,Toast.LENGTH_SHORT).show(); Toast.makeText(this,R.string.could_not_verify_fingerprint,Toast.LENGTH_SHORT).show();

View file

@ -8,35 +8,22 @@ import android.os.Bundle;
import android.view.View; import android.view.View;
import android.widget.Button; import android.widget.Button;
import java.util.List;
import eu.siacs.conversations.R; import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
public class WelcomeActivity extends XmppActivity { public class WelcomeActivity extends Activity {
@Override
protected void refreshUiReal() {
}
@Override
void onBackendConnected() {
}
@Override @Override
protected void onCreate(final Bundle savedInstanceState) { protected void onCreate(final Bundle savedInstanceState) {
if (getResources().getBoolean(R.bool.portrait_only)) { if (getResources().getBoolean(R.bool.portrait_only)) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
} }
super.onCreate(savedInstanceState);
setContentView(R.layout.welcome);
final ActionBar ab = getActionBar(); final ActionBar ab = getActionBar();
if (ab != null) { if (ab != null) {
ab.setDisplayShowHomeEnabled(false); ab.setDisplayShowHomeEnabled(false);
ab.setDisplayHomeAsUpEnabled(false); ab.setDisplayHomeAsUpEnabled(false);
} }
super.onCreate(savedInstanceState);
setContentView(R.layout.welcome);
final Button createAccount = (Button) findViewById(R.id.create_account); final Button createAccount = (Button) findViewById(R.id.create_account);
createAccount.setOnClickListener(new View.OnClickListener() { createAccount.setOnClickListener(new View.OnClickListener() {
@Override @Override
@ -50,15 +37,7 @@ public class WelcomeActivity extends XmppActivity {
useOwnProvider.setOnClickListener(new View.OnClickListener() { useOwnProvider.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
List<Account> accounts = xmppConnectionService.getAccounts(); startActivity(new Intent(WelcomeActivity.this, EditAccountActivity.class));
Intent intent = new Intent(WelcomeActivity.this, EditAccountActivity.class);
if (accounts.size() == 1) {
intent.putExtra("jid",accounts.get(0).getJid().toBareJid().toString());
intent.putExtra("init",true);
} else if (accounts.size() >= 1) {
intent = new Intent(WelcomeActivity.this, ManageAccountActivity.class);
}
startActivity(intent);
} }
}); });

View file

@ -28,7 +28,6 @@ import android.graphics.Color;
import android.graphics.Point; import android.graphics.Point;
import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.net.ConnectivityManager;
import android.net.Uri; import android.net.Uri;
import android.nfc.NdefMessage; import android.nfc.NdefMessage;
import android.nfc.NdefRecord; import android.nfc.NdefRecord;
@ -44,18 +43,24 @@ import android.os.SystemClock;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.text.InputType; import android.text.InputType;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
import android.util.Log;
import android.util.Pair; import android.util.Pair;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodManager;
import android.widget.CompoundButton;
import android.widget.EditText; import android.widget.EditText;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import com.google.zxing.BarcodeFormat; import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType; import com.google.zxing.EncodeHintType;
import com.google.zxing.aztec.AztecWriter; import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix; import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import net.java.otr4j.session.SessionID; import net.java.otr4j.session.SessionID;
@ -63,6 +68,7 @@ import java.io.FileNotFoundException;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.Hashtable; import java.util.Hashtable;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -71,16 +77,19 @@ import java.util.concurrent.atomic.AtomicInteger;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.R; import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.entities.Presence;
import eu.siacs.conversations.entities.Presences; import eu.siacs.conversations.entities.Presences;
import eu.siacs.conversations.entities.ServiceDiscoveryResult;
import eu.siacs.conversations.services.AvatarService; import eu.siacs.conversations.services.AvatarService;
import eu.siacs.conversations.services.BarcodeProvider;
import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder; import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder;
import eu.siacs.conversations.ui.widget.Switch;
import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.ExceptionHelper; import eu.siacs.conversations.utils.ExceptionHelper;
import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.utils.UIHelper;
@ -437,16 +446,6 @@ public abstract class XmppActivity extends Activity {
} }
} }
protected boolean isAffectedByDataSaver() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
return cm.isActiveNetworkMetered()
&& cm.getRestrictBackgroundStatus() == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED;
} else {
return false;
}
}
protected boolean usingEnterKey() { protected boolean usingEnterKey() {
return getPreferences().getBoolean("display_enter_key", false); return getPreferences().getBoolean("display_enter_key", false);
} }
@ -769,6 +768,164 @@ public abstract class XmppActivity extends Activity {
builder.create().show(); builder.create().show();
} }
protected boolean addFingerprintRow(LinearLayout keys, final Account account, final String fingerprint, boolean highlight, View.OnClickListener onKeyClickedListener) {
final XmppAxolotlSession.Trust trust = account.getAxolotlService()
.getFingerprintTrust(fingerprint);
if (trust == null) {
return false;
}
return addFingerprintRowWithListeners(keys, account, fingerprint, highlight, trust, true,
new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
account.getAxolotlService().setFingerprintTrust(fingerprint,
(isChecked) ? XmppAxolotlSession.Trust.TRUSTED :
XmppAxolotlSession.Trust.UNTRUSTED);
}
},
new View.OnClickListener() {
@Override
public void onClick(View v) {
account.getAxolotlService().setFingerprintTrust(fingerprint,
XmppAxolotlSession.Trust.UNTRUSTED);
v.setEnabled(true);
}
},
onKeyClickedListener
);
}
protected boolean addFingerprintRowWithListeners(LinearLayout keys, final Account account,
final String fingerprint,
boolean highlight,
XmppAxolotlSession.Trust trust,
boolean showTag,
CompoundButton.OnCheckedChangeListener
onCheckedChangeListener,
View.OnClickListener onClickListener,
View.OnClickListener onKeyClickedListener) {
if (trust == XmppAxolotlSession.Trust.COMPROMISED) {
return false;
}
View view = getLayoutInflater().inflate(R.layout.contact_key, keys, false);
TextView key = (TextView) view.findViewById(R.id.key);
key.setOnClickListener(onKeyClickedListener);
TextView keyType = (TextView) view.findViewById(R.id.key_type);
keyType.setOnClickListener(onKeyClickedListener);
Switch trustToggle = (Switch) view.findViewById(R.id.tgl_trust);
trustToggle.setVisibility(View.VISIBLE);
trustToggle.setOnCheckedChangeListener(onCheckedChangeListener);
trustToggle.setOnClickListener(onClickListener);
final View.OnLongClickListener purge = new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
showPurgeKeyDialog(account, fingerprint);
return true;
}
};
boolean active = true;
view.setOnLongClickListener(purge);
key.setOnLongClickListener(purge);
keyType.setOnLongClickListener(purge);
boolean x509 = Config.X509_VERIFICATION
&& (trust == XmppAxolotlSession.Trust.TRUSTED_X509 || trust == XmppAxolotlSession.Trust.INACTIVE_TRUSTED_X509);
switch (trust) {
case UNTRUSTED:
case TRUSTED:
case TRUSTED_X509:
trustToggle.setChecked(trust.trusted(), false);
trustToggle.setEnabled(!Config.X509_VERIFICATION || trust != XmppAxolotlSession.Trust.TRUSTED_X509);
if (Config.X509_VERIFICATION && trust == XmppAxolotlSession.Trust.TRUSTED_X509) {
trustToggle.setOnClickListener(null);
}
key.setTextColor(getPrimaryTextColor());
keyType.setTextColor(getSecondaryTextColor());
break;
case UNDECIDED:
trustToggle.setChecked(false, false);
trustToggle.setEnabled(false);
key.setTextColor(getPrimaryTextColor());
keyType.setTextColor(getSecondaryTextColor());
break;
case INACTIVE_UNTRUSTED:
case INACTIVE_UNDECIDED:
trustToggle.setOnClickListener(null);
trustToggle.setChecked(false, false);
trustToggle.setEnabled(false);
key.setTextColor(getTertiaryTextColor());
keyType.setTextColor(getTertiaryTextColor());
active = false;
break;
case INACTIVE_TRUSTED:
case INACTIVE_TRUSTED_X509:
trustToggle.setOnClickListener(null);
trustToggle.setChecked(true, false);
trustToggle.setEnabled(false);
key.setTextColor(getTertiaryTextColor());
keyType.setTextColor(getTertiaryTextColor());
active = false;
break;
}
if (showTag) {
keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509 : R.string.omemo_fingerprint));
} else {
keyType.setVisibility(View.GONE);
}
if (highlight) {
keyType.setTextColor(getResources().getColor(R.color.accent));
keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509_selected_message : R.string.omemo_fingerprint_selected_message));
} else {
keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509 : R.string.omemo_fingerprint));
}
key.setText(CryptoHelper.prettifyFingerprint(fingerprint.substring(2)));
final View.OnClickListener toast;
if (!active) {
toast = new View.OnClickListener() {
@Override
public void onClick(View v) {
replaceToast(getString(R.string.this_device_is_no_longer_in_use), false);
}
};
trustToggle.setOnClickListener(toast);
} else {
toast = new View.OnClickListener() {
@Override
public void onClick(View v) {
hideToast();
}
};
}
view.setOnClickListener(toast);
key.setOnClickListener(toast);
keyType.setOnClickListener(toast);
keys.addView(view);
return true;
}
public void showPurgeKeyDialog(final Account account, final String fingerprint) {
Builder builder = new Builder(this);
builder.setTitle(getString(R.string.purge_key));
builder.setIconAttribute(android.R.attr.alertDialogIcon);
builder.setMessage(getString(R.string.purge_key_desc_part1)
+ "\n\n" + CryptoHelper.prettifyFingerprint(fingerprint.substring(2))
+ "\n\n" + getString(R.string.purge_key_desc_part2));
builder.setNegativeButton(getString(R.string.cancel), null);
builder.setPositiveButton(getString(R.string.purge_key),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
account.getAxolotlService().purgeKey(fingerprint);
refreshUi();
}
});
builder.create().show();
}
public boolean hasStoragePermission(int requestCode) { public boolean hasStoragePermission(int requestCode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
@ -992,7 +1149,7 @@ public abstract class XmppActivity extends Activity {
} }
protected boolean manuallyChangePresence() { protected boolean manuallyChangePresence() {
return getPreferences().getBoolean(SettingsActivity.MANUALLY_CHANGE_PRESENCE, false); return getPreferences().getBoolean("manually_change_presence", false);
} }
protected void unregisterNdefPushMessageCallback() { protected void unregisterNdefPushMessageCallback() {
@ -1059,7 +1216,7 @@ public abstract class XmppActivity extends Activity {
Point size = new Point(); Point size = new Point();
getWindowManager().getDefaultDisplay().getSize(size); getWindowManager().getDefaultDisplay().getSize(size);
final int width = (size.x < size.y ? size.x : size.y); final int width = (size.x < size.y ? size.x : size.y);
Bitmap bitmap = BarcodeProvider.createAztecBitmap(uri, width); Bitmap bitmap = createQrCodeBitmap(uri, width);
ImageView view = new ImageView(this); ImageView view = new ImageView(this);
view.setBackgroundColor(Color.WHITE); view.setBackgroundColor(Color.WHITE);
view.setImageBitmap(bitmap); view.setImageBitmap(bitmap);
@ -1069,6 +1226,31 @@ public abstract class XmppActivity extends Activity {
} }
} }
protected Bitmap createQrCodeBitmap(String input, int size) {
Log.d(Config.LOGTAG,"qr code requested size: "+size);
try {
final QRCodeWriter QR_CODE_WRITER = new QRCodeWriter();
final Hashtable<EncodeHintType, Object> hints = new Hashtable<>();
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
final BitMatrix result = QR_CODE_WRITER.encode(input, BarcodeFormat.QR_CODE, size, size, hints);
final int width = result.getWidth();
final int height = result.getHeight();
final int[] pixels = new int[width * height];
for (int y = 0; y < height; y++) {
final int offset = y * width;
for (int x = 0; x < width; x++) {
pixels[offset + x] = result.get(x, y) ? Color.BLACK : Color.TRANSPARENT;
}
}
final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Log.d(Config.LOGTAG,"output size: "+width+"x"+height);
bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
return bitmap;
} catch (final WriterException e) {
return null;
}
}
protected Account extractAccount(Intent intent) { protected Account extractAccount(Intent intent) {
String jid = intent != null ? intent.getStringExtra(EXTRA_ACCOUNT) : null; String jid = intent != null ? intent.getStringExtra(EXTRA_ACCOUNT) : null;
try { try {

View file

@ -11,6 +11,8 @@ import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Build;
import android.support.v4.content.FileProvider;
import android.text.Spannable; import android.text.Spannable;
import android.text.SpannableString; import android.text.SpannableString;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
@ -20,9 +22,7 @@ import android.text.style.RelativeSizeSpan;
import android.text.style.StyleSpan; import android.text.style.StyleSpan;
import android.text.util.Linkify; import android.text.util.Linkify;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
import android.view.ActionMode; import android.util.Patterns;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
import android.view.View.OnLongClickListener; import android.view.View.OnLongClickListener;
@ -37,14 +37,14 @@ import android.widget.Toast;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.net.URL; import java.net.URL;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.RejectedExecutionException;
import java.util.regex.MatchResult;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.R; import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.FingerprintStatus; import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.DownloadableFile;
@ -52,17 +52,12 @@ import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.Message.FileParams; import eu.siacs.conversations.entities.Message.FileParams;
import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.MessageArchiveService;
import eu.siacs.conversations.services.NotificationService;
import eu.siacs.conversations.ui.ConversationActivity; import eu.siacs.conversations.ui.ConversationActivity;
import eu.siacs.conversations.ui.text.DividerSpan;
import eu.siacs.conversations.ui.text.QuoteSpan;
import eu.siacs.conversations.ui.widget.ClickableMovementMethod; import eu.siacs.conversations.ui.widget.ClickableMovementMethod;
import eu.siacs.conversations.ui.widget.CopyTextView; import eu.siacs.conversations.ui.widget.CopyTextView;
import eu.siacs.conversations.ui.widget.ListSelectionManager; import eu.siacs.conversations.ui.widget.ListSelectionManager;
import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.GeoHelper; import eu.siacs.conversations.utils.GeoHelper;
import eu.siacs.conversations.utils.Patterns;
import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.utils.UIHelper;
public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextView.CopyHandler { public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextView.CopyHandler {
@ -76,27 +71,6 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
+ "\\;\\/\\?\\@\\&\\=\\#\\~\\-\\.\\+\\!\\*\\'\\(\\)\\,\\_])" + "\\;\\/\\?\\@\\&\\=\\#\\~\\-\\.\\+\\!\\*\\'\\(\\)\\,\\_])"
+ "|(?:\\%[a-fA-F0-9]{2}))+"); + "|(?:\\%[a-fA-F0-9]{2}))+");
private static final Linkify.TransformFilter WEBURL_TRANSFORM_FILTER = new Linkify.TransformFilter() {
@Override
public String transformUrl(Matcher matcher, String url) {
if (url == null) {
return null;
}
final String lcUrl = url.toLowerCase(Locale.US);
if (lcUrl.startsWith("http://") || lcUrl.startsWith("https://")) {
return url;
} else {
return "http://"+url;
}
}
};
private static final Linkify.MatchFilter WEBURL_MATCH_FILTER = new Linkify.MatchFilter() {
@Override
public boolean acceptMatch(CharSequence charSequence, int start, int end) {
return start < 1 || charSequence.charAt(start-1) != '@';
}
};
private ConversationActivity activity; private ConversationActivity activity;
private DisplayMetrics metrics; private DisplayMetrics metrics;
@ -107,8 +81,6 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
private boolean mIndicateReceived = false; private boolean mIndicateReceived = false;
private boolean mUseGreenBackground = false; private boolean mUseGreenBackground = false;
private OnQuoteListener onQuoteListener;
private final ListSelectionManager listSelectionManager = new ListSelectionManager(); private final ListSelectionManager listSelectionManager = new ListSelectionManager();
public MessageAdapter(ConversationActivity activity, List<Message> messages) { public MessageAdapter(ConversationActivity activity, List<Message> messages) {
@ -127,10 +99,6 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
this.mOnContactPictureLongClickedListener = listener; this.mOnContactPictureLongClickedListener = listener;
} }
public void setOnQuoteListener(OnQuoteListener listener) {
this.onQuoteListener = listener;
}
@Override @Override
public int getViewTypeCount() { public int getViewTypeCount() {
return 3; return 3;
@ -159,7 +127,7 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
} }
} }
private void displayStatus(ViewHolder viewHolder, Message message, int type, boolean darkBackground, boolean inValidSession) { private void displayStatus(ViewHolder viewHolder, Message message, int type, boolean darkBackground) {
String filesize = null; String filesize = null;
String info = null; String info = null;
boolean error = false; boolean error = false;
@ -232,26 +200,32 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
if (message.getEncryption() == Message.ENCRYPTION_NONE) { if (message.getEncryption() == Message.ENCRYPTION_NONE) {
viewHolder.indicator.setVisibility(View.GONE); viewHolder.indicator.setVisibility(View.GONE);
} else { } else {
boolean verified = false; viewHolder.indicator.setImageResource(darkBackground ? R.drawable.ic_lock_white_18dp : R.drawable.ic_lock_black_18dp);
viewHolder.indicator.setVisibility(View.VISIBLE);
if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) { if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
final FingerprintStatus status = message.getConversation() XmppAxolotlSession.Trust trust = message.getConversation()
.getAccount().getAxolotlService().getFingerprintTrust( .getAccount().getAxolotlService().getFingerprintTrust(
message.getFingerprint()); message.getFingerprint());
if (status != null && status.isVerified()) {
verified = true; if(trust == null || (!trust.trusted() && !trust.trustedInactive())) {
viewHolder.indicator.setColorFilter(activity.getWarningTextColor());
viewHolder.indicator.setAlpha(1.0f);
} else {
viewHolder.indicator.clearColorFilter();
if (darkBackground) {
viewHolder.indicator.setAlpha(0.7f);
} else {
viewHolder.indicator.setAlpha(0.57f);
}
}
} else {
viewHolder.indicator.clearColorFilter();
if (darkBackground) {
viewHolder.indicator.setAlpha(0.7f);
} else {
viewHolder.indicator.setAlpha(0.57f);
} }
} }
if (verified) {
viewHolder.indicator.setImageResource(darkBackground ? R.drawable.ic_verified_user_white_18dp : R.drawable.ic_verified_user_black_18dp);
} else {
viewHolder.indicator.setImageResource(darkBackground ? R.drawable.ic_lock_white_18dp : R.drawable.ic_lock_black_18dp);
}
if (darkBackground) {
viewHolder.indicator.setAlpha(0.7f);
} else {
viewHolder.indicator.setAlpha(0.57f);
}
viewHolder.indicator.setVisibility(View.VISIBLE);
} }
String formatedTime = UIHelper.readableTimeDifferenceFull(getContext(), String formatedTime = UIHelper.readableTimeDifferenceFull(getContext(),
@ -317,78 +291,10 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
viewHolder.messageBody.setIncludeFontPadding(false); viewHolder.messageBody.setIncludeFontPadding(false);
Spannable span = new SpannableString(body); Spannable span = new SpannableString(body);
span.setSpan(new RelativeSizeSpan(4.0f), 0, body.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); span.setSpan(new RelativeSizeSpan(4.0f), 0, body.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
span.setSpan(new ForegroundColorSpan(activity.getWarningTextColor()), 0, body.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); span.setSpan(new ForegroundColorSpan(activity.getWarningTextColor()), 0, body.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
viewHolder.messageBody.setText(span); viewHolder.messageBody.setText(span);
} }
private int applyQuoteSpan(SpannableStringBuilder body, int start, int end, boolean darkBackground) {
if (start > 1 && !"\n\n".equals(body.subSequence(start - 2, start).toString())) {
body.insert(start++, "\n");
body.setSpan(new DividerSpan(false), start - 2, start, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
end++;
}
if (end < body.length() - 1 && !"\n\n".equals(body.subSequence(end, end + 2).toString())) {
body.insert(end, "\n");
body.setSpan(new DividerSpan(false), end, end + 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
int color = darkBackground ? this.getMessageTextColor(darkBackground, false)
: getContext().getResources().getColor(R.color.bubble);
DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
body.setSpan(new QuoteSpan(color, metrics), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
return 0;
}
/**
* Applies QuoteSpan to group of lines which starts with > or » characters.
* Appends likebreaks and applies DividerSpan to them to show a padding between quote and text.
*/
private boolean handleTextQuotes(SpannableStringBuilder body, boolean darkBackground) {
boolean startsWithQuote = false;
char previous = '\n';
int lineStart = -1;
int lineTextStart = -1;
int quoteStart = -1;
for (int i = 0; i <= body.length(); i++) {
char current = body.length() > i ? body.charAt(i) : '\n';
if (lineStart == -1) {
if (previous == '\n') {
if ((current == '>' && !UIHelper.isPositionFollowedByNumber(body,i)) || current == '\u00bb') {
// Line start with quote
lineStart = i;
if (quoteStart == -1) quoteStart = i;
if (i == 0) startsWithQuote = true;
} else if (quoteStart >= 0) {
// Line start without quote, apply spans there
applyQuoteSpan(body, quoteStart, i - 1, darkBackground);
quoteStart = -1;
}
}
} else {
// Remove extra spaces between > and first character in the line
// > character will be removed too
if (current != ' ' && lineTextStart == -1) {
lineTextStart = i;
}
if (current == '\n') {
body.delete(lineStart, lineTextStart);
i -= lineTextStart - lineStart;
if (i == lineStart) {
// Avoid empty lines because span over empty line can be hidden
body.insert(i++, " ");
}
lineStart = -1;
lineTextStart = -1;
}
}
previous = current;
}
if (quoteStart >= 0) {
// Apply spans to finishing open quote
applyQuoteSpan(body, quoteStart, body.length(), darkBackground);
}
return startsWithQuote;
}
private void displayTextMessage(final ViewHolder viewHolder, final Message message, boolean darkBackground, int type) { private void displayTextMessage(final ViewHolder viewHolder, final Message message, boolean darkBackground, int type) {
if (viewHolder.download_button != null) { if (viewHolder.download_button != null) {
viewHolder.download_button.setVisibility(View.GONE); viewHolder.download_button.setVisibility(View.GONE);
@ -411,9 +317,8 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
for (Message.MergeSeparator mergeSeparator : mergeSeparators) { for (Message.MergeSeparator mergeSeparator : mergeSeparators) {
int start = body.getSpanStart(mergeSeparator); int start = body.getSpanStart(mergeSeparator);
int end = body.getSpanEnd(mergeSeparator); int end = body.getSpanEnd(mergeSeparator);
body.setSpan(new DividerSpan(true), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); body.setSpan(new RelativeSizeSpan(0.3f), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
} }
boolean startsWithQuote = handleTextQuotes(body, darkBackground);
if (message.getType() != Message.TYPE_PRIVATE) { if (message.getType() != Message.TYPE_PRIVATE) {
if (hasMeCommand) { if (hasMeCommand) {
body.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), 0, nick.length(), body.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), 0, nick.length(),
@ -434,13 +339,7 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
} }
body.insert(0, privateMarker); body.insert(0, privateMarker);
int privateMarkerIndex = privateMarker.length(); int privateMarkerIndex = privateMarker.length();
if (startsWithQuote) { body.insert(privateMarkerIndex, " ");
body.insert(privateMarkerIndex, "\n\n");
body.setSpan(new DividerSpan(false), privateMarkerIndex, privateMarkerIndex + 2,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
} else {
body.insert(privateMarkerIndex, " ");
}
body.setSpan(new ForegroundColorSpan(getMessageTextColor(darkBackground, false)), body.setSpan(new ForegroundColorSpan(getMessageTextColor(darkBackground, false)),
0, privateMarkerIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 0, privateMarkerIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
body.setSpan(new StyleSpan(Typeface.BOLD), body.setSpan(new StyleSpan(Typeface.BOLD),
@ -450,15 +349,8 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
privateMarkerIndex + 1 + nick.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); privateMarkerIndex + 1 + nick.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
} }
} }
if (message.getConversation().getMode() == Conversation.MODE_MULTI && message.getStatus() == Message.STATUS_RECEIVED) { Linkify.addLinks(body, Linkify.WEB_URLS);
Pattern pattern = NotificationService.generateNickHighlightPattern(message.getConversation().getMucOptions().getActualNick());
Matcher matcher = pattern.matcher(body);
while(matcher.find()) {
body.setSpan(new StyleSpan(Typeface.BOLD), matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
Linkify.addLinks(body, XMPP_PATTERN, "xmpp"); Linkify.addLinks(body, XMPP_PATTERN, "xmpp");
Linkify.addLinks(body, Patterns.AUTOLINK_WEB_URL, "http", WEBURL_MATCH_FILTER, WEBURL_TRANSFORM_FILTER);
Linkify.addLinks(body, GeoHelper.GEO_URI, "geo"); Linkify.addLinks(body, GeoHelper.GEO_URI, "geo");
viewHolder.messageBody.setAutoLinkMask(0); viewHolder.messageBody.setAutoLinkMask(0);
viewHolder.messageBody.setText(body); viewHolder.messageBody.setText(body);
@ -471,8 +363,7 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
} }
viewHolder.messageBody.setTextColor(this.getMessageTextColor(darkBackground, true)); viewHolder.messageBody.setTextColor(this.getMessageTextColor(darkBackground, true));
viewHolder.messageBody.setLinkTextColor(this.getMessageTextColor(darkBackground, true)); viewHolder.messageBody.setLinkTextColor(this.getMessageTextColor(darkBackground, true));
viewHolder.messageBody.setHighlightColor(activity.getResources().getColor(darkBackground viewHolder.messageBody.setHighlightColor(activity.getResources().getColor(darkBackground ? (type == SENT || !mUseGreenBackground ? R.color.black26 : R.color.grey800) : R.color.grey500));
? (type == SENT || !mUseGreenBackground ? R.color.black26 : R.color.grey800) : R.color.grey500));
viewHolder.messageBody.setTypeface(null, Typeface.NORMAL); viewHolder.messageBody.setTypeface(null, Typeface.NORMAL);
} }
@ -565,20 +456,15 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
if (timestamp == 0) { if (timestamp == 0) {
timestamp = System.currentTimeMillis(); timestamp = System.currentTimeMillis();
} }
conversation.messagesLoaded.set(true); activity.setMessagesLoaded();
MessageArchiveService.Query query = activity.xmppConnectionService.getMessageArchiveService().query(conversation, 0, timestamp); activity.xmppConnectionService.getMessageArchiveService().query(conversation, 0, timestamp);
if (query != null) { Toast.makeText(activity, R.string.fetching_history_from_server,Toast.LENGTH_LONG).show();
Toast.makeText(activity, R.string.fetching_history_from_server, Toast.LENGTH_LONG).show();
} else {
Toast.makeText(activity,R.string.not_fetching_history_retention_period, Toast.LENGTH_SHORT).show();
}
} }
@Override @Override
public View getView(int position, View view, ViewGroup parent) { public View getView(int position, View view, ViewGroup parent) {
final Message message = getItem(position); final Message message = getItem(position);
final boolean omemoEncryption = message.getEncryption() == Message.ENCRYPTION_AXOLOTL; final boolean isInValidSession = message.isValidInSession();
final boolean isInValidSession = message.isValidInSession() && (!omemoEncryption || message.isTrusted());
final Conversation conversation = message.getConversation(); final Conversation conversation = message.getConversation();
final Account account = conversation.getAccount(); final Account account = conversation.getAccount();
final int type = getItemViewType(position); final int type = getItemViewType(position);
@ -640,8 +526,7 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
break; break;
} }
if (viewHolder.messageBody != null) { if (viewHolder.messageBody != null) {
listSelectionManager.onCreate(viewHolder.messageBody, listSelectionManager.onCreate(viewHolder.messageBody);
new MessageBodyActionModeCallback(viewHolder.messageBody));
viewHolder.messageBody.setCopyHandler(this); viewHolder.messageBody.setCopyHandler(this);
} }
view.setTag(viewHolder); view.setTag(viewHolder);
@ -785,15 +670,11 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
} else { } else {
viewHolder.message_box.setBackgroundResource(R.drawable.message_bubble_received_warning); viewHolder.message_box.setBackgroundResource(R.drawable.message_bubble_received_warning);
viewHolder.encryption.setVisibility(View.VISIBLE); viewHolder.encryption.setVisibility(View.VISIBLE);
if (omemoEncryption && !message.isTrusted()) { viewHolder.encryption.setText(CryptoHelper.encryptionTypeToText(message.getEncryption()));
viewHolder.encryption.setText(R.string.not_trusted);
} else {
viewHolder.encryption.setText(CryptoHelper.encryptionTypeToText(message.getEncryption()));
}
} }
} }
displayStatus(viewHolder, message, type, darkBackground, isInValidSession); displayStatus(viewHolder, message, type, darkBackground);
return view; return view;
} }
@ -805,84 +686,9 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
listSelectionManager.onAfterNotifyDataSetChanged(); listSelectionManager.onAfterNotifyDataSetChanged();
} }
private String transformText(CharSequence text, int start, int end, boolean forCopy) {
SpannableStringBuilder builder = new SpannableStringBuilder(text);
Object copySpan = new Object();
builder.setSpan(copySpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
DividerSpan[] dividerSpans = builder.getSpans(0, builder.length(), DividerSpan.class);
for (DividerSpan dividerSpan : dividerSpans) {
builder.replace(builder.getSpanStart(dividerSpan), builder.getSpanEnd(dividerSpan),
dividerSpan.isLarge() ? "\n\n" : "\n");
}
start = builder.getSpanStart(copySpan);
end = builder.getSpanEnd(copySpan);
if (start == -1 || end == -1) return "";
builder = new SpannableStringBuilder(builder, start, end);
if (forCopy) {
QuoteSpan[] quoteSpans = builder.getSpans(0, builder.length(), QuoteSpan.class);
for (QuoteSpan quoteSpan : quoteSpans) {
builder.insert(builder.getSpanStart(quoteSpan), "> ");
}
}
return builder.toString();
}
@Override @Override
public String transformTextForCopy(CharSequence text, int start, int end) { public String transformTextForCopy(CharSequence text, int start, int end) {
if (text instanceof Spanned) { return text.toString().substring(start, end);
return transformText(text, start, end, true);
} else {
return text.toString().substring(start, end);
}
}
public interface OnQuoteListener {
public void onQuote(String text);
}
private class MessageBodyActionModeCallback implements ActionMode.Callback {
private final TextView textView;
public MessageBodyActionModeCallback(TextView textView) {
this.textView = textView;
}
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
if (onQuoteListener != null) {
int quoteResId = activity.getThemeResource(R.attr.icon_quote, R.drawable.ic_action_reply);
// 3rd item is placed after "copy" item
menu.add(0, android.R.id.button1, 3, R.string.quote).setIcon(quoteResId)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
}
return false;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
if (item.getItemId() == android.R.id.button1) {
int start = textView.getSelectionStart();
int end = textView.getSelectionEnd();
if (end > start) {
String text = transformText(textView.getText(), start, end, false);
if (onQuoteListener != null) {
onQuoteListener.onQuote(text);
}
mode.finish();
}
return true;
}
return false;
}
@Override
public void onDestroyActionMode(ActionMode mode) {}
} }
public void openDownloadable(Message message) { public void openDownloadable(Message message) {
@ -897,18 +703,23 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
mime = "*/*"; mime = "*/*";
} }
Uri uri; Uri uri;
try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
uri = FileBackend.getUriForFile(activity, file); try {
} catch (SecurityException e) { uri = FileProvider.getUriForFile(activity, FileBackend.CONVERSATIONS_FILE_PROVIDER, file);
Toast.makeText(activity, activity.getString(R.string.no_permission_to_access_x, file.getAbsolutePath()), Toast.LENGTH_SHORT).show(); } catch (IllegalArgumentException e) {
return; Toast.makeText(activity,activity.getString(R.string.no_permission_to_access_x,file.getAbsolutePath()), Toast.LENGTH_SHORT).show();
return;
}
openIntent.setDataAndType(uri, mime);
openIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
} else {
uri = Uri.fromFile(file);
} }
openIntent.setDataAndType(uri, mime); openIntent.setDataAndType(uri, mime);
openIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
PackageManager manager = activity.getPackageManager(); PackageManager manager = activity.getPackageManager();
List<ResolveInfo> info = manager.queryIntentActivities(openIntent, 0); List<ResolveInfo> info = manager.queryIntentActivities(openIntent, 0);
if (info.size() == 0) { if (info.size() == 0) {
openIntent.setDataAndType(uri,"*/*"); openIntent.setDataAndType(Uri.fromFile(file),"*/*");
} }
try { try {
getContext().startActivity(openIntent); getContext().startActivity(openIntent);

View file

@ -1,29 +0,0 @@
package eu.siacs.conversations.ui.text;
import android.text.TextPaint;
import android.text.style.MetricAffectingSpan;
public class DividerSpan extends MetricAffectingSpan {
private static final float PROPORTION = 0.3f;
private final boolean large;
public DividerSpan(boolean large) {
this.large = large;
}
public boolean isLarge() {
return large;
}
@Override
public void updateDrawState(TextPaint tp) {
tp.setTextSize(tp.getTextSize() * PROPORTION);
}
@Override
public void updateMeasureState(TextPaint p) {
p.setTextSize(p.getTextSize() * PROPORTION);
}
}

View file

@ -1,52 +0,0 @@
package eu.siacs.conversations.ui.text;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.text.Layout;
import android.text.TextPaint;
import android.text.style.CharacterStyle;
import android.text.style.LeadingMarginSpan;
import android.util.DisplayMetrics;
import android.util.TypedValue;
public class QuoteSpan extends CharacterStyle implements LeadingMarginSpan {
private final int color;
private final int width;
private final int paddingLeft;
private final int paddingRight;
private static final float WIDTH_SP = 2f;
private static final float PADDING_LEFT_SP = 1.5f;
private static final float PADDING_RIGHT_SP = 8f;
public QuoteSpan(int color, DisplayMetrics metrics) {
this.color = color;
this.width = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, WIDTH_SP, metrics);
this.paddingLeft = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, PADDING_LEFT_SP, metrics);
this.paddingRight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, PADDING_RIGHT_SP, metrics);
}
@Override
public void updateDrawState(TextPaint tp) {
tp.setColor(this.color);
}
@Override
public int getLeadingMargin(boolean first) {
return paddingLeft + width + paddingRight;
}
@Override
public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom,
CharSequence text, int start, int end, boolean first, Layout layout) {
Paint.Style style = p.getStyle();
int color = p.getColor();
p.setStyle(Paint.Style.FILL);
p.setColor(this.color);
c.drawRect(x + dir * paddingLeft, top, x + dir * (paddingLeft + width), bottom, p);
p.setStyle(style);
p.setColor(color);
}
}

View file

@ -69,8 +69,8 @@ public class ListSelectionManager {
private int futureSelectionStart; private int futureSelectionStart;
private int futureSelectionEnd; private int futureSelectionEnd;
public void onCreate(TextView textView, ActionMode.Callback additionalCallback) { public void onCreate(TextView textView) {
final CustomCallback callback = new CustomCallback(textView, additionalCallback); final CustomCallback callback = new CustomCallback(textView);
textView.setCustomSelectionActionModeCallback(callback); textView.setCustomSelectionActionModeCallback(callback);
} }
@ -112,12 +112,10 @@ public class ListSelectionManager {
private class CustomCallback implements ActionMode.Callback { private class CustomCallback implements ActionMode.Callback {
private final TextView textView; private final TextView textView;
private final ActionMode.Callback additionalCallback;
public Object identifier; public Object identifier;
public CustomCallback(TextView textView, ActionMode.Callback additionalCallback) { public CustomCallback(TextView textView) {
this.textView = textView; this.textView = textView;
this.additionalCallback = additionalCallback;
} }
@Override @Override
@ -125,33 +123,21 @@ public class ListSelectionManager {
selectionActionMode = mode; selectionActionMode = mode;
selectionIdentifier = identifier; selectionIdentifier = identifier;
selectionTextView = textView; selectionTextView = textView;
if (additionalCallback != null) {
additionalCallback.onCreateActionMode(mode, menu);
}
return true; return true;
} }
@Override @Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) { public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
if (additionalCallback != null) {
additionalCallback.onPrepareActionMode(mode, menu);
}
return true; return true;
} }
@Override @Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) { public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
if (additionalCallback != null && additionalCallback.onActionItemClicked(mode, item)) {
return true;
}
return false; return false;
} }
@Override @Override
public void onDestroyActionMode(ActionMode mode) { public void onDestroyActionMode(ActionMode mode) {
if (additionalCallback != null) {
additionalCallback.onDestroyActionMode(mode);
}
if (selectionActionMode == mode) { if (selectionActionMode == mode) {
selectionActionMode = null; selectionActionMode = null;
selectionIdentifier = null; selectionIdentifier = null;

View file

@ -206,13 +206,9 @@ public final class CryptoHelper {
} }
public static String getAccountFingerprint(Account account) { public static String getAccountFingerprint(Account account) {
return getFingerprint(account.getJid().toBareJid().toString());
}
public static String getFingerprint(String value) {
try { try {
MessageDigest md = MessageDigest.getInstance("SHA-256"); MessageDigest md = MessageDigest.getInstance("SHA-256");
return bytesToHex(md.digest(value.getBytes("UTF-8"))); return bytesToHex(md.digest(account.getJid().toBareJid().toString().getBytes("UTF-8")));
} catch (Exception e) { } catch (Exception e) {
return ""; return "";
} }

View file

@ -19,7 +19,6 @@ import java.net.UnknownHostException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Random; import java.util.Random;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.Map; import java.util.Map;
@ -149,29 +148,26 @@ public class DNSHelper {
for (Record[] rrset : new Record[][] { message.getAnswers(), message.getAdditionalResourceRecords() }) { for (Record[] rrset : new Record[][] { message.getAnswers(), message.getAdditionalResourceRecords() }) {
for (Record rr : rrset) { for (Record rr : rrset) {
Data d = rr.getPayload(); Data d = rr.getPayload();
final String name = rr.getName() != null ? rr.getName().toLowerCase(Locale.US) : null; if (d instanceof SRV && NameUtil.idnEquals(qname, rr.getName())) {
if (d instanceof SRV && NameUtil.idnEquals(qname, name)) {
SRV srv = (SRV) d; SRV srv = (SRV) d;
if (!priorities.containsKey(srv.getPriority())) { if (!priorities.containsKey(srv.getPriority())) {
priorities.put(srv.getPriority(),new ArrayList<TlsSrv>()); priorities.put(srv.getPriority(),new ArrayList<TlsSrv>());
} }
priorities.get(srv.getPriority()).add(new TlsSrv(srv, tls)); priorities.get(srv.getPriority()).add(new TlsSrv(srv, tls));
} else if (d instanceof SRV) {
Log.d(Config.LOGTAG,"found unrecognized SRV record with name: "+name);
} }
if (d instanceof A) { if (d instanceof A) {
A a = (A) d; A a = (A) d;
if (!ips4.containsKey(name)) { if (!ips4.containsKey(rr.getName())) {
ips4.put(name, new ArrayList<String>()); ips4.put(rr.getName(), new ArrayList<String>());
} }
ips4.get(name).add(a.toString()); ips4.get(rr.getName()).add(a.toString());
} }
if (d instanceof AAAA) { if (d instanceof AAAA) {
AAAA aaaa = (AAAA) d; AAAA aaaa = (AAAA) d;
if (!ips6.containsKey(name)) { if (!ips6.containsKey(rr.getName())) {
ips6.put(name, new ArrayList<String>()); ips6.put(rr.getName(), new ArrayList<String>());
} }
ips6.get(name).add("[" + aaaa.toString() + "]"); ips6.get(rr.getName()).add("[" + aaaa.toString() + "]");
} }
} }
} }
@ -181,8 +177,8 @@ public class DNSHelper {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
try { try {
client.setTimeout(Config.SOCKET_TIMEOUT * 1000); client.setTimeout(Config.SOCKET_TIMEOUT * 1000);
final String qname = "_xmpp-client._tcp." + host.toLowerCase(Locale.US); final String qname = "_xmpp-client._tcp." + host;
final String tlsQname = "_xmpps-client._tcp." + host.toLowerCase(Locale.US); final String tlsQname = "_xmpps-client._tcp." + host;
Log.d(Config.LOGTAG, "using dns server: " + dnsServer.getHostAddress() + " to look up " + host); Log.d(Config.LOGTAG, "using dns server: " + dnsServer.getHostAddress() + " to look up " + host);
final Map<Integer, List<TlsSrv>> priorities = new TreeMap<>(); final Map<Integer, List<TlsSrv>> priorities = new TreeMap<>();
@ -222,28 +218,27 @@ public class DNSHelper {
} }
for (final TlsSrv tlsSrv : result) { for (final TlsSrv tlsSrv : result) {
final SRV srv = tlsSrv.srv; final SRV srv = tlsSrv.srv;
final String name = srv.getName() != null ? srv.getName().toLowerCase(Locale.US) : null; if (ips6.containsKey(srv.getName())) {
if (ips6.containsKey(name)) { values.add(createNamePortBundle(srv.getName(),srv.getPort(),ips6, tlsSrv.tls));
values.add(createNamePortBundle(name,srv.getPort(),ips6, tlsSrv.tls));
} else { } else {
try { try {
DNSMessage response = client.query(name, TYPE.AAAA, CLASS.IN, dnsServer.getHostAddress()); DNSMessage response = client.query(srv.getName(), TYPE.AAAA, CLASS.IN, dnsServer.getHostAddress());
for (int i = 0; i < response.getAnswers().length; ++i) { for (int i = 0; i < response.getAnswers().length; ++i) {
values.add(createNamePortBundle(name, srv.getPort(), response.getAnswers()[i].getPayload(), tlsSrv.tls)); values.add(createNamePortBundle(srv.getName(), srv.getPort(), response.getAnswers()[i].getPayload(), tlsSrv.tls));
} }
} catch (SocketTimeoutException e) { } catch (SocketTimeoutException e) {
Log.d(Config.LOGTAG,"ignoring timeout exception when querying AAAA record on "+dnsServer.getHostAddress()); Log.d(Config.LOGTAG,"ignoring timeout exception when querying AAAA record on "+dnsServer.getHostAddress());
} }
} }
if (ips4.containsKey(name)) { if (ips4.containsKey(srv.getName())) {
values.add(createNamePortBundle(name,srv.getPort(),ips4, tlsSrv.tls)); values.add(createNamePortBundle(srv.getName(),srv.getPort(),ips4, tlsSrv.tls));
} else { } else {
DNSMessage response = client.query(name, TYPE.A, CLASS.IN, dnsServer.getHostAddress()); DNSMessage response = client.query(srv.getName(), TYPE.A, CLASS.IN, dnsServer.getHostAddress());
for(int i = 0; i < response.getAnswers().length; ++i) { for(int i = 0; i < response.getAnswers().length; ++i) {
values.add(createNamePortBundle(name,srv.getPort(),response.getAnswers()[i].getPayload(), tlsSrv.tls)); values.add(createNamePortBundle(srv.getName(),srv.getPort(),response.getAnswers()[i].getPayload(), tlsSrv.tls));
} }
} }
values.add(createNamePortBundle(name, srv.getPort(), tlsSrv.tls)); values.add(createNamePortBundle(srv.getName(), srv.getPort(), tlsSrv.tls));
} }
bundle.putParcelableArrayList("values", values); bundle.putParcelableArrayList("values", values);
} catch (SocketTimeoutException e) { } catch (SocketTimeoutException e) {

View file

@ -1,4 +0,0 @@
package eu.siacs.conversations.utils;
public class FileWriterException extends Exception {
}

View file

@ -14,9 +14,6 @@
* limitations under the License. * limitations under the License.
*/ */
package eu.siacs.conversations.utils; package eu.siacs.conversations.utils;
import android.content.Context;
import android.net.Uri;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
@ -487,22 +484,4 @@ public final class MimeUtils {
} }
return mimeTypeToExtensionMap.get(mimeType); return mimeTypeToExtensionMap.get(mimeType);
} }
public static String guessMimeTypeFromUri(Context context, Uri uri) {
// try the content resolver
String mimeType = context.getContentResolver().getType(uri);
// try the extension
if (mimeType == null && uri.getPath() != null) {
String path = uri.getPath();
int start = path.lastIndexOf('.') + 1;
if (start < path.length()) {
mimeType = MimeUtils.guessMimeTypeFromExtension(path.substring(start));
}
}
// sometimes this works (as with the commit content api)
if (mimeType == null) {
mimeType = uri.getQueryParameter("mimeType");
}
return mimeType;
}
} }

View file

@ -1,474 +0,0 @@
/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*
* Download latest version here: https://android.googlesource.com/platform/frameworks/base.git/+/master
*
*
*/
package eu.siacs.conversations.utils;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Commonly used regular expression patterns.
*/
public class Patterns {
/**
* Regular expression to match all IANA top-level domains.
* List accurate as of 2011/07/18. List taken from:
* http://data.iana.org/TLD/tlds-alpha-by-domain.txt
* This pattern is auto-generated by frameworks/ex/common/tools/make-iana-tld-pattern.py
*
* @deprecated Due to the recent profileration of gTLDs, this API is
* expected to become out-of-date very quickly. Therefore it is now
* deprecated.
*/
@Deprecated
public static final String TOP_LEVEL_DOMAIN_STR =
"((aero|arpa|asia|a[cdefgilmnoqrstuwxz])"
+ "|(biz|b[abdefghijmnorstvwyz])"
+ "|(cat|com|coop|c[acdfghiklmnoruvxyz])"
+ "|d[ejkmoz]"
+ "|(edu|e[cegrstu])"
+ "|f[ijkmor]"
+ "|(gov|g[abdefghilmnpqrstuwy])"
+ "|h[kmnrtu]"
+ "|(info|int|i[delmnoqrst])"
+ "|(jobs|j[emop])"
+ "|k[eghimnprwyz]"
+ "|l[abcikrstuvy]"
+ "|(mil|mobi|museum|m[acdeghklmnopqrstuvwxyz])"
+ "|(name|net|n[acefgilopruz])"
+ "|(org|om)"
+ "|(pro|p[aefghklmnrstwy])"
+ "|qa"
+ "|r[eosuw]"
+ "|s[abcdeghijklmnortuvyz]"
+ "|(tel|travel|t[cdfghjklmnoprtvwz])"
+ "|u[agksyz]"
+ "|v[aceginu]"
+ "|w[fs]"
+ "|(\u03b4\u03bf\u03ba\u03b9\u03bc\u03ae|\u0438\u0441\u043f\u044b\u0442\u0430\u043d\u0438\u0435|\u0440\u0444|\u0441\u0440\u0431|\u05d8\u05e2\u05e1\u05d8|\u0622\u0632\u0645\u0627\u06cc\u0634\u06cc|\u0625\u062e\u062a\u0628\u0627\u0631|\u0627\u0644\u0627\u0631\u062f\u0646|\u0627\u0644\u062c\u0632\u0627\u0626\u0631|\u0627\u0644\u0633\u0639\u0648\u062f\u064a\u0629|\u0627\u0644\u0645\u063a\u0631\u0628|\u0627\u0645\u0627\u0631\u0627\u062a|\u0628\u06be\u0627\u0631\u062a|\u062a\u0648\u0646\u0633|\u0633\u0648\u0631\u064a\u0629|\u0641\u0644\u0633\u0637\u064a\u0646|\u0642\u0637\u0631|\u0645\u0635\u0631|\u092a\u0930\u0940\u0915\u094d\u0937\u093e|\u092d\u093e\u0930\u0924|\u09ad\u09be\u09b0\u09a4|\u0a2d\u0a3e\u0a30\u0a24|\u0aad\u0abe\u0ab0\u0aa4|\u0b87\u0ba8\u0bcd\u0ba4\u0bbf\u0baf\u0bbe|\u0b87\u0bb2\u0b99\u0bcd\u0b95\u0bc8|\u0b9a\u0bbf\u0b99\u0bcd\u0b95\u0baa\u0bcd\u0baa\u0bc2\u0bb0\u0bcd|\u0baa\u0bb0\u0bbf\u0b9f\u0bcd\u0b9a\u0bc8|\u0c2d\u0c3e\u0c30\u0c24\u0c4d|\u0dbd\u0d82\u0d9a\u0dcf|\u0e44\u0e17\u0e22|\u30c6\u30b9\u30c8|\u4e2d\u56fd|\u4e2d\u570b|\u53f0\u6e7e|\u53f0\u7063|\u65b0\u52a0\u5761|\u6d4b\u8bd5|\u6e2c\u8a66|\u9999\u6e2f|\ud14c\uc2a4\ud2b8|\ud55c\uad6d|xn\\-\\-0zwm56d|xn\\-\\-11b5bs3a9aj6g|xn\\-\\-3e0b707e|xn\\-\\-45brj9c|xn\\-\\-80akhbyknj4f|xn\\-\\-90a3ac|xn\\-\\-9t4b11yi5a|xn\\-\\-clchc0ea0b2g2a9gcd|xn\\-\\-deba0ad|xn\\-\\-fiqs8s|xn\\-\\-fiqz9s|xn\\-\\-fpcrj9c3d|xn\\-\\-fzc2c9e2c|xn\\-\\-g6w251d|xn\\-\\-gecrj9c|xn\\-\\-h2brj9c|xn\\-\\-hgbk6aj7f53bba|xn\\-\\-hlcj6aya9esc7a|xn\\-\\-j6w193g|xn\\-\\-jxalpdlp|xn\\-\\-kgbechtv|xn\\-\\-kprw13d|xn\\-\\-kpry57d|xn\\-\\-lgbbat1ad8j|xn\\-\\-mgbaam7a8h|xn\\-\\-mgbayh7gpa|xn\\-\\-mgbbh1a71e|xn\\-\\-mgbc0a9azcg|xn\\-\\-mgberp4a5d4ar|xn\\-\\-o3cw4h|xn\\-\\-ogbpf8fl|xn\\-\\-p1ai|xn\\-\\-pgbs0dh|xn\\-\\-s9brj9c|xn\\-\\-wgbh1c|xn\\-\\-wgbl6a|xn\\-\\-xkc2al3hye2a|xn\\-\\-xkc2dl3a5ee0h|xn\\-\\-yfro4i67o|xn\\-\\-ygbi2ammx|xn\\-\\-zckzah|xxx)"
+ "|y[et]"
+ "|z[amw])";
/**
* Regular expression pattern to match all IANA top-level domains.
* @deprecated This API is deprecated. See {@link #TOP_LEVEL_DOMAIN_STR}.
*/
@Deprecated
public static final Pattern TOP_LEVEL_DOMAIN =
Pattern.compile(TOP_LEVEL_DOMAIN_STR);
/**
* Regular expression to match all IANA top-level domains for WEB_URL.
* List accurate as of 2011/07/18. List taken from:
* http://data.iana.org/TLD/tlds-alpha-by-domain.txt
* This pattern is auto-generated by frameworks/ex/common/tools/make-iana-tld-pattern.py
*
* @deprecated This API is deprecated. See {@link #TOP_LEVEL_DOMAIN_STR}.
*/
@Deprecated
public static final String TOP_LEVEL_DOMAIN_STR_FOR_WEB_URL =
"(?:"
+ "(?:aero|arpa|asia|a[cdefgilmnoqrstuwxz])"
+ "|(?:biz|b[abdefghijmnorstvwyz])"
+ "|(?:cat|com|coop|c[acdfghiklmnoruvxyz])"
+ "|d[ejkmoz]"
+ "|(?:edu|e[cegrstu])"
+ "|f[ijkmor]"
+ "|(?:gov|g[abdefghilmnpqrstuwy])"
+ "|h[kmnrtu]"
+ "|(?:info|int|i[delmnoqrst])"
+ "|(?:jobs|j[emop])"
+ "|k[eghimnprwyz]"
+ "|l[abcikrstuvy]"
+ "|(?:mil|mobi|museum|m[acdeghklmnopqrstuvwxyz])"
+ "|(?:name|net|n[acefgilopruz])"
+ "|(?:org|om)"
+ "|(?:pro|p[aefghklmnrstwy])"
+ "|qa"
+ "|r[eosuw]"
+ "|s[abcdeghijklmnortuvyz]"
+ "|(?:tel|travel|t[cdfghjklmnoprtvwz])"
+ "|u[agksyz]"
+ "|v[aceginu]"
+ "|w[fs]"
+ "|(?:\u03b4\u03bf\u03ba\u03b9\u03bc\u03ae|\u0438\u0441\u043f\u044b\u0442\u0430\u043d\u0438\u0435|\u0440\u0444|\u0441\u0440\u0431|\u05d8\u05e2\u05e1\u05d8|\u0622\u0632\u0645\u0627\u06cc\u0634\u06cc|\u0625\u062e\u062a\u0628\u0627\u0631|\u0627\u0644\u0627\u0631\u062f\u0646|\u0627\u0644\u062c\u0632\u0627\u0626\u0631|\u0627\u0644\u0633\u0639\u0648\u062f\u064a\u0629|\u0627\u0644\u0645\u063a\u0631\u0628|\u0627\u0645\u0627\u0631\u0627\u062a|\u0628\u06be\u0627\u0631\u062a|\u062a\u0648\u0646\u0633|\u0633\u0648\u0631\u064a\u0629|\u0641\u0644\u0633\u0637\u064a\u0646|\u0642\u0637\u0631|\u0645\u0635\u0631|\u092a\u0930\u0940\u0915\u094d\u0937\u093e|\u092d\u093e\u0930\u0924|\u09ad\u09be\u09b0\u09a4|\u0a2d\u0a3e\u0a30\u0a24|\u0aad\u0abe\u0ab0\u0aa4|\u0b87\u0ba8\u0bcd\u0ba4\u0bbf\u0baf\u0bbe|\u0b87\u0bb2\u0b99\u0bcd\u0b95\u0bc8|\u0b9a\u0bbf\u0b99\u0bcd\u0b95\u0baa\u0bcd\u0baa\u0bc2\u0bb0\u0bcd|\u0baa\u0bb0\u0bbf\u0b9f\u0bcd\u0b9a\u0bc8|\u0c2d\u0c3e\u0c30\u0c24\u0c4d|\u0dbd\u0d82\u0d9a\u0dcf|\u0e44\u0e17\u0e22|\u30c6\u30b9\u30c8|\u4e2d\u56fd|\u4e2d\u570b|\u53f0\u6e7e|\u53f0\u7063|\u65b0\u52a0\u5761|\u6d4b\u8bd5|\u6e2c\u8a66|\u9999\u6e2f|\ud14c\uc2a4\ud2b8|\ud55c\uad6d|xn\\-\\-0zwm56d|xn\\-\\-11b5bs3a9aj6g|xn\\-\\-3e0b707e|xn\\-\\-45brj9c|xn\\-\\-80akhbyknj4f|xn\\-\\-90a3ac|xn\\-\\-9t4b11yi5a|xn\\-\\-clchc0ea0b2g2a9gcd|xn\\-\\-deba0ad|xn\\-\\-fiqs8s|xn\\-\\-fiqz9s|xn\\-\\-fpcrj9c3d|xn\\-\\-fzc2c9e2c|xn\\-\\-g6w251d|xn\\-\\-gecrj9c|xn\\-\\-h2brj9c|xn\\-\\-hgbk6aj7f53bba|xn\\-\\-hlcj6aya9esc7a|xn\\-\\-j6w193g|xn\\-\\-jxalpdlp|xn\\-\\-kgbechtv|xn\\-\\-kprw13d|xn\\-\\-kpry57d|xn\\-\\-lgbbat1ad8j|xn\\-\\-mgbaam7a8h|xn\\-\\-mgbayh7gpa|xn\\-\\-mgbbh1a71e|xn\\-\\-mgbc0a9azcg|xn\\-\\-mgberp4a5d4ar|xn\\-\\-o3cw4h|xn\\-\\-ogbpf8fl|xn\\-\\-p1ai|xn\\-\\-pgbs0dh|xn\\-\\-s9brj9c|xn\\-\\-wgbh1c|xn\\-\\-wgbl6a|xn\\-\\-xkc2al3hye2a|xn\\-\\-xkc2dl3a5ee0h|xn\\-\\-yfro4i67o|xn\\-\\-ygbi2ammx|xn\\-\\-zckzah|xxx)"
+ "|y[et]"
+ "|z[amw]))";
/**
* Regular expression to match all IANA top-level domains.
*
* List accurate as of 2015/11/24. List taken from:
* http://data.iana.org/TLD/tlds-alpha-by-domain.txt
* This pattern is auto-generated by frameworks/ex/common/tools/make-iana-tld-pattern.py
*
* @hide
*/
static final String IANA_TOP_LEVEL_DOMAINS =
"(?:"
+ "(?:aaa|aarp|abb|abbott|abogado|academy|accenture|accountant|accountants|aco|active"
+ "|actor|ads|adult|aeg|aero|afl|agency|aig|airforce|airtel|allfinanz|alsace|amica|amsterdam"
+ "|android|apartments|app|apple|aquarelle|aramco|archi|army|arpa|arte|asia|associates"
+ "|attorney|auction|audio|auto|autos|axa|azure|a[cdefgilmoqrstuwxz])"
+ "|(?:band|bank|bar|barcelona|barclaycard|barclays|bargains|bauhaus|bayern|bbc|bbva"
+ "|bcn|beats|beer|bentley|berlin|best|bet|bharti|bible|bid|bike|bing|bingo|bio|biz|black"
+ "|blackfriday|bloomberg|blue|bms|bmw|bnl|bnpparibas|boats|bom|bond|boo|boots|boutique"
+ "|bradesco|bridgestone|broadway|broker|brother|brussels|budapest|build|builders|business"
+ "|buzz|bzh|b[abdefghijmnorstvwyz])"
+ "|(?:cab|cafe|cal|camera|camp|cancerresearch|canon|capetown|capital|car|caravan|cards"
+ "|care|career|careers|cars|cartier|casa|cash|casino|cat|catering|cba|cbn|ceb|center|ceo"
+ "|cern|cfa|cfd|chanel|channel|chat|cheap|chloe|christmas|chrome|church|cipriani|cisco"
+ "|citic|city|cityeats|claims|cleaning|click|clinic|clothing|cloud|club|clubmed|coach"
+ "|codes|coffee|college|cologne|com|commbank|community|company|computer|comsec|condos"
+ "|construction|consulting|contractors|cooking|cool|coop|corsica|country|coupons|courses"
+ "|credit|creditcard|creditunion|cricket|crown|crs|cruises|csc|cuisinella|cymru|cyou|c[acdfghiklmnoruvwxyz])"
+ "|(?:dabur|dad|dance|date|dating|datsun|day|dclk|deals|degree|delivery|dell|delta"
+ "|democrat|dental|dentist|desi|design|dev|diamonds|diet|digital|direct|directory|discount"
+ "|dnp|docs|dog|doha|domains|doosan|download|drive|durban|dvag|d[ejkmoz])"
+ "|(?:earth|eat|edu|education|email|emerck|energy|engineer|engineering|enterprises"
+ "|epson|equipment|erni|esq|estate|eurovision|eus|events|everbank|exchange|expert|exposed"
+ "|express|e[cegrstu])"
+ "|(?:fage|fail|fairwinds|faith|family|fan|fans|farm|fashion|feedback|ferrero|film"
+ "|final|finance|financial|firmdale|fish|fishing|fit|fitness|flights|florist|flowers|flsmidth"
+ "|fly|foo|football|forex|forsale|forum|foundation|frl|frogans|fund|furniture|futbol|fyi"
+ "|f[ijkmor])"
+ "|(?:gal|gallery|game|garden|gbiz|gdn|gea|gent|genting|ggee|gift|gifts|gives|giving"
+ "|glass|gle|global|globo|gmail|gmo|gmx|gold|goldpoint|golf|goo|goog|google|gop|gov|grainger"
+ "|graphics|gratis|green|gripe|group|gucci|guge|guide|guitars|guru|g[abdefghilmnpqrstuwy])"
+ "|(?:hamburg|hangout|haus|healthcare|help|here|hermes|hiphop|hitachi|hiv|hockey|holdings"
+ "|holiday|homedepot|homes|honda|horse|host|hosting|hoteles|hotmail|house|how|hsbc|hyundai"
+ "|h[kmnrtu])"
+ "|(?:ibm|icbc|ice|icu|ifm|iinet|immo|immobilien|industries|infiniti|info|ing|ink|institute"
+ "|insure|int|international|investments|ipiranga|irish|ist|istanbul|itau|iwc|i[delmnoqrst])"
+ "|(?:jaguar|java|jcb|jetzt|jewelry|jlc|jll|jobs|joburg|jprs|juegos|j[emop])"
+ "|(?:kaufen|kddi|kia|kim|kinder|kitchen|kiwi|koeln|komatsu|krd|kred|kyoto|k[eghimnprwyz])"
+ "|(?:lacaixa|lancaster|land|landrover|lasalle|lat|latrobe|law|lawyer|lds|lease|leclerc"
+ "|legal|lexus|lgbt|liaison|lidl|life|lifestyle|lighting|limited|limo|linde|link|live"
+ "|lixil|loan|loans|lol|london|lotte|lotto|love|ltd|ltda|lupin|luxe|luxury|l[abcikrstuvy])"
+ "|(?:madrid|maif|maison|man|management|mango|market|marketing|markets|marriott|mba"
+ "|media|meet|melbourne|meme|memorial|men|menu|meo|miami|microsoft|mil|mini|mma|mobi|moda"
+ "|moe|moi|mom|monash|money|montblanc|mormon|mortgage|moscow|motorcycles|mov|movie|movistar"
+ "|mtn|mtpc|mtr|museum|mutuelle|m[acdeghklmnopqrstuvwxyz])"
+ "|(?:nadex|nagoya|name|navy|nec|net|netbank|network|neustar|new|news|nexus|ngo|nhk"
+ "|nico|ninja|nissan|nokia|nra|nrw|ntt|nyc|n[acefgilopruz])"
+ "|(?:obi|office|okinawa|omega|one|ong|onl|online|ooo|oracle|orange|org|organic|osaka"
+ "|otsuka|ovh|om)"
+ "|(?:page|panerai|paris|partners|parts|party|pet|pharmacy|philips|photo|photography"
+ "|photos|physio|piaget|pics|pictet|pictures|ping|pink|pizza|place|play|playstation|plumbing"
+ "|plus|pohl|poker|porn|post|praxi|press|pro|prod|productions|prof|properties|property"
+ "|protection|pub|p[aefghklmnrstwy])"
+ "|(?:qpon|quebec|qa)"
+ "|(?:racing|realtor|realty|recipes|red|redstone|rehab|reise|reisen|reit|ren|rent|rentals"
+ "|repair|report|republican|rest|restaurant|review|reviews|rich|ricoh|rio|rip|rocher|rocks"
+ "|rodeo|rsvp|ruhr|run|rwe|ryukyu|r[eosuw])"
+ "|(?:saarland|sakura|sale|samsung|sandvik|sandvikcoromant|sanofi|sap|sapo|sarl|saxo"
+ "|sbs|sca|scb|schmidt|scholarships|school|schule|schwarz|science|scor|scot|seat|security"
+ "|seek|sener|services|seven|sew|sex|sexy|shiksha|shoes|show|shriram|singles|site|ski"
+ "|sky|skype|sncf|soccer|social|software|sohu|solar|solutions|sony|soy|space|spiegel|spreadbetting"
+ "|srl|stada|starhub|statoil|stc|stcgroup|stockholm|studio|study|style|sucks|supplies"
+ "|supply|support|surf|surgery|suzuki|swatch|swiss|sydney|systems|s[abcdeghijklmnortuvxyz])"
+ "|(?:tab|taipei|tatamotors|tatar|tattoo|tax|taxi|team|tech|technology|tel|telefonica"
+ "|temasek|tennis|thd|theater|theatre|tickets|tienda|tips|tires|tirol|today|tokyo|tools"
+ "|top|toray|toshiba|tours|town|toyota|toys|trade|trading|training|travel|trust|tui|t[cdfghjklmnortvwz])"
+ "|(?:ubs|university|uno|uol|u[agksyz])"
+ "|(?:vacations|vana|vegas|ventures|versicherung|vet|viajes|video|villas|vin|virgin"
+ "|vision|vista|vistaprint|viva|vlaanderen|vodka|vote|voting|voto|voyage|v[aceginu])"
+ "|(?:wales|walter|wang|watch|webcam|website|wed|wedding|weir|whoswho|wien|wiki|williamhill"
+ "|win|windows|wine|wme|work|works|world|wtc|wtf|w[fs])"
+ "|(?:\u03b5\u03bb|\u0431\u0435\u043b|\u0434\u0435\u0442\u0438|\u043a\u043e\u043c|\u043c\u043a\u0434"
+ "|\u043c\u043e\u043d|\u043c\u043e\u0441\u043a\u0432\u0430|\u043e\u043d\u043b\u0430\u0439\u043d"
+ "|\u043e\u0440\u0433|\u0440\u0443\u0441|\u0440\u0444|\u0441\u0430\u0439\u0442|\u0441\u0440\u0431"
+ "|\u0443\u043a\u0440|\u049b\u0430\u0437|\u0570\u0561\u0575|\u05e7\u05d5\u05dd|\u0627\u0631\u0627\u0645\u0643\u0648"
+ "|\u0627\u0644\u0627\u0631\u062f\u0646|\u0627\u0644\u062c\u0632\u0627\u0626\u0631|\u0627\u0644\u0633\u0639\u0648\u062f\u064a\u0629"
+ "|\u0627\u0644\u0645\u063a\u0631\u0628|\u0627\u0645\u0627\u0631\u0627\u062a|\u0627\u06cc\u0631\u0627\u0646"
+ "|\u0628\u0627\u0632\u0627\u0631|\u0628\u06be\u0627\u0631\u062a|\u062a\u0648\u0646\u0633"
+ "|\u0633\u0648\u062f\u0627\u0646|\u0633\u0648\u0631\u064a\u0629|\u0634\u0628\u0643\u0629"
+ "|\u0639\u0631\u0627\u0642|\u0639\u0645\u0627\u0646|\u0641\u0644\u0633\u0637\u064a\u0646"
+ "|\u0642\u0637\u0631|\u0643\u0648\u0645|\u0645\u0635\u0631|\u0645\u0644\u064a\u0633\u064a\u0627"
+ "|\u0645\u0648\u0642\u0639|\u0915\u0949\u092e|\u0928\u0947\u091f|\u092d\u093e\u0930\u0924"
+ "|\u0938\u0902\u0917\u0920\u0928|\u09ad\u09be\u09b0\u09a4|\u0a2d\u0a3e\u0a30\u0a24|\u0aad\u0abe\u0ab0\u0aa4"
+ "|\u0b87\u0ba8\u0bcd\u0ba4\u0bbf\u0baf\u0bbe|\u0b87\u0bb2\u0b99\u0bcd\u0b95\u0bc8|\u0b9a\u0bbf\u0b99\u0bcd\u0b95\u0baa\u0bcd\u0baa\u0bc2\u0bb0\u0bcd"
+ "|\u0c2d\u0c3e\u0c30\u0c24\u0c4d|\u0dbd\u0d82\u0d9a\u0dcf|\u0e04\u0e2d\u0e21|\u0e44\u0e17\u0e22"
+ "|\u10d2\u10d4|\u307f\u3093\u306a|\u30b0\u30fc\u30b0\u30eb|\u30b3\u30e0|\u4e16\u754c"
+ "|\u4e2d\u4fe1|\u4e2d\u56fd|\u4e2d\u570b|\u4e2d\u6587\u7f51|\u4f01\u4e1a|\u4f5b\u5c71"
+ "|\u4fe1\u606f|\u5065\u5eb7|\u516b\u5366|\u516c\u53f8|\u516c\u76ca|\u53f0\u6e7e|\u53f0\u7063"
+ "|\u5546\u57ce|\u5546\u5e97|\u5546\u6807|\u5728\u7ebf|\u5927\u62ff|\u5a31\u4e50|\u5de5\u884c"
+ "|\u5e7f\u4e1c|\u6148\u5584|\u6211\u7231\u4f60|\u624b\u673a|\u653f\u52a1|\u653f\u5e9c"
+ "|\u65b0\u52a0\u5761|\u65b0\u95fb|\u65f6\u5c1a|\u673a\u6784|\u6de1\u9a6c\u9521|\u6e38\u620f"
+ "|\u70b9\u770b|\u79fb\u52a8|\u7ec4\u7ec7\u673a\u6784|\u7f51\u5740|\u7f51\u5e97|\u7f51\u7edc"
+ "|\u8c37\u6b4c|\u96c6\u56e2|\u98de\u5229\u6d66|\u9910\u5385|\u9999\u6e2f|\ub2f7\ub137"
+ "|\ub2f7\ucef4|\uc0bc\uc131|\ud55c\uad6d|xbox"
+ "|xerox|xin|xn\\-\\-11b4c3d|xn\\-\\-1qqw23a|xn\\-\\-30rr7y|xn\\-\\-3bst00m|xn\\-\\-3ds443g"
+ "|xn\\-\\-3e0b707e|xn\\-\\-3pxu8k|xn\\-\\-42c2d9a|xn\\-\\-45brj9c|xn\\-\\-45q11c|xn\\-\\-4gbrim"
+ "|xn\\-\\-55qw42g|xn\\-\\-55qx5d|xn\\-\\-6frz82g|xn\\-\\-6qq986b3xl|xn\\-\\-80adxhks"
+ "|xn\\-\\-80ao21a|xn\\-\\-80asehdb|xn\\-\\-80aswg|xn\\-\\-90a3ac|xn\\-\\-90ais|xn\\-\\-9dbq2a"
+ "|xn\\-\\-9et52u|xn\\-\\-b4w605ferd|xn\\-\\-c1avg|xn\\-\\-c2br7g|xn\\-\\-cg4bki|xn\\-\\-clchc0ea0b2g2a9gcd"
+ "|xn\\-\\-czr694b|xn\\-\\-czrs0t|xn\\-\\-czru2d|xn\\-\\-d1acj3b|xn\\-\\-d1alf|xn\\-\\-efvy88h"
+ "|xn\\-\\-estv75g|xn\\-\\-fhbei|xn\\-\\-fiq228c5hs|xn\\-\\-fiq64b|xn\\-\\-fiqs8s|xn\\-\\-fiqz9s"
+ "|xn\\-\\-fjq720a|xn\\-\\-flw351e|xn\\-\\-fpcrj9c3d|xn\\-\\-fzc2c9e2c|xn\\-\\-gecrj9c"
+ "|xn\\-\\-h2brj9c|xn\\-\\-hxt814e|xn\\-\\-i1b6b1a6a2e|xn\\-\\-imr513n|xn\\-\\-io0a7i"
+ "|xn\\-\\-j1aef|xn\\-\\-j1amh|xn\\-\\-j6w193g|xn\\-\\-kcrx77d1x4a|xn\\-\\-kprw13d|xn\\-\\-kpry57d"
+ "|xn\\-\\-kput3i|xn\\-\\-l1acc|xn\\-\\-lgbbat1ad8j|xn\\-\\-mgb9awbf|xn\\-\\-mgba3a3ejt"
+ "|xn\\-\\-mgba3a4f16a|xn\\-\\-mgbaam7a8h|xn\\-\\-mgbab2bd|xn\\-\\-mgbayh7gpa|xn\\-\\-mgbbh1a71e"
+ "|xn\\-\\-mgbc0a9azcg|xn\\-\\-mgberp4a5d4ar|xn\\-\\-mgbpl2fh|xn\\-\\-mgbtx2b|xn\\-\\-mgbx4cd0ab"
+ "|xn\\-\\-mk1bu44c|xn\\-\\-mxtq1m|xn\\-\\-ngbc5azd|xn\\-\\-node|xn\\-\\-nqv7f|xn\\-\\-nqv7fs00ema"
+ "|xn\\-\\-nyqy26a|xn\\-\\-o3cw4h|xn\\-\\-ogbpf8fl|xn\\-\\-p1acf|xn\\-\\-p1ai|xn\\-\\-pgbs0dh"
+ "|xn\\-\\-pssy2u|xn\\-\\-q9jyb4c|xn\\-\\-qcka1pmc|xn\\-\\-qxam|xn\\-\\-rhqv96g|xn\\-\\-s9brj9c"
+ "|xn\\-\\-ses554g|xn\\-\\-t60b56a|xn\\-\\-tckwe|xn\\-\\-unup4y|xn\\-\\-vermgensberater\\-ctb"
+ "|xn\\-\\-vermgensberatung\\-pwb|xn\\-\\-vhquv|xn\\-\\-vuq861b|xn\\-\\-wgbh1c|xn\\-\\-wgbl6a"
+ "|xn\\-\\-xhq521b|xn\\-\\-xkc2al3hye2a|xn\\-\\-xkc2dl3a5ee0h|xn\\-\\-y9a3aq|xn\\-\\-yfro4i67o"
+ "|xn\\-\\-ygbi2ammx|xn\\-\\-zfr164b|xperia|xxx|xyz)"
+ "|(?:yachts|yamaxun|yandex|yodobashi|yoga|yokohama|youtube|y[et])"
+ "|(?:zara|zip|zone|zuerich|z[amw]))";
/**
* Kept for backward compatibility reasons.
*
* @deprecated Deprecated since it does not include all IRI characters defined in RFC 3987
*/
@Deprecated
public static final String GOOD_IRI_CHAR =
"a-zA-Z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF";
public static final Pattern IP_ADDRESS
= Pattern.compile(
"((25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9])\\.(25[0-5]|2[0-4]"
+ "[0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1]"
+ "[0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}"
+ "|[1-9][0-9]|[0-9]))");
/**
* Valid UCS characters defined in RFC 3987. Excludes space characters.
*/
private static final String UCS_CHAR = "[" +
"\u00A0-\uD7FF" +
"\uF900-\uFDCF" +
"\uFDF0-\uFFEF" +
"\uD800\uDC00-\uD83F\uDFFD" +
"\uD840\uDC00-\uD87F\uDFFD" +
"\uD880\uDC00-\uD8BF\uDFFD" +
"\uD8C0\uDC00-\uD8FF\uDFFD" +
"\uD900\uDC00-\uD93F\uDFFD" +
"\uD940\uDC00-\uD97F\uDFFD" +
"\uD980\uDC00-\uD9BF\uDFFD" +
"\uD9C0\uDC00-\uD9FF\uDFFD" +
"\uDA00\uDC00-\uDA3F\uDFFD" +
"\uDA40\uDC00-\uDA7F\uDFFD" +
"\uDA80\uDC00-\uDABF\uDFFD" +
"\uDAC0\uDC00-\uDAFF\uDFFD" +
"\uDB00\uDC00-\uDB3F\uDFFD" +
"\uDB44\uDC00-\uDB7F\uDFFD" +
"&&[^\u00A0[\u2000-\u200A]\u2028\u2029\u202F\u3000]]";
/**
* Valid characters for IRI label defined in RFC 3987.
*/
private static final String LABEL_CHAR = "a-zA-Z0-9" + UCS_CHAR;
/**
* Valid characters for IRI TLD defined in RFC 3987.
*/
private static final String TLD_CHAR = "a-zA-Z" + UCS_CHAR;
/**
* RFC 1035 Section 2.3.4 limits the labels to a maximum 63 octets.
*/
private static final String IRI_LABEL =
"[" + LABEL_CHAR + "](?:[" + LABEL_CHAR + "\\-]{0,61}[" + LABEL_CHAR + "]){0,1}";
/**
* RFC 3492 references RFC 1034 and limits Punycode algorithm output to 63 characters.
*/
private static final String PUNYCODE_TLD = "xn\\-\\-[\\w\\-]{0,58}\\w";
private static final String TLD = "(" + PUNYCODE_TLD + "|" + "[" + TLD_CHAR + "]{2,63}" +")";
private static final String HOST_NAME = "(" + IRI_LABEL + "\\.)+" + TLD;
public static final Pattern DOMAIN_NAME
= Pattern.compile("(" + HOST_NAME + "|" + IP_ADDRESS + ")");
private static final String PROTOCOL = "(?i:http|https|rtsp):\\/\\/";
/* A word boundary or end of input. This is to stop foo.sure from matching as foo.su */
private static final String WORD_BOUNDARY = "(?:\\b|$|^)";
private static final String USER_INFO = "(?:[a-zA-Z0-9\\$\\-\\_\\.\\+\\!\\*\\'\\(\\)"
+ "\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,64}(?:\\:(?:[a-zA-Z0-9\\$\\-\\_"
+ "\\.\\+\\!\\*\\'\\(\\)\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,25})?\\@";
private static final String PORT_NUMBER = "\\:\\d{1,5}";
private static final String PATH_AND_QUERY = "\\/(?:(?:[" + LABEL_CHAR
+ "\\;\\/\\?\\:\\@\\&\\=\\#\\~" // plus optional query params
+ "\\-\\.\\+\\!\\*\\'\\(\\)\\,\\_])|(?:\\%[a-fA-F0-9]{2}))*";
/**
* Regular expression pattern to match most part of RFC 3987
* Internationalized URLs, aka IRIs.
*/
public static final Pattern WEB_URL = Pattern.compile("("
+ "("
+ "(?:" + PROTOCOL + "(?:" + USER_INFO + ")?" + ")?"
+ "(?:" + DOMAIN_NAME + ")"
+ "(?:" + PORT_NUMBER + ")?"
+ ")"
+ "(" + PATH_AND_QUERY + ")?"
+ WORD_BOUNDARY
+ ")");
/**
* Regular expression that matches known TLDs and punycode TLDs
*/
private static final String STRICT_TLD = "(?:" +
IANA_TOP_LEVEL_DOMAINS + "|" + PUNYCODE_TLD + ")";
/**
* Regular expression that matches host names using {@link #STRICT_TLD}
*/
private static final String STRICT_HOST_NAME = "(?:(?:" + IRI_LABEL + "\\.)+"
+ STRICT_TLD + ")";
/**
* Regular expression that matches domain names using either {@link #STRICT_HOST_NAME} or
* {@link #IP_ADDRESS}
*/
private static final Pattern STRICT_DOMAIN_NAME
= Pattern.compile("(?:" + STRICT_HOST_NAME + "|" + IP_ADDRESS + ")");
/**
* Regular expression that matches domain names without a TLD
*/
private static final String RELAXED_DOMAIN_NAME =
"(?:" + "(?:" + IRI_LABEL + "(?:\\.(?=\\S))" +"?)+" + "|" + IP_ADDRESS + ")";
/**
* Regular expression to match strings that do not start with a supported protocol. The TLDs
* are expected to be one of the known TLDs.
*/
private static final String WEB_URL_WITHOUT_PROTOCOL = "("
+ WORD_BOUNDARY
+ "(?<!:\\/\\/)"
+ "("
+ "(?:" + STRICT_DOMAIN_NAME + ")"
+ "(?:" + PORT_NUMBER + ")?"
+ ")"
+ "(?:" + PATH_AND_QUERY + ")?"
+ WORD_BOUNDARY
+ ")";
/**
* Regular expression to match strings that start with a supported protocol. Rules for domain
* names and TLDs are more relaxed. TLDs are optional.
*/
private static final String WEB_URL_WITH_PROTOCOL = "("
+ WORD_BOUNDARY
+ "(?:"
+ "(?:" + PROTOCOL + "(?:" + USER_INFO + ")?" + ")"
+ "(?:" + RELAXED_DOMAIN_NAME + ")?"
+ "(?:" + PORT_NUMBER + ")?"
+ ")"
+ "(?:" + PATH_AND_QUERY + ")?"
+ WORD_BOUNDARY
+ ")";
/**
* Regular expression pattern to match IRIs. If a string starts with http(s):// the expression
* tries to match the URL structure with a relaxed rule for TLDs. If the string does not start
* with http(s):// the TLDs are expected to be one of the known TLDs.
*
* @hide
*/
public static final Pattern AUTOLINK_WEB_URL = Pattern.compile(
"(" + WEB_URL_WITH_PROTOCOL + "|" + WEB_URL_WITHOUT_PROTOCOL + ")");
/**
* Regular expression for valid email characters. Does not include some of the valid characters
* defined in RFC5321: #&~!^`{}/=$*?|
*/
private static final String EMAIL_CHAR = LABEL_CHAR + "\\+\\-_%'";
/**
* Regular expression for local part of an email address. RFC5321 section 4.5.3.1.1 limits
* the local part to be at most 64 octets.
*/
private static final String EMAIL_ADDRESS_LOCAL_PART =
"[" + EMAIL_CHAR + "]" + "(?:[" + EMAIL_CHAR + "\\.]{1,62}[" + EMAIL_CHAR + "])?";
/**
* Regular expression for the domain part of an email address. RFC5321 section 4.5.3.1.2 limits
* the domain to be at most 255 octets.
*/
private static final String EMAIL_ADDRESS_DOMAIN =
"(?=.{1,255}(?:\\s|$|^))" + HOST_NAME;
/**
* Regular expression pattern to match email addresses. It excludes double quoted local parts
* and the special characters #&~!^`{}/=$*?| that are included in RFC5321.
* @hide
*/
public static final Pattern AUTOLINK_EMAIL_ADDRESS = Pattern.compile("(" + WORD_BOUNDARY +
"(?:" + EMAIL_ADDRESS_LOCAL_PART + "@" + EMAIL_ADDRESS_DOMAIN + ")" +
WORD_BOUNDARY + ")"
);
public static final Pattern EMAIL_ADDRESS
= Pattern.compile(
"[a-zA-Z0-9\\+\\.\\_\\%\\-\\+]{1,256}" +
"\\@" +
"[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}" +
"(" +
"\\." +
"[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25}" +
")+"
);
/**
* This pattern is intended for searching for things that look like they
* might be phone numbers in arbitrary text, not for validating whether
* something is in fact a phone number. It will miss many things that
* are legitimate phone numbers.
*
* <p> The pattern matches the following:
* <ul>
* <li>Optionally, a + sign followed immediately by one or more digits. Spaces, dots, or dashes
* may follow.
* <li>Optionally, sets of digits in parentheses, separated by spaces, dots, or dashes.
* <li>A string starting and ending with a digit, containing digits, spaces, dots, and/or dashes.
* </ul>
*/
public static final Pattern PHONE
= Pattern.compile( // sdd = space, dot, or dash
"(\\+[0-9]+[\\- \\.]*)?" // +<digits><sdd>*
+ "(\\([0-9]+\\)[\\- \\.]*)?" // (<digits>)<sdd>*
+ "([0-9][0-9\\- \\.]+[0-9])"); // <digit><digit|sdd>+<digit>
/**
* Convenience method to take all of the non-null matching groups in a
* regex Matcher and return them as a concatenated string.
*
* @param matcher The Matcher object from which grouped text will
* be extracted
*
* @return A String comprising all of the non-null matched
* groups concatenated together
*/
public static final String concatGroups(Matcher matcher) {
StringBuilder b = new StringBuilder();
final int numGroups = matcher.groupCount();
for (int i = 1; i <= numGroups; i++) {
String s = matcher.group(i);
if (s != null) {
b.append(s);
}
}
return b.toString();
}
/**
* Convenience method to return only the digits and plus signs
* in the matching string.
*
* @param matcher The Matcher object from which digits and plus will
* be extracted
*
* @return A String comprising all of the digits and plus in
* the match
*/
public static final String digitsAndPlusOnly(Matcher matcher) {
StringBuilder buffer = new StringBuilder();
String matchingRegion = matcher.group();
for (int i = 0, size = matchingRegion.length(); i < size; i++) {
char character = matchingRegion.charAt(i);
if (character == '+' || Character.isDigit(character)) {
buffer.append(character);
}
}
return buffer.toString();
}
/**
* Do not create this static utility class.
*/
private Patterns() {}
}

View file

@ -1,69 +0,0 @@
package eu.siacs.conversations.utils;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.X509TrustManager;
public class TLSSocketFactory extends SSLSocketFactory {
private final SSLSocketFactory internalSSLSocketFactory;
public TLSSocketFactory(X509TrustManager[] trustManager, SecureRandom random) throws KeyManagementException, NoSuchAlgorithmException {
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, trustManager, random);
this.internalSSLSocketFactory = context.getSocketFactory();
}
@Override
public String[] getDefaultCipherSuites() {
return CryptoHelper.getOrderedCipherSuites(internalSSLSocketFactory.getDefaultCipherSuites());
}
@Override
public String[] getSupportedCipherSuites() {
return internalSSLSocketFactory.getSupportedCipherSuites();
}
@Override
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
return enableTLSOnSocket(internalSSLSocketFactory.createSocket(s, host, port, autoClose));
}
@Override
public Socket createSocket(String host, int port) throws IOException {
return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port));
}
@Override
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException {
return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port, localHost, localPort));
}
@Override
public Socket createSocket(InetAddress host, int port) throws IOException {
return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port));
}
@Override
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
return enableTLSOnSocket(internalSSLSocketFactory.createSocket(address, port, localAddress, localPort));
}
private static Socket enableTLSOnSocket(Socket socket) {
if(socket != null && (socket instanceof SSLSocket)) {
try {
SSLSocketHelper.setSecurity((SSLSocket) socket);
} catch (NoSuchAlgorithmException e) {
//ignoring
}
}
return socket;
}
}

View file

@ -9,7 +9,6 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Calendar; import java.util.Calendar;
import java.util.Date; import java.util.Date;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
@ -30,9 +29,9 @@ public class UIHelper {
private static String HEAVY_BLACK_HEART_SUIT = "\u2764"; private static String HEAVY_BLACK_HEART_SUIT = "\u2764";
private static String WHITE_HEART_SUIT = "\u2661"; private static String WHITE_HEART_SUIT = "\u2661";
public static final List<String> HEARTS = Arrays.asList(BLACK_HEART_SUIT,HEAVY_BLACK_HEART_SUIT,WHITE_HEART_SUIT); public static final ArrayList<String> HEARTS = new ArrayList<>(Arrays.asList(BLACK_HEART_SUIT,HEAVY_BLACK_HEART_SUIT,WHITE_HEART_SUIT));
private static final List<String> LOCATION_QUESTIONS = Arrays.asList( private static final ArrayList<String> LOCATION_QUESTIONS = new ArrayList<>(Arrays.asList(
"where are you", //en "where are you", //en
"where are you now", //en "where are you now", //en
"where are you right now", //en "where are you right now", //en
@ -50,9 +49,7 @@ public class UIHelper {
"wo seid ihr gerade", //de "wo seid ihr gerade", //de
"dónde estás", //es "dónde estás", //es
"donde estas" //es "donde estas" //es
); ));
private static final List<Character> PUNCTIONATION = Arrays.asList('.',',','?','!',';',':');
private static final int SHORT_DATE_FLAGS = DateUtils.FORMAT_SHOW_DATE private static final int SHORT_DATE_FLAGS = DateUtils.FORMAT_SHOW_DATE
| DateUtils.FORMAT_NO_YEAR | DateUtils.FORMAT_ABBREV_ALL; | DateUtils.FORMAT_NO_YEAR | DateUtils.FORMAT_ABBREV_ALL;
@ -183,7 +180,10 @@ public class UIHelper {
return new Pair<>(getFileDescriptionString(context,message),true); return new Pair<>(getFileDescriptionString(context,message),true);
} }
} else { } else {
final String body = message.getBody(); String body = message.getBody();
if (body.length() > 256) {
body = body.substring(0,256);
}
if (body.startsWith(Message.ME_COMMAND)) { if (body.startsWith(Message.ME_COMMAND)) {
return new Pair<>(body.replaceAll("^" + Message.ME_COMMAND, return new Pair<>(body.replaceAll("^" + Message.ME_COMMAND,
UIHelper.getMessageDisplayName(message) + " "), false); UIHelper.getMessageDisplayName(message) + " "), false);
@ -196,51 +196,12 @@ public class UIHelper {
} else if (message.treatAsDownloadable() == Message.Decision.MUST) { } else if (message.treatAsDownloadable() == Message.Decision.MUST) {
return new Pair<>(context.getString(R.string.x_file_offered_for_download, return new Pair<>(context.getString(R.string.x_file_offered_for_download,
getFileDescriptionString(context,message)),true); getFileDescriptionString(context,message)),true);
} else { } else{
String[] lines = body.split("\n"); return new Pair<>(body.trim(), false);
StringBuilder builder = new StringBuilder();
for(String l : lines) {
if (l.length() > 0) {
char first = l.charAt(0);
if ((first != '>' || isPositionFollowedByNumber(l,0)) && first != '\u00bb') {
String line = l.trim();
if (line.isEmpty()) {
continue;
}
char last = line.charAt(line.length()-1);
if (builder.length() != 0) {
builder.append(' ');
}
builder.append(line);
if (!PUNCTIONATION.contains(last)) {
break;
}
}
}
}
if (builder.length() == 0) {
builder.append(body.trim());
}
return new Pair<>(builder.length() > 256 ? builder.substring(0,256) : builder.toString(), false);
} }
} }
} }
public static boolean isPositionFollowedByNumber(CharSequence body, int pos) {
boolean previousWasNumber = false;
for (int i = pos +1; i < body.length(); i++) {
char c = body.charAt(i);
if (Character.isDigit(body.charAt(i))) {
previousWasNumber = true;
} else if (previousWasNumber && (c == '.' || c == ',')) {
previousWasNumber = false;
} else {
return Character.isWhitespace(c) && previousWasNumber;
}
}
return previousWasNumber;
}
public static String getFileDescriptionString(final Context context, final Message message) { public static String getFileDescriptionString(final Context context, final Message message) {
if (message.getType() == Message.TYPE_IMAGE) { if (message.getType() == Message.TYPE_IMAGE) {
return context.getString(R.string.image); return context.getString(R.string.image);

View file

@ -6,5 +6,4 @@ public final class Xmlns {
public static final String REGISTER = "jabber:iq:register"; public static final String REGISTER = "jabber:iq:register";
public static final String BYTE_STREAMS = "http://jabber.org/protocol/bytestreams"; public static final String BYTE_STREAMS = "http://jabber.org/protocol/bytestreams";
public static final String HTTP_UPLOAD = "urn:xmpp:http:upload"; public static final String HTTP_UPLOAD = "urn:xmpp:http:upload";
public static final String STANZA_IDS = "urn:xmpp:sid:0";
} }

View file

@ -4,9 +4,7 @@ import android.net.Uri;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.net.URLDecoder; import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale;
import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid; import eu.siacs.conversations.xmpp.jid.Jid;
@ -15,12 +13,7 @@ public class XmppUri {
protected String jid; protected String jid;
protected boolean muc; protected boolean muc;
protected List<Fingerprint> fingerprints = new ArrayList<>(); protected String fingerprint;
private String body;
protected boolean safeSource = true;
public static final String OMEMO_URI_PARAM = "omemo-sid-";
public static final String OTR_URI_PARAM = "otr-fingerprint";
public XmppUri(String uri) { public XmppUri(String uri) {
try { try {
@ -38,15 +31,6 @@ public class XmppUri {
parse(uri); parse(uri);
} }
public XmppUri(Uri uri, boolean safeSource) {
this.safeSource = safeSource;
parse(uri);
}
public boolean isSafeSource() {
return safeSource;
}
protected void parse(Uri uri) { protected void parse(Uri uri) {
String scheme = uri.getScheme(); String scheme = uri.getScheme();
String host = uri.getHost(); String host = uri.getHost();
@ -64,17 +48,15 @@ public class XmppUri {
jid = segments.get(1) + "@" + segments.get(2); jid = segments.get(1) + "@" + segments.get(2);
} }
muc = segments.size() > 1 && "j".equalsIgnoreCase(segments.get(0)); muc = segments.size() > 1 && "j".equalsIgnoreCase(segments.get(0));
fingerprints = parseFingerprints(uri.getQuery(),'&');
} else if ("xmpp".equalsIgnoreCase(scheme)) { } else if ("xmpp".equalsIgnoreCase(scheme)) {
// sample: xmpp:foo@bar.com // sample: xmpp:foo@bar.com
muc = isMuc(uri.getQuery()); muc = "join".equalsIgnoreCase(uri.getQuery());
if (uri.getAuthority() != null) { if (uri.getAuthority() != null) {
jid = uri.getAuthority(); jid = uri.getAuthority();
} else { } else {
jid = uri.getSchemeSpecificPart().split("\\?")[0]; jid = uri.getSchemeSpecificPart().split("\\?")[0];
} }
this.fingerprints = parseFingerprints(uri.getQuery()); fingerprint = parseFingerprint(uri.getQuery());
this.body = parseBody(uri.getQuery());
} else if ("imto".equalsIgnoreCase(scheme)) { } else if ("imto".equalsIgnoreCase(scheme)) {
// sample: imto://xmpp/foo@bar.com // sample: imto://xmpp/foo@bar.com
try { try {
@ -91,56 +73,18 @@ public class XmppUri {
} }
} }
protected List<Fingerprint> parseFingerprints(String query) { protected String parseFingerprint(String query) {
return parseFingerprints(query,';'); if (query == null) {
} return null;
} else {
protected List<Fingerprint> parseFingerprints(String query, char seperator) { final String NEEDLE = "otr-fingerprint=";
List<Fingerprint> fingerprints = new ArrayList<>(); int index = query.indexOf(NEEDLE);
String[] pairs = query == null ? new String[0] : query.split(String.valueOf(seperator)); if (index >= 0 && query.length() >= (NEEDLE.length() + index + 40)) {
for(String pair : pairs) { return query.substring(index + NEEDLE.length(), index + NEEDLE.length() + 40);
String[] parts = pair.split("=",2); } else {
if (parts.length == 2) { return null;
String key = parts[0].toLowerCase(Locale.US);
String value = parts[1].toLowerCase(Locale.US);
if (OTR_URI_PARAM.equals(key)) {
fingerprints.add(new Fingerprint(FingerprintType.OTR,value));
}
if (key.startsWith(OMEMO_URI_PARAM)) {
try {
int id = Integer.parseInt(key.substring(OMEMO_URI_PARAM.length()));
fingerprints.add(new Fingerprint(FingerprintType.OMEMO,value,id));
} catch (Exception e) {
//ignoring invalid device id
}
}
} }
} }
return fingerprints;
}
protected String parseBody(String query) {
for(String pair : query == null ? new String[0] : query.split(";")) {
final String[] parts = pair.split("=",2);
if (parts.length == 2 && "body".equals(parts[0].toLowerCase(Locale.US))) {
try {
return URLDecoder.decode(parts[1],"UTF-8");
} catch (UnsupportedEncodingException e) {
return null;
}
}
}
return null;
}
protected boolean isMuc(String query) {
for(String pair : query == null ? new String[0] : query.split(";")) {
final String[] parts = pair.split("=",2);
if (parts.length == 1 && "join".equals(parts[0])) {
return true;
}
}
return false;
} }
public Jid getJid() { public Jid getJid() {
@ -151,60 +95,7 @@ public class XmppUri {
} }
} }
public String getBody() { public String getFingerprint() {
return body; return this.fingerprint;
}
public List<Fingerprint> getFingerprints() {
return this.fingerprints;
}
public boolean hasFingerprints() {
return fingerprints.size() > 0;
}
public enum FingerprintType {
OMEMO,
OTR
}
public static String getFingerprintUri(String base, List<XmppUri.Fingerprint> fingerprints, char seperator) {
StringBuilder builder = new StringBuilder(base);
builder.append('?');
for(int i = 0; i < fingerprints.size(); ++i) {
XmppUri.FingerprintType type = fingerprints.get(i).type;
if (type == XmppUri.FingerprintType.OMEMO) {
builder.append(XmppUri.OMEMO_URI_PARAM);
builder.append(fingerprints.get(i).deviceId);
} else if (type == XmppUri.FingerprintType.OTR) {
builder.append(XmppUri.OTR_URI_PARAM);
}
builder.append('=');
builder.append(fingerprints.get(i).fingerprint);
if (i != fingerprints.size() -1) {
builder.append(seperator);
}
}
return builder.toString();
}
public static class Fingerprint {
public final FingerprintType type;
public final String fingerprint;
public final int deviceId;
public Fingerprint(FingerprintType type, String fingerprint) {
this(type, fingerprint, 0);
}
public Fingerprint(FingerprintType type, String fingerprint, int deviceId) {
this.type = type;
this.fingerprint = fingerprint;
this.deviceId = deviceId;
}
@Override
public String toString() {
return type.toString()+": "+fingerprint+(deviceId != 0 ? " "+String.valueOf(deviceId) : "");
}
} }
} }

View file

@ -99,13 +99,6 @@ public class TagWriter {
public void forceClose() { public void forceClose() {
finish(); finish();
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
//ignoring
}
}
outputStream = null; outputStream = null;
} }
} }

View file

@ -82,7 +82,7 @@ public class XmlReader {
} }
} catch (Throwable throwable) { } catch (Throwable throwable) {
throw new IOException("xml parser mishandled "+throwable.getClass().getSimpleName()+"("+throwable.getMessage()+")", throwable); throw new IOException("xml parser mishandled "+throwable.getClass().getName(), throwable);
} finally { } finally {
if (wakeLock.isHeld()) { if (wakeLock.isHeld()) {
try { try {

View file

@ -13,6 +13,8 @@ import android.util.Log;
import android.util.Pair; import android.util.Pair;
import android.util.SparseArray; import android.util.SparseArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserException;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
@ -24,8 +26,8 @@ import java.net.IDN;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.Socket; import java.net.Socket;
import java.net.URL;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.net.URL;
import java.security.KeyManagementException; import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.Principal; import java.security.Principal;
@ -59,7 +61,6 @@ import eu.siacs.conversations.crypto.sasl.External;
import eu.siacs.conversations.crypto.sasl.Plain; import eu.siacs.conversations.crypto.sasl.Plain;
import eu.siacs.conversations.crypto.sasl.SaslMechanism; import eu.siacs.conversations.crypto.sasl.SaslMechanism;
import eu.siacs.conversations.crypto.sasl.ScramSha1; import eu.siacs.conversations.crypto.sasl.ScramSha1;
import eu.siacs.conversations.crypto.sasl.ScramSha256;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.ServiceDiscoveryResult; import eu.siacs.conversations.entities.ServiceDiscoveryResult;
@ -177,6 +178,8 @@ public class XmppConnection implements Runnable {
} }
} }
private Identity mServerIdentity = Identity.UNKNOWN;
public final OnIqPacketReceived registrationResponseListener = new OnIqPacketReceived() { public final OnIqPacketReceived registrationResponseListener = new OnIqPacketReceived() {
@Override @Override
public void onIqPacketReceived(Account account, IqPacket packet) { public void onIqPacketReceived(Account account, IqPacket packet) {
@ -215,10 +218,7 @@ public class XmppConnection implements Runnable {
mXmppConnectionService = service; mXmppConnectionService = service;
} }
protected synchronized void changeStatus(final Account.State nextStatus) { protected void changeStatus(final Account.State nextStatus) {
if (Thread.currentThread().isInterrupted()) {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": not changing status to "+nextStatus+" because thread was interrupted");
}
if (account.getStatus() != nextStatus) { if (account.getStatus() != nextStatus) {
if ((nextStatus == Account.State.OFFLINE) if ((nextStatus == Account.State.OFFLINE)
&& (account.getStatus() != Account.State.CONNECTING) && (account.getStatus() != Account.State.CONNECTING)
@ -250,8 +250,18 @@ public class XmppConnection implements Runnable {
Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": connecting"); Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": connecting");
features.encryptionEnabled = false; features.encryptionEnabled = false;
this.attempt++; this.attempt++;
switch (account.getJid().getDomainpart()) {
case "chat.facebook.com":
mServerIdentity = Identity.FACEBOOK;
break;
case "nimbuzz.com":
mServerIdentity = Identity.NIMBUZZ;
break;
default:
mServerIdentity = Identity.UNKNOWN;
break;
}
try { try {
Socket localSocket;
shouldAuthenticate = needsBinding = !account.isOptionSet(Account.OPTION_REGISTER); shouldAuthenticate = needsBinding = !account.isOptionSet(Account.OPTION_REGISTER);
tagReader = new XmlReader(wakeLock); tagReader = new XmlReader(wakeLock);
tagWriter = new TagWriter(); tagWriter = new TagWriter();
@ -266,15 +276,8 @@ public class XmppConnection implements Runnable {
destination = account.getHostname(); destination = account.getHostname();
} }
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": connect to " + destination + " via Tor"); Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": connect to " + destination + " via Tor");
localSocket = SocksSocketFactory.createSocketOverTor(destination, account.getPort()); socket = SocksSocketFactory.createSocketOverTor(destination, account.getPort());
try { startXmpp();
startXmpp(localSocket);
} catch (InterruptedException e) {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": thread was interrupted before beginning stream");
return;
} catch (Exception e) {
throw new IOException(e.getMessage());
}
} else if (extended && account.getHostname() != null && !account.getHostname().isEmpty()) { } else if (extended && account.getHostname() != null && !account.getHostname().isEmpty()) {
InetSocketAddress address = new InetSocketAddress(account.getHostname(), account.getPort()); InetSocketAddress address = new InetSocketAddress(account.getHostname(), account.getPort());
@ -285,47 +288,33 @@ public class XmppConnection implements Runnable {
if (features.encryptionEnabled) { if (features.encryptionEnabled) {
try { try {
final TlsFactoryVerifier tlsFactoryVerifier = getTlsFactoryVerifier(); final TlsFactoryVerifier tlsFactoryVerifier = getTlsFactoryVerifier();
localSocket = tlsFactoryVerifier.factory.createSocket(); socket = tlsFactoryVerifier.factory.createSocket();
localSocket.connect(address, Config.SOCKET_TIMEOUT * 1000); socket.connect(address, Config.SOCKET_TIMEOUT * 1000);
final SSLSession session = ((SSLSocket) localSocket).getSession(); final SSLSession session = ((SSLSocket) socket).getSession();
if (!tlsFactoryVerifier.verifier.verify(account.getServer().getDomainpart(), session)) { if (!tlsFactoryVerifier.verifier.verify(account.getServer().getDomainpart(), session)) {
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": TLS certificate verification failed"); Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": TLS certificate verification failed");
throw new SecurityException(); throw new SecurityException();
} }
} catch (KeyManagementException e) { } catch (KeyManagementException e) {
features.encryptionEnabled = false; features.encryptionEnabled = false;
localSocket = new Socket(); socket = new Socket();
} }
} else { } else {
localSocket = new Socket(); socket = new Socket();
localSocket.connect(address, Config.SOCKET_TIMEOUT * 1000); socket.connect(address, Config.SOCKET_TIMEOUT * 1000);
} }
} catch (IOException e) { } catch (IOException e) {
throw new UnknownHostException(); throw new UnknownHostException();
} }
try { startXmpp();
startXmpp(localSocket);
} catch (InterruptedException e) {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": thread was interrupted before beginning stream");
return;
} catch (Exception e) {
throw new IOException(e.getMessage());
}
} else if (DNSHelper.isIp(account.getServer().toString())) { } else if (DNSHelper.isIp(account.getServer().toString())) {
localSocket = new Socket(); socket = new Socket();
try { try {
localSocket.connect(new InetSocketAddress(account.getServer().toString(), 5222), Config.SOCKET_TIMEOUT * 1000); socket.connect(new InetSocketAddress(account.getServer().toString(), 5222), Config.SOCKET_TIMEOUT * 1000);
} catch (IOException e) { } catch (IOException e) {
throw new UnknownHostException(); throw new UnknownHostException();
} }
try { startXmpp();
startXmpp(localSocket);
} catch (InterruptedException e) {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": thread was interrupted before beginning stream");
return;
} catch (Exception e) {
throw new IOException(e.getMessage());
}
} else { } else {
final Bundle result = DNSHelper.getSRVRecord(account.getServer(), mXmppConnectionService); final Bundle result = DNSHelper.getSRVRecord(account.getServer(), mXmppConnectionService);
final ArrayList<Parcelable> values = result.getParcelableArrayList("values"); final ArrayList<Parcelable> values = result.getParcelableArrayList("values");
@ -361,37 +350,32 @@ public class XmppConnection implements Runnable {
} }
if (!features.encryptionEnabled) { if (!features.encryptionEnabled) {
localSocket = new Socket(); socket = new Socket();
localSocket.connect(addr, Config.SOCKET_TIMEOUT * 1000); socket.connect(addr, Config.SOCKET_TIMEOUT * 1000);
} else { } else {
final TlsFactoryVerifier tlsFactoryVerifier = getTlsFactoryVerifier(); final TlsFactoryVerifier tlsFactoryVerifier = getTlsFactoryVerifier();
localSocket = tlsFactoryVerifier.factory.createSocket(); socket = tlsFactoryVerifier.factory.createSocket();
if (localSocket == null) { if (socket == null) {
throw new IOException("could not initialize ssl socket"); throw new IOException("could not initialize ssl socket");
} }
SSLSocketHelper.setSecurity((SSLSocket) localSocket); SSLSocketHelper.setSecurity((SSLSocket) socket);
SSLSocketHelper.setSNIHost(tlsFactoryVerifier.factory, (SSLSocket) localSocket, account.getServer().getDomainpart()); SSLSocketHelper.setSNIHost(tlsFactoryVerifier.factory, (SSLSocket) socket, account.getServer().getDomainpart());
SSLSocketHelper.setAlpnProtocol(tlsFactoryVerifier.factory, (SSLSocket) localSocket, "xmpp-client"); SSLSocketHelper.setAlpnProtocol(tlsFactoryVerifier.factory, (SSLSocket) socket, "xmpp-client");
localSocket.connect(addr, Config.SOCKET_TIMEOUT * 1000); socket.connect(addr, Config.SOCKET_TIMEOUT * 1000);
if (!tlsFactoryVerifier.verifier.verify(account.getServer().getDomainpart(), ((SSLSocket) localSocket).getSession())) { if (!tlsFactoryVerifier.verifier.verify(account.getServer().getDomainpart(), ((SSLSocket) socket).getSession())) {
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": TLS certificate verification failed"); Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": TLS certificate verification failed");
throw new SecurityException(); throw new SecurityException();
} }
} }
if (startXmpp(localSocket)) {
if (startXmpp())
break; // successfully connected to server that speaks xmpp break; // successfully connected to server that speaks xmpp
} else {
localSocket.close();
}
} catch (final SecurityException e) { } catch (final SecurityException e) {
throw e; throw e;
} catch (InterruptedException e) {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": thread was interrupted before beginning stream");
return;
} catch (final Throwable e) { } catch (final Throwable e) {
Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage() + "(" + e.getClass().getName() + ")"); Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage() + "(" + e.getClass().getName() + ")");
if (!iterator.hasNext()) { if (!iterator.hasNext()) {
@ -401,10 +385,8 @@ public class XmppConnection implements Runnable {
} }
} }
processStream(); processStream();
} catch (final java.lang.SecurityException e) { } catch (final java.lang.SecurityException e) {
this.changeStatus(Account.State.MISSING_INTERNET_PERMISSION); this.changeStatus(Account.State.MISSING_INTERNET_PERMISSION);
} catch (final RegistrationNotSupportedException e) {
this.changeStatus(Account.State.REGISTRATION_NOT_SUPPORTED);
} catch (final IncompatibleServerException e) { } catch (final IncompatibleServerException e) {
this.changeStatus(Account.State.INCOMPATIBLE_SERVER); this.changeStatus(Account.State.INCOMPATIBLE_SERVER);
} catch (final SecurityException e) { } catch (final SecurityException e) {
@ -428,16 +410,12 @@ public class XmppConnection implements Runnable {
this.changeStatus(Account.State.OFFLINE); this.changeStatus(Account.State.OFFLINE);
this.attempt = Math.max(0, this.attempt - 1); this.attempt = Math.max(0, this.attempt - 1);
} finally { } finally {
if (!Thread.currentThread().isInterrupted()) { forceCloseSocket();
forceCloseSocket(); if (wakeLock.isHeld()) {
if (wakeLock.isHeld()) { try {
try { wakeLock.release();
wakeLock.release(); } catch (final RuntimeException ignored) {
} catch (final RuntimeException ignored) {
}
} }
} else {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": not force closing socket and releasing wake lock because thread was interrupted");
} }
} }
} }
@ -445,18 +423,27 @@ public class XmppConnection implements Runnable {
/** /**
* Starts xmpp protocol, call after connecting to socket * Starts xmpp protocol, call after connecting to socket
* @return true if server returns with valid xmpp, false otherwise * @return true if server returns with valid xmpp, false otherwise
* @throws IOException Unknown tag on connect
* @throws XmlPullParserException Bad Xml
* @throws NoSuchAlgorithmException Other error
*/ */
private boolean startXmpp(Socket socket) throws Exception { private boolean startXmpp() throws IOException, XmlPullParserException, NoSuchAlgorithmException {
if (Thread.currentThread().isInterrupted()) {
throw new InterruptedException();
}
this.socket = socket;
tagWriter.setOutputStream(socket.getOutputStream()); tagWriter.setOutputStream(socket.getOutputStream());
tagReader.setInputStream(socket.getInputStream()); tagReader.setInputStream(socket.getInputStream());
tagWriter.beginDocument(); tagWriter.beginDocument();
sendStartStream(); sendStartStream();
final Tag tag = tagReader.readTag(); Tag nextTag;
return tag != null && tag.isStart("stream"); while ((nextTag = tagReader.readTag()) != null) {
if (nextTag.isStart("stream")) {
return true;
} else {
throw new IOException("unknown tag on connect");
}
}
if (socket.isConnected()) {
socket.close();
}
return false;
} }
private static class TlsFactoryVerifier { private static class TlsFactoryVerifier {
@ -481,8 +468,7 @@ public class XmppConnection implements Runnable {
} else { } else {
keyManager = null; keyManager = null;
} }
String domain = account.getJid().getDomainpart(); sc.init(keyManager, new X509TrustManager[]{mInteractive ? trustManager : trustManager.getNonInteractive()}, mXmppConnectionService.getRNG());
sc.init(keyManager, new X509TrustManager[]{mInteractive ? trustManager.getInteractive(domain) : trustManager.getNonInteractive(domain)}, mXmppConnectionService.getRNG());
final SSLSocketFactory factory = sc.getSocketFactory(); final SSLSocketFactory factory = sc.getSocketFactory();
final HostnameVerifier verifier; final HostnameVerifier verifier;
if (mInteractive) { if (mInteractive) {
@ -733,7 +719,7 @@ public class XmppConnection implements Runnable {
final Pair<IqPacket, OnIqPacketReceived> packetCallbackDuple = packetCallbacks.get(packet.getId()); final Pair<IqPacket, OnIqPacketReceived> packetCallbackDuple = packetCallbacks.get(packet.getId());
// Packets to the server should have responses from the server // Packets to the server should have responses from the server
if (packetCallbackDuple.first.toServer(account)) { if (packetCallbackDuple.first.toServer(account)) {
if (packet.fromServer(account)) { if (packet.fromServer(account) || mServerIdentity == Identity.FACEBOOK) {
callback = packetCallbackDuple.second; callback = packetCallbackDuple.second;
packetCallbacks.remove(packet.getId()); packetCallbacks.remove(packet.getId());
} else { } else {
@ -826,8 +812,10 @@ public class XmppConnection implements Runnable {
} else { } else {
throw new IncompatibleServerException(); throw new IncompatibleServerException();
} }
} else if (!this.streamFeatures.hasChild("register") && account.isOptionSet(Account.OPTION_REGISTER)) { } else if (!this.streamFeatures.hasChild("register")
throw new RegistrationNotSupportedException(); && account.isOptionSet(Account.OPTION_REGISTER)) {
forceCloseSocket();
changeStatus(Account.State.REGISTRATION_NOT_SUPPORTED);
} else if (this.streamFeatures.hasChild("mechanisms") } else if (this.streamFeatures.hasChild("mechanisms")
&& shouldAuthenticate && shouldAuthenticate
&& (features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS)) { && (features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS)) {
@ -854,8 +842,6 @@ public class XmppConnection implements Runnable {
auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl"); auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
if (mechanisms.contains("EXTERNAL") && account.getPrivateKeyAlias() != null) { if (mechanisms.contains("EXTERNAL") && account.getPrivateKeyAlias() != null) {
saslMechanism = new External(tagWriter, account, mXmppConnectionService.getRNG()); saslMechanism = new External(tagWriter, account, mXmppConnectionService.getRNG());
} else if (mechanisms.contains("SCRAM-SHA-256")) {
saslMechanism = new ScramSha256(tagWriter, account, mXmppConnectionService.getRNG());
} else if (mechanisms.contains("SCRAM-SHA-1")) { } else if (mechanisms.contains("SCRAM-SHA-1")) {
saslMechanism = new ScramSha1(tagWriter, account, mXmppConnectionService.getRNG()); saslMechanism = new ScramSha1(tagWriter, account, mXmppConnectionService.getRNG());
} else if (mechanisms.contains("PLAIN")) { } else if (mechanisms.contains("PLAIN")) {
@ -964,7 +950,7 @@ public class XmppConnection implements Runnable {
} }
public void resetEverything() { public void resetEverything() {
resetAttemptCount(true); resetAttemptCount();
resetStreamId(); resetStreamId();
clearIqCallbacks(); clearIqCallbacks();
mStanzaQueue.clear(); mStanzaQueue.clear();
@ -1087,7 +1073,7 @@ public class XmppConnection implements Runnable {
this.disco.clear(); this.disco.clear();
} }
mPendingServiceDiscoveries.set(0); mPendingServiceDiscoveries.set(0);
mWaitForDisco.set(smVersion != 0 && !account.getJid().getDomainpart().equalsIgnoreCase("nimbuzz.com")); mWaitForDisco.set(mServerIdentity != Identity.NIMBUZZ && smVersion != 0);
lastDiscoStarted = SystemClock.elapsedRealtime(); lastDiscoStarted = SystemClock.elapsedRealtime();
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": starting service discovery"); Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": starting service discovery");
mXmppConnectionService.scheduleWakeUpCall(Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode()); mXmppConnectionService.scheduleWakeUpCall(Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode());
@ -1126,6 +1112,24 @@ public class XmppConnection implements Runnable {
boolean advancedStreamFeaturesLoaded; boolean advancedStreamFeaturesLoaded;
synchronized (XmppConnection.this.disco) { synchronized (XmppConnection.this.disco) {
ServiceDiscoveryResult result = new ServiceDiscoveryResult(packet); ServiceDiscoveryResult result = new ServiceDiscoveryResult(packet);
for (final ServiceDiscoveryResult.Identity id : result.getIdentities()) {
if (mServerIdentity == Identity.UNKNOWN && id.getType().equals("im") &&
id.getCategory().equals("server") && id.getName() != null &&
jid.equals(account.getServer())) {
switch (id.getName()) {
case "Prosody":
mServerIdentity = Identity.PROSODY;
break;
case "ejabberd":
mServerIdentity = Identity.EJABBERD;
break;
case "Slack-XMPP":
mServerIdentity = Identity.SLACK;
break;
}
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": server name: " + id.getName());
}
}
if (jid.equals(account.getServer())) { if (jid.equals(account.getServer())) {
mXmppConnectionService.databaseBackend.insertDiscoveryResult(result); mXmppConnectionService.databaseBackend.insertDiscoveryResult(result);
} }
@ -1255,7 +1259,7 @@ public class XmppConnection implements Runnable {
} }
private String nextRandomId() { private String nextRandomId() {
return new BigInteger(50, mXmppConnectionService.getRNG()).toString(36); return new BigInteger(50, mXmppConnectionService.getRNG()).toString(32);
} }
public String sendIqPacket(final IqPacket packet, final OnIqPacketReceived callback) { public String sendIqPacket(final IqPacket packet, final OnIqPacketReceived callback) {
@ -1355,10 +1359,31 @@ public class XmppConnection implements Runnable {
} }
} }
private void forceCloseSocket() { public void waitForPush() {
if (tagWriter != null) { if (tagWriter.isActive()) {
tagWriter.forceClose(); tagWriter.finish();
new Thread(new Runnable() {
@Override
public void run() {
try {
while(!tagWriter.finished()) {
Thread.sleep(10);
}
socket.close();
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": closed tcp without closing stream");
changeStatus(Account.State.OFFLINE);
} catch (IOException | InterruptedException e) {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": error while closing socket for waitForPush()");
}
}
}).start();
} else {
forceCloseSocket();
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": closed tcp without closing stream (no waiting)");
} }
}
private void forceCloseSocket() {
if (socket != null) { if (socket != null) {
try { try {
socket.close(); socket.close();
@ -1378,6 +1403,7 @@ public class XmppConnection implements Runnable {
interrupt(); interrupt();
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": disconnecting force="+Boolean.valueOf(force)); Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": disconnecting force="+Boolean.valueOf(force));
if (force) { if (force) {
tagWriter.forceClose();
forceCloseSocket(); forceCloseSocket();
} else { } else {
if (tagWriter.isActive()) { if (tagWriter.isActive()) {
@ -1500,11 +1526,9 @@ public class XmppConnection implements Runnable {
this.sendPacket(new InactivePacket()); this.sendPacket(new InactivePacket());
} }
public void resetAttemptCount(boolean resetConnectTime) { public void resetAttemptCount() {
this.attempt = 0; this.attempt = 0;
if (resetConnectTime) { this.lastConnect = 0;
this.lastConnect = 0;
}
} }
public void setInteractive(boolean interactive) { public void setInteractive(boolean interactive) {
@ -1512,25 +1536,7 @@ public class XmppConnection implements Runnable {
} }
public Identity getServerIdentity() { public Identity getServerIdentity() {
synchronized (this.disco) { return mServerIdentity;
ServiceDiscoveryResult result = disco.get(account.getJid().toDomainJid());
if (result == null) {
return Identity.UNKNOWN;
}
for (final ServiceDiscoveryResult.Identity id : result.getIdentities()) {
if (id.getType().equals("im") && id.getCategory().equals("server") && id.getName() != null) {
switch (id.getName()) {
case "Prosody":
return Identity.PROSODY;
case "ejabberd":
return Identity.EJABBERD;
case "Slack-XMPP":
return Identity.SLACK;
}
}
}
}
return Identity.UNKNOWN;
} }
private class UnauthorizedException extends IOException { private class UnauthorizedException extends IOException {
@ -1561,10 +1567,6 @@ public class XmppConnection implements Runnable {
} }
private class RegistrationNotSupportedException extends IOException {
}
public enum Identity { public enum Identity {
FACEBOOK, FACEBOOK,
SLACK, SLACK,
@ -1683,10 +1685,6 @@ public class XmppConnection implements Runnable {
return -1; return -1;
} }
} }
public boolean stanzaIds() {
return hasDiscoFeature(account.getJid().toBareJid(),Xmlns.STANZA_IDS);
}
} }
private IqGenerator getIqGenerator() { private IqGenerator getIqGenerator() {

View file

@ -21,8 +21,6 @@ public final class Jid {
private final String domainpart; private final String domainpart;
private final String resourcepart; private final String resourcepart;
private static final char[] JID_ESCAPING_CHARS = {' ','"','&','\'','/',':','<','>','@','\\'};
// It's much more efficient to store the ful JID as well as the parts instead of figuring them // It's much more efficient to store the ful JID as well as the parts instead of figuring them
// all out every time (since some characters are displayed but aren't used for comparisons). // all out every time (since some characters are displayed but aren't used for comparisons).
private final String displayjid; private final String displayjid;
@ -31,18 +29,6 @@ public final class Jid {
return localpart; return localpart;
} }
public String getUnescapedLocalpart() {
if (localpart == null || !localpart.contains("\\")) {
return localpart;
} else {
String localpart = this.localpart;
for(char c : JID_ESCAPING_CHARS) {
localpart = localpart.replace(String.format ("\\%02x", (int)c),String.valueOf(c));
}
return localpart;
}
}
public String getDomainpart() { public String getDomainpart() {
return IDN.toUnicode(domainpart); return IDN.toUnicode(domainpart);
} }

View file

@ -388,7 +388,7 @@ public class JingleConnection implements Transferable {
long size = Long.parseLong(fileSize.getContent()); long size = Long.parseLong(fileSize.getContent());
message.setBody(Long.toString(size)); message.setBody(Long.toString(size));
conversation.add(message); conversation.add(message);
mJingleConnectionManager.updateConversationUi(true); mXmppConnectionService.updateConversationUi();
if (mJingleConnectionManager.hasStoragePermission() if (mJingleConnectionManager.hasStoragePermission()
&& size < this.mJingleConnectionManager.getAutoAcceptFileSize() && size < this.mJingleConnectionManager.getAutoAcceptFileSize()
&& mXmppConnectionService.isDataSaverDisabled()) { && mXmppConnectionService.isDataSaverDisabled()) {
@ -510,7 +510,7 @@ public class JingleConnection implements Transferable {
private void sendAccept() { private void sendAccept() {
mJingleStatus = JINGLE_STATUS_ACCEPTED; mJingleStatus = JINGLE_STATUS_ACCEPTED;
this.mStatus = Transferable.STATUS_DOWNLOADING; this.mStatus = Transferable.STATUS_DOWNLOADING;
this.mJingleConnectionManager.updateConversationUi(true); mXmppConnectionService.updateConversationUi();
this.mJingleConnectionManager.getPrimaryCandidate(this.account, new OnPrimaryCandidateFound() { this.mJingleConnectionManager.getPrimaryCandidate(this.account, new OnPrimaryCandidateFound() {
@Override @Override
public void onPrimaryCandidateFound(boolean success, final JingleCandidate candidate) { public void onPrimaryCandidateFound(boolean success, final JingleCandidate candidate) {
@ -620,10 +620,6 @@ public class JingleConnection implements Transferable {
if (cid != null) { if (cid != null) {
Log.d(Config.LOGTAG, "candidate used by counterpart:" + cid); Log.d(Config.LOGTAG, "candidate used by counterpart:" + cid);
JingleCandidate candidate = getCandidate(cid); JingleCandidate candidate = getCandidate(cid);
if (candidate == null) {
Log.d(Config.LOGTAG,"could not find candidate with cid="+cid);
return false;
}
candidate.flagAsUsedByCounterpart(); candidate.flagAsUsedByCounterpart();
this.receivedCandidate = true; this.receivedCandidate = true;
if ((mJingleStatus == JINGLE_STATUS_ACCEPTED) if ((mJingleStatus == JINGLE_STATUS_ACCEPTED)
@ -756,7 +752,7 @@ public class JingleConnection implements Transferable {
} }
private void sendFallbackToIbb() { private void sendFallbackToIbb() {
Log.d(Config.LOGTAG, account.getJid().toBareJid()+": sending fallback to ibb"); Log.d(Config.LOGTAG, "sending fallback to ibb");
JinglePacket packet = this.bootstrapPacket("transport-replace"); JinglePacket packet = this.bootstrapPacket("transport-replace");
Content content = new Content(this.contentCreator, this.contentName); Content content = new Content(this.contentCreator, this.contentName);
this.transportId = this.mJingleConnectionManager.nextRandomId(); this.transportId = this.mJingleConnectionManager.nextRandomId();
@ -767,18 +763,6 @@ public class JingleConnection implements Transferable {
this.sendJinglePacket(packet); this.sendJinglePacket(packet);
} }
OnTransportConnected onIbbTransportConnected = new OnTransportConnected() {
@Override
public void failed() {
Log.d(Config.LOGTAG, "ibb open failed");
}
@Override
public void established() {
JingleConnection.this.transport.send(file, onFileTransmissionSatusChanged);
}
};
private boolean receiveFallbackToIbb(JinglePacket packet) { private boolean receiveFallbackToIbb(JinglePacket packet) {
Log.d(Config.LOGTAG, "receiving fallack to ibb"); Log.d(Config.LOGTAG, "receiving fallack to ibb");
String receivedBlockSize = packet.getJingleContent().ibbTransport() String receivedBlockSize = packet.getJingleContent().ibbTransport()
@ -791,28 +775,13 @@ public class JingleConnection implements Transferable {
} }
this.transportId = packet.getJingleContent().getTransportId(); this.transportId = packet.getJingleContent().getTransportId();
this.transport = new JingleInbandTransport(this, this.transportId, this.ibbBlockSize); this.transport = new JingleInbandTransport(this, this.transportId, this.ibbBlockSize);
this.transport.receive(file, onFileTransmissionSatusChanged);
JinglePacket answer = bootstrapPacket("transport-accept"); JinglePacket answer = bootstrapPacket("transport-accept");
Content content = new Content("initiator", "a-file-offer"); Content content = new Content("initiator", "a-file-offer");
content.setTransportId(this.transportId); content.setTransportId(this.transportId);
content.ibbTransport().setAttribute("block-size",this.ibbBlockSize); content.ibbTransport().setAttribute("block-size",this.ibbBlockSize);
answer.setContent(content); answer.setContent(content);
this.sendJinglePacket(answer);
if (initiator.equals(account.getJid())) {
this.sendJinglePacket(answer, new OnIqPacketReceived() {
@Override
public void onIqPacketReceived(Account account, IqPacket packet) {
if (packet.getType() == IqPacket.TYPE.RESULT) {
Log.d(Config.LOGTAG, account.getJid().toBareJid() + " recipient ACKed our transport-accept. creating ibb");
transport.connect(onIbbTransportConnected);
}
}
});
} else {
this.transport.receive(file, onFileTransmissionSatusChanged);
this.sendJinglePacket(answer);
}
return true; return true;
} }
@ -827,13 +796,19 @@ public class JingleConnection implements Transferable {
} }
} }
this.transport = new JingleInbandTransport(this, this.transportId, this.ibbBlockSize); this.transport = new JingleInbandTransport(this, this.transportId, this.ibbBlockSize);
this.transport.connect(new OnTransportConnected() {
//might be receive instead if we are not initiating @Override
if (initiator.equals(account.getJid())) { public void failed() {
this.transport.connect(onIbbTransportConnected); Log.d(Config.LOGTAG, "ibb open failed");
} else { }
this.transport.receive(file,onFileTransmissionSatusChanged);
} @Override
public void established() {
JingleConnection.this.transport.send(file,
onFileTransmissionSatusChanged);
}
});
return true; return true;
} else { } else {
return false; return false;
@ -863,7 +838,7 @@ public class JingleConnection implements Transferable {
if (this.file!=null) { if (this.file!=null) {
file.delete(); file.delete();
} }
this.mJingleConnectionManager.updateConversationUi(true); this.mXmppConnectionService.updateConversationUi();
} else { } else {
this.mXmppConnectionService.markMessage(this.message, this.mXmppConnectionService.markMessage(this.message,
Message.STATUS_SEND_FAILED); Message.STATUS_SEND_FAILED);
@ -889,7 +864,7 @@ public class JingleConnection implements Transferable {
if (this.file!=null) { if (this.file!=null) {
file.delete(); file.delete();
} }
this.mJingleConnectionManager.updateConversationUi(true); this.mXmppConnectionService.updateConversationUi();
} else { } else {
this.mXmppConnectionService.markMessage(this.message, this.mXmppConnectionService.markMessage(this.message,
Message.STATUS_SEND_FAILED, Message.STATUS_SEND_FAILED,
@ -1037,7 +1012,7 @@ public class JingleConnection implements Transferable {
public void updateProgress(int i) { public void updateProgress(int i) {
this.mProgress = i; this.mProgress = i;
mJingleConnectionManager.updateConversationUi(false); mXmppConnectionService.updateConversationUi();
} }
public String getTransportId() { public String getTransportId() {

View file

@ -142,9 +142,6 @@ public class JingleConnectionManager extends AbstractConnectionManager {
} else if (packet.hasChild("data", "http://jabber.org/protocol/ibb")) { } else if (packet.hasChild("data", "http://jabber.org/protocol/ibb")) {
payload = packet.findChild("data", "http://jabber.org/protocol/ibb"); payload = packet.findChild("data", "http://jabber.org/protocol/ibb");
sid = payload.getAttribute("sid"); sid = payload.getAttribute("sid");
} else if (packet.hasChild("close","http://jabber.org/protocol/ibb")) {
payload = packet.findChild("close", "http://jabber.org/protocol/ibb");
sid = payload.getAttribute("sid");
} }
if (sid != null) { if (sid != null) {
for (JingleConnection connection : connections) { for (JingleConnection connection : connections) {

View file

@ -47,9 +47,7 @@ public class JingleInbandTransport extends JingleTransport {
@Override @Override
public void onIqPacketReceived(Account account, IqPacket packet) { public void onIqPacketReceived(Account account, IqPacket packet) {
if (connected && packet.getType() == IqPacket.TYPE.RESULT) { if (connected && packet.getType() == IqPacket.TYPE.RESULT) {
if (remainingSize > 0) { sendNextBlock();
sendNextBlock();
}
} }
} }
}; };
@ -62,14 +60,6 @@ public class JingleInbandTransport extends JingleTransport {
this.sessionId = sid; 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) { public void connect(final OnTransportConnected callback) {
IqPacket iq = new IqPacket(IqPacket.TYPE.SET); IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
iq.setTo(this.counterpart); iq.setTo(this.counterpart);
@ -165,7 +155,6 @@ public class JingleInbandTransport extends JingleTransport {
try { try {
int count = fileInputStream.read(buffer); int count = fileInputStream.read(buffer);
if (count == -1) { if (count == -1) {
sendClose();
file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest())); file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest()));
this.onFileTransmissionStatusChanged.onFileTransmitted(file); this.onFileTransmissionStatusChanged.onFileTransmitted(file);
fileInputStream.close(); fileInputStream.close();
@ -192,13 +181,12 @@ public class JingleInbandTransport extends JingleTransport {
if (this.remainingSize > 0) { if (this.remainingSize > 0) {
connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100)); connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100));
} else { } else {
sendClose();
file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest())); file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest()));
this.onFileTransmissionStatusChanged.onFileTransmitted(file); this.onFileTransmissionStatusChanged.onFileTransmitted(file);
fileInputStream.close(); fileInputStream.close();
} }
} catch (IOException e) { } catch (IOException e) {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": io exception during sendNextBlock() "+e.getMessage()); Log.d(Config.LOGTAG,account.getJid().toBareJid()+": "+e.getMessage());
FileBackend.close(fileInputStream); FileBackend.close(fileInputStream);
this.onFileTransmissionStatusChanged.onFileTransferAborted(); this.onFileTransmissionStatusChanged.onFileTransferAborted();
} }
@ -244,13 +232,7 @@ public class JingleInbandTransport extends JingleTransport {
this.receiveNextBlock(payload.getContent()); this.receiveNextBlock(payload.getContent());
this.account.getXmppConnection().sendIqPacket( this.account.getXmppConnection().sendIqPacket(
packet.generateResponse(IqPacket.TYPE.RESULT), null); 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().toBareJid()+": received ibb close");
} else { } else {
Log.d(Config.LOGTAG,payload.toString());
// TODO some sort of exception // TODO some sort of exception
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 601 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 364 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 B

After

Width:  |  Height:  |  Size: 368 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 281 B

After

Width:  |  Height:  |  Size: 371 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 253 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 320 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 329 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 765 B

After

Width:  |  Height:  |  Size: 765 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 779 B

After

Width:  |  Height:  |  Size: 779 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 750 B

After

Width:  |  Height:  |  Size: 750 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 757 B

After

Width:  |  Height:  |  Size: 757 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 779 B

After

Width:  |  Height:  |  Size: 779 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 687 B

After

Width:  |  Height:  |  Size: 687 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 707 B

After

Width:  |  Height:  |  Size: 707 B

Some files were not shown because too many files have changed in this diff Show more