aboutsummaryrefslogtreecommitdiffstats
path: root/src/main/java/eu
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/eu')
-rw-r--r--src/main/java/eu/siacs/conversations/Config.java134
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/OtrService.java308
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java162
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/PgpEngine.java406
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/XmppDomainVerifier.java127
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java120
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlServiceImpl.java1047
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlServiceStub.java212
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/axolotl/CryptoFailedException.java7
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/axolotl/NoSessionsCreatedException.java4
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/axolotl/OnMessageCreatedCallback.java5
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/axolotl/SQLiteAxolotlStore.java429
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java249
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlSession.java221
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/sasl/DigestMd5.java91
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/sasl/External.java30
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java30
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java62
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java234
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/sasl/Tokenizer.java78
-rw-r--r--src/main/java/eu/siacs/conversations/entities/AbstractEntity.java20
-rw-r--r--src/main/java/eu/siacs/conversations/entities/Account.java559
-rw-r--r--src/main/java/eu/siacs/conversations/entities/Blockable.java11
-rw-r--r--src/main/java/eu/siacs/conversations/entities/Bookmark.java176
-rw-r--r--src/main/java/eu/siacs/conversations/entities/Contact.java562
-rw-r--r--src/main/java/eu/siacs/conversations/entities/Conversation.java947
-rw-r--r--src/main/java/eu/siacs/conversations/entities/DownloadableFile.java83
-rw-r--r--src/main/java/eu/siacs/conversations/entities/ListItem.java37
-rw-r--r--src/main/java/eu/siacs/conversations/entities/Message.java617
-rw-r--r--src/main/java/eu/siacs/conversations/entities/MucOptions.java509
-rw-r--r--src/main/java/eu/siacs/conversations/entities/Presence.java80
-rw-r--r--src/main/java/eu/siacs/conversations/entities/Presences.java60
-rw-r--r--src/main/java/eu/siacs/conversations/entities/Roster.java96
-rw-r--r--src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java293
-rw-r--r--src/main/java/eu/siacs/conversations/entities/Transferable.java31
-rw-r--r--src/main/java/eu/siacs/conversations/entities/TransferablePlaceholder.java34
-rw-r--r--src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java74
-rw-r--r--src/main/java/eu/siacs/conversations/generator/IqGenerator.java290
-rw-r--r--src/main/java/eu/siacs/conversations/generator/MessageGenerator.java217
-rw-r--r--src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java62
-rw-r--r--src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java92
-rw-r--r--src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java376
-rw-r--r--src/main/java/eu/siacs/conversations/parser/AbstractParser.java96
-rw-r--r--src/main/java/eu/siacs/conversations/parser/IqParser.java355
-rw-r--r--src/main/java/eu/siacs/conversations/parser/MessageParser.java581
-rw-r--r--src/main/java/eu/siacs/conversations/parser/PresenceParser.java265
-rw-r--r--src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java1219
-rw-r--r--src/main/java/eu/siacs/conversations/persistance/FileBackend.java274
-rw-r--r--src/main/java/eu/siacs/conversations/persistance/OnPhoneContactsMerged.java5
-rw-r--r--src/main/java/eu/siacs/conversations/providers/ConversationsPlusFileProvider.java20
-rw-r--r--src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java121
-rw-r--r--src/main/java/eu/siacs/conversations/services/AvatarService.java709
-rw-r--r--src/main/java/eu/siacs/conversations/services/ContactChooserTargetService.java83
-rw-r--r--src/main/java/eu/siacs/conversations/services/EventReceiver.java25
-rw-r--r--src/main/java/eu/siacs/conversations/services/ExportLogsService.java147
-rw-r--r--src/main/java/eu/siacs/conversations/services/MessageArchiveService.java422
-rw-r--r--src/main/java/eu/siacs/conversations/services/NotificationService.java584
-rw-r--r--src/main/java/eu/siacs/conversations/services/XmppConnectionService.java2775
-rw-r--r--src/main/java/eu/siacs/conversations/ui/AboutActivity.java15
-rw-r--r--src/main/java/eu/siacs/conversations/ui/AboutPreference.java32
-rw-r--r--src/main/java/eu/siacs/conversations/ui/AbstractSearchableListItemActivity.java124
-rw-r--r--src/main/java/eu/siacs/conversations/ui/BlockContactDialog.java41
-rw-r--r--src/main/java/eu/siacs/conversations/ui/BlocklistActivity.java74
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ChangePasswordActivity.java103
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java223
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java714
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java529
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ConversationActivity.java1619
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ConversationFragment.java1389
-rw-r--r--src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java970
-rw-r--r--src/main/java/eu/siacs/conversations/ui/EditMessage.java90
-rw-r--r--src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java134
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ExportLogsPreference.java36
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java390
-rw-r--r--src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java320
-rw-r--r--src/main/java/eu/siacs/conversations/ui/SettingsActivity.java229
-rw-r--r--src/main/java/eu/siacs/conversations/ui/SettingsFragment.java65
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java359
-rw-r--r--src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java886
-rw-r--r--src/main/java/eu/siacs/conversations/ui/TimePreference.java105
-rw-r--r--src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java320
-rw-r--r--src/main/java/eu/siacs/conversations/ui/UiCallback.java11
-rw-r--r--src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java444
-rw-r--r--src/main/java/eu/siacs/conversations/ui/XmppActivity.java1174
-rw-r--r--src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java77
-rw-r--r--src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java247
-rw-r--r--src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java70
-rw-r--r--src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java188
-rw-r--r--src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java881
-rw-r--r--src/main/java/eu/siacs/conversations/ui/forms/FormBooleanFieldWrapper.java80
-rw-r--r--src/main/java/eu/siacs/conversations/ui/forms/FormFieldFactory.java30
-rw-r--r--src/main/java/eu/siacs/conversations/ui/forms/FormFieldWrapper.java95
-rw-r--r--src/main/java/eu/siacs/conversations/ui/forms/FormJidSingleFieldWrapper.java44
-rw-r--r--src/main/java/eu/siacs/conversations/ui/forms/FormTextFieldWrapper.java97
-rw-r--r--src/main/java/eu/siacs/conversations/ui/forms/FormWrapper.java72
-rw-r--r--src/main/java/eu/siacs/conversations/ui/listeners/ConversationMoreMessagesLoadedListener.java143
-rw-r--r--src/main/java/eu/siacs/conversations/ui/listeners/ConversationSwipeRefreshListener.java103
-rw-r--r--src/main/java/eu/siacs/conversations/utils/CryptoHelper.java211
-rw-r--r--src/main/java/eu/siacs/conversations/utils/DNSHelper.java165
-rw-r--r--src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java46
-rw-r--r--src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java117
-rw-r--r--src/main/java/eu/siacs/conversations/utils/ExifHelper.java161
-rw-r--r--src/main/java/eu/siacs/conversations/utils/FileUtils.java261
-rw-r--r--src/main/java/eu/siacs/conversations/utils/GeoHelper.java77
-rw-r--r--src/main/java/eu/siacs/conversations/utils/MimeUtils.java487
-rw-r--r--src/main/java/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java9
-rw-r--r--src/main/java/eu/siacs/conversations/utils/PRNGFixes.java328
-rw-r--r--src/main/java/eu/siacs/conversations/utils/PhoneHelper.java136
-rw-r--r--src/main/java/eu/siacs/conversations/utils/SSLSocketHelper.java73
-rw-r--r--src/main/java/eu/siacs/conversations/utils/SerialSingleThreadExecutor.java34
-rw-r--r--src/main/java/eu/siacs/conversations/utils/SocksSocketFactory.java53
-rw-r--r--src/main/java/eu/siacs/conversations/utils/UIHelper.java323
-rw-r--r--src/main/java/eu/siacs/conversations/utils/XmlHelper.java12
-rw-r--r--src/main/java/eu/siacs/conversations/utils/Xmlns.java11
-rw-r--r--src/main/java/eu/siacs/conversations/utils/XmppUri.java85
-rw-r--r--src/main/java/eu/siacs/conversations/xml/Element.java188
-rw-r--r--src/main/java/eu/siacs/conversations/xml/Tag.java104
-rw-r--r--src/main/java/eu/siacs/conversations/xml/TagWriter.java114
-rw-r--r--src/main/java/eu/siacs/conversations/xml/XmlReader.java141
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/OnAdvancedStreamFeaturesLoaded.java7
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/OnBindListener.java7
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/OnContactStatusChanged.java7
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/OnIqPacketReceived.java8
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/OnKeyStatusUpdated.java7
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/OnMessageAcknowledged.java7
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/OnMessagePacketReceived.java8
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/OnPresencePacketReceived.java8
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/OnStatusChanged.java7
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/OnUpdateBlocklist.java13
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/PacketReceived.java5
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java1574
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/chatstate/ChatState.java32
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/forms/Data.java99
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/forms/Field.java81
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jid/InvalidJidException.java49
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jid/Jid.java226
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java147
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java1025
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java170
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java239
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java217
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java15
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/OnFileTransmissionStatusChanged.java9
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java9
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java5
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/OnTransportConnected.java7
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java103
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java96
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java13
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java102
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractAcknowledgeableStanza.java31
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java50
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java69
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java99
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/stanzas/PresencePacket.java8
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/ActivePacket.java10
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/InactivePacket.java10
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/AckPacket.java13
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/EnablePacket.java13
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/RequestPacket.java12
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/ResumePacket.java14
161 files changed, 37153 insertions, 0 deletions
diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java
new file mode 100644
index 00000000..a9297c81
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/Config.java
@@ -0,0 +1,134 @@
+package eu.siacs.conversations;
+
+import android.graphics.Bitmap;
+
+import de.thedevstack.conversationsplus.ConversationsPlusPreferences;
+import eu.siacs.conversations.xmpp.chatstate.ChatState;
+
+public final class Config {
+
+
+ private static final int UNENCRYPTED = 1;
+ private static final int OPENPGP = 2;
+ private static final int OTR = 4;
+ private static final int OMEMO = 8;
+
+ private static final int ENCRYPTION_MASK = UNENCRYPTED | OPENPGP | OTR | OMEMO;
+
+ public static boolean supportUnencrypted() {
+ return (ENCRYPTION_MASK & UNENCRYPTED) != 0;
+ }
+
+ public static boolean supportOpenPgp() {
+ return (ENCRYPTION_MASK & OPENPGP) != 0;
+ }
+
+ public static boolean supportOtr() {
+ return (ENCRYPTION_MASK & OTR) != 0;
+ }
+
+ public static boolean supportOmemo() {
+ return ConversationsPlusPreferences.omemoEnabled();
+ }
+
+ public static boolean multipleEncryptionChoices() {
+ return (ENCRYPTION_MASK & (ENCRYPTION_MASK - 1)) != 0;
+ }
+
+ public static final String LOGTAG = BuildConfig.LOGTAG;
+
+ public static final String DOMAIN_LOCK = BuildConfig.LOCKED_IN_DOMAIN; //only allow account creation for this domain
+ public static final String CONFERENCE_DOMAIN_LOCK = BuildConfig.LOCKED_IN_DOMAIN_CONFERENCES; //only allow conference creation for this domain
+ public static final boolean LOCK_DOMAINS_IN_CONVERSATIONS = BuildConfig.CONTACTS_CONFERENCES_LOCKED_TO_DOMAIN; //only add contacts and conferences for own domains
+
+ public static final boolean LOCK_SETTINGS = BuildConfig.ACCOUNT_SETTINGS_LOCKED; //set to true to disallow account and settings editing
+ public static final boolean DISALLOW_REGISTRATION_IN_UI = BuildConfig.DISALLOW_REGISTRATION_IN_UI; //hide the register checkbox
+
+ public static final boolean ALLOW_NON_TLS_CONNECTIONS = BuildConfig.ALLOW_NON_TLS_CONNECTIONS; //very dangerous. you should have a good reason to set this to true
+ public static final boolean HIDE_MESSAGE_TEXT_IN_NOTIFICATION = BuildConfig.HIDE_MESSAGE_TEXT_IN_NOTIFICATION;
+ public static final boolean SHOW_CONNECTED_ACCOUNTS = BuildConfig.SHOW_CONNECTED_ACCOUNTS_IN_FOREGROUND_NOTIFICATION; //show number of connected accounts in foreground notification
+
+ public static final int PING_MAX_INTERVAL = BuildConfig.PING_MAX_INTERVAL;
+ public static final int PING_MIN_INTERVAL = BuildConfig.PING_MIN_INTERVAL;
+ public static final int PING_TIMEOUT = BuildConfig.PING_TIMEOUT;
+ public static final int SOCKET_TIMEOUT = BuildConfig.SOCKET_TIMEOUT;
+ public static final int CONNECT_TIMEOUT = BuildConfig.CONNECT_TIMEOUT;
+ public static final int CONNECT_DISCO_TIMEOUT = BuildConfig.CONNECT_DISCO_TIMEOUT;
+ public static final int CARBON_GRACE_PERIOD = BuildConfig.CARBON_GRACE_PERIOD;
+ public static final int MINI_GRACE_PERIOD = BuildConfig.MINI_GRACE_PERIOD;
+
+ public static final boolean CLOSE_TCP_WHEN_SWITCHING_TO_BACKGROUND = BuildConfig.CLOSE_TCP_WHEN_SWITCHING_TO_BACKGROUND;
+
+ public static final int AVATAR_SIZE = BuildConfig.AVATAR_SIZE;
+ public static final Bitmap.CompressFormat AVATAR_FORMAT = BuildConfig.AVATAR_FORMAT;
+
+ public static final int PAGE_SIZE = BuildConfig.PAGE_SIZE;
+ public static final int MAX_NUM_PAGES = BuildConfig.MAX_NUM_PAGES;
+
+ public static final int REFRESH_UI_INTERVAL = BuildConfig.REFRESH_UI_INTERVAL;
+
+ public static final boolean DISABLE_PROXY_LOOKUP = BuildConfig.DISABLE_PROXY_LOOKUP; //useful to debug ibb
+ public static final boolean DISABLE_HTTP_UPLOAD = BuildConfig.DISABLE_HTTP_UPLOAD;
+ public static final boolean DISABLE_STRING_PREP = BuildConfig.DISABLE_STRING_PREP; // setting to true might increase startup performance
+ public static final boolean EXTENDED_SM_LOGGING = BuildConfig.EXTENDED_SM_LOGGING; // log stanza counts
+ public static final boolean RESET_ATTEMPT_COUNT_ON_NETWORK_CHANGE = BuildConfig.RESET_ATTEMPT_COUNT_ON_NETWORK_CHANGE; //setting to true might increase power consumption
+
+ public static final boolean ENCRYPT_ON_HTTP_UPLOADED = BuildConfig.ENCRYPT_ON_HTTP_UPLOADED;
+
+ public static final boolean REPORT_WRONG_FILESIZE_IN_OTR_JINGLE = BuildConfig.REPORT_WRONG_FILESIZE_IN_OTR_JINGLE;
+
+ public static final boolean SHOW_REGENERATE_AXOLOTL_KEYS_BUTTON = BuildConfig.SHOW_REGENERATE_AXOLOTL_KEYS_BUTTON;
+
+ public static final boolean X509_VERIFICATION = BuildConfig.X509_VERIFICATION_OF_OMEMO_KEYS; //use x509 certificates to verify OMEMO keys
+
+ public static final boolean IGNORE_ID_REWRITE_IN_MUC = BuildConfig.IGNORE_ID_REWRITE_IN_MUC;
+
+ public static final boolean REQUEST_DISCO = BuildConfig.REQUEST_DISCO;
+
+ 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 int MAM_MAX_MESSAGES = BuildConfig.MAM_MAX_MESSAGES;
+
+ public static final ChatState DEFAULT_CHATSTATE = ChatState.ACTIVE;
+ public static final int TYPING_TIMEOUT = BuildConfig.TYPING_TIMEOUT;
+
+ public static final String ENABLED_CIPHERS[] = {
+ "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
+ "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA384",
+ "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA256",
+ "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
+ "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
+ "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
+
+ "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256",
+ "TLS_DHE_RSA_WITH_AES_128_GCM_SHA384",
+ "TLS_DHE_RSA_WITH_AES_256_GCM_SHA256",
+ "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384",
+
+ "TLS_DHE_RSA_WITH_CAMELLIA_256_SHA",
+
+ // Fallback.
+ "TLS_RSA_WITH_AES_128_GCM_SHA256",
+ "TLS_RSA_WITH_AES_128_GCM_SHA384",
+ "TLS_RSA_WITH_AES_256_GCM_SHA256",
+ "TLS_RSA_WITH_AES_256_GCM_SHA384",
+ "TLS_RSA_WITH_AES_128_CBC_SHA256",
+ "TLS_RSA_WITH_AES_128_CBC_SHA384",
+ "TLS_RSA_WITH_AES_256_CBC_SHA256",
+ "TLS_RSA_WITH_AES_256_CBC_SHA384",
+ "TLS_RSA_WITH_AES_128_CBC_SHA",
+ "TLS_RSA_WITH_AES_256_CBC_SHA",
+ };
+
+ public static final String WEAK_CIPHER_PATTERNS[] = {
+ "_NULL_",
+ "_EXPORT_",
+ "_anon_",
+ "_RC4_",
+ "_DES_",
+ "_MD5",
+ };
+
+ private Config() {
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/crypto/OtrService.java b/src/main/java/eu/siacs/conversations/crypto/OtrService.java
new file mode 100644
index 00000000..4ddf51fb
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/crypto/OtrService.java
@@ -0,0 +1,308 @@
+package eu.siacs.conversations.crypto;
+
+import net.java.otr4j.OtrEngineHost;
+import net.java.otr4j.OtrException;
+import net.java.otr4j.OtrPolicy;
+import net.java.otr4j.OtrPolicyImpl;
+import net.java.otr4j.crypto.OtrCryptoEngineImpl;
+import net.java.otr4j.crypto.OtrCryptoException;
+import net.java.otr4j.session.FragmenterInstructions;
+import net.java.otr4j.session.InstanceTag;
+import net.java.otr4j.session.SessionID;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.math.BigInteger;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.spec.DSAPrivateKeySpec;
+import java.security.spec.DSAPublicKeySpec;
+import java.security.spec.InvalidKeySpecException;
+
+import de.thedevstack.android.logcat.Logging;
+import de.thedevstack.conversationsplus.ConversationsPlusPreferences;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.generator.MessageGenerator;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xmpp.chatstate.ChatState;
+import eu.siacs.conversations.xmpp.jid.InvalidJidException;
+import eu.siacs.conversations.xmpp.jid.Jid;
+import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
+
+public class OtrService extends OtrCryptoEngineImpl implements OtrEngineHost {
+
+ private Account account;
+ private OtrPolicy otrPolicy;
+ private KeyPair keyPair;
+ private XmppConnectionService mXmppConnectionService;
+
+ public OtrService(XmppConnectionService service, Account account) {
+ this.account = account;
+ this.otrPolicy = new OtrPolicyImpl();
+ this.otrPolicy.setAllowV1(false);
+ this.otrPolicy.setAllowV2(true);
+ this.otrPolicy.setAllowV3(true);
+ this.keyPair = loadKey(account.getKeys());
+ this.mXmppConnectionService = service;
+ }
+
+ private KeyPair loadKey(JSONObject keys) {
+ if (keys == null) {
+ return null;
+ }
+ try {
+ BigInteger x = new BigInteger(keys.getString("otr_x"), 16);
+ BigInteger y = new BigInteger(keys.getString("otr_y"), 16);
+ BigInteger p = new BigInteger(keys.getString("otr_p"), 16);
+ BigInteger q = new BigInteger(keys.getString("otr_q"), 16);
+ BigInteger g = new BigInteger(keys.getString("otr_g"), 16);
+ KeyFactory keyFactory = KeyFactory.getInstance("DSA");
+ DSAPublicKeySpec pubKeySpec = new DSAPublicKeySpec(y, p, q, g);
+ DSAPrivateKeySpec privateKeySpec = new DSAPrivateKeySpec(x, p, q, g);
+ PublicKey publicKey = keyFactory.generatePublic(pubKeySpec);
+ PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec);
+ return new KeyPair(publicKey, privateKey);
+ } catch (JSONException e) {
+ return null;
+ } catch (NoSuchAlgorithmException e) {
+ return null;
+ } catch (InvalidKeySpecException e) {
+ return null;
+ }
+ }
+
+ private void saveKey() {
+ PublicKey publicKey = keyPair.getPublic();
+ PrivateKey privateKey = keyPair.getPrivate();
+ KeyFactory keyFactory;
+ try {
+ keyFactory = KeyFactory.getInstance("DSA");
+ DSAPrivateKeySpec privateKeySpec = keyFactory.getKeySpec(
+ privateKey, DSAPrivateKeySpec.class);
+ DSAPublicKeySpec publicKeySpec = keyFactory.getKeySpec(publicKey,
+ DSAPublicKeySpec.class);
+ this.account.setKey("otr_x", privateKeySpec.getX().toString(16));
+ this.account.setKey("otr_g", privateKeySpec.getG().toString(16));
+ this.account.setKey("otr_p", privateKeySpec.getP().toString(16));
+ this.account.setKey("otr_q", privateKeySpec.getQ().toString(16));
+ this.account.setKey("otr_y", publicKeySpec.getY().toString(16));
+ } catch (final NoSuchAlgorithmException | InvalidKeySpecException e) {
+ e.printStackTrace();
+ }
+
+ }
+
+ @Override
+ public void askForSecret(SessionID id, InstanceTag instanceTag, String question) {
+ try {
+ final Jid jid = Jid.fromSessionID(id);
+ Conversation conversation = this.mXmppConnectionService.find(this.account,jid);
+ if (conversation!=null) {
+ conversation.smp().hint = question;
+ conversation.smp().status = Conversation.Smp.STATUS_CONTACT_REQUESTED;
+ mXmppConnectionService.updateConversationUi();
+ }
+ } catch (InvalidJidException e) {
+ Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": smp in invalid session "+id.toString());
+ }
+ }
+
+ @Override
+ public void finishedSessionMessage(SessionID arg0, String arg1)
+ throws OtrException {
+
+ }
+
+ @Override
+ public String getFallbackMessage(SessionID arg0) {
+ return "I would like to start a private (OTR encrypted) conversation but your client doesn’t seem to support that";
+ }
+
+ @Override
+ public byte[] getLocalFingerprintRaw(SessionID arg0) {
+ try {
+ return getFingerprintRaw(getPublicKey());
+ } catch (OtrCryptoException e) {
+ return null;
+ }
+ }
+
+ public PublicKey getPublicKey() {
+ if (this.keyPair == null) {
+ return null;
+ }
+ return this.keyPair.getPublic();
+ }
+
+ @Override
+ public KeyPair getLocalKeyPair(SessionID arg0) throws OtrException {
+ if (this.keyPair == null) {
+ KeyPairGenerator kg;
+ try {
+ kg = KeyPairGenerator.getInstance("DSA");
+ this.keyPair = kg.genKeyPair();
+ this.saveKey();
+ mXmppConnectionService.databaseBackend.updateAccount(account);
+ } catch (NoSuchAlgorithmException e) {
+ Logging.d(Config.LOGTAG,
+ "error generating key pair " + e.getMessage());
+ }
+ }
+ return this.keyPair;
+ }
+
+ @Override
+ public String getReplyForUnreadableMessage(SessionID arg0) {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public OtrPolicy getSessionPolicy(SessionID arg0) {
+ return otrPolicy;
+ }
+
+ @Override
+ public void injectMessage(SessionID session, String body)
+ throws OtrException {
+ MessagePacket packet = new MessagePacket();
+ packet.setFrom(account.getJid());
+ if (session.getUserID().isEmpty()) {
+ packet.setAttribute("to", session.getAccountID());
+ } else {
+ packet.setAttribute("to", session.getAccountID() + "/" + session.getUserID());
+ }
+ packet.setBody(body);
+ MessageGenerator.addMessageHints(packet);
+ try {
+ Jid jid = Jid.fromSessionID(session);
+ Conversation conversation = mXmppConnectionService.find(account,jid);
+ if (conversation != null && conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) {
+ if (ConversationsPlusPreferences.chatStates()) {
+ packet.addChild(ChatState.toElement(conversation.getOutgoingChatState()));
+ }
+ }
+ } catch (final InvalidJidException ignored) {
+
+ }
+
+ packet.setType(MessagePacket.TYPE_CHAT);
+ account.getXmppConnection().sendMessagePacket(packet);
+ }
+
+ @Override
+ public void messageFromAnotherInstanceReceived(SessionID session) {
+ sendOtrErrorMessage(session, "Message from another OTR-instance received");
+ }
+
+ @Override
+ public void multipleInstancesDetected(SessionID arg0) {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void requireEncryptedMessage(SessionID arg0, String arg1)
+ throws OtrException {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void showError(SessionID arg0, String arg1) throws OtrException {
+ Logging.d(Config.LOGTAG,"show error");
+ }
+
+ @Override
+ public void smpAborted(SessionID id) throws OtrException {
+ setSmpStatus(id, Conversation.Smp.STATUS_NONE);
+ }
+
+ private void setSmpStatus(SessionID id, int status) {
+ try {
+ final Jid jid = Jid.fromSessionID(id);
+ Conversation conversation = this.mXmppConnectionService.find(this.account,jid);
+ if (conversation!=null) {
+ conversation.smp().status = status;
+ mXmppConnectionService.updateConversationUi();
+ }
+ } catch (final InvalidJidException ignored) {
+
+ }
+ }
+
+ @Override
+ public void smpError(SessionID id, int arg1, boolean arg2)
+ throws OtrException {
+ setSmpStatus(id, Conversation.Smp.STATUS_NONE);
+ }
+
+ @Override
+ public void unencryptedMessageReceived(SessionID arg0, String arg1)
+ throws OtrException {
+ throw new OtrException(new Exception("unencrypted message received"));
+ }
+
+ @Override
+ public void unreadableMessageReceived(SessionID session) throws OtrException {
+ Logging.d(Config.LOGTAG,"unreadable message received");
+ // Hier update des contents fuer FS#96
+ sendOtrErrorMessage(session, "You sent me an unreadable OTR-encrypted message");
+ }
+
+ public void sendOtrErrorMessage(SessionID session, String errorText) {
+ try {
+ Jid jid = Jid.fromSessionID(session);
+ Conversation conversation = mXmppConnectionService.find(account, jid);
+ String id = conversation == null ? null : conversation.getLastReceivedOtrMessageId();
+ if (id != null) {
+ MessagePacket packet = mXmppConnectionService.getMessageGenerator()
+ .generateOtrError(jid, id, errorText);
+ packet.setFrom(account.getJid());
+ mXmppConnectionService.sendMessagePacket(account,packet);
+ Logging.d(Config.LOGTAG,packet.toString());
+ Logging.d(Config.LOGTAG,account.getJid().toBareJid().toString()
+ +": unreadable OTR message in "+conversation.getName());
+ }
+ } catch (InvalidJidException e) {
+ return;
+ }
+ }
+
+ @Override
+ public void unverify(SessionID id, String arg1) {
+ setSmpStatus(id, Conversation.Smp.STATUS_FAILED);
+ }
+
+ @Override
+ public void verify(SessionID id, String fingerprint, boolean approved) {
+ Logging.d(Config.LOGTAG,"OtrService.verify("+id.toString()+","+fingerprint+","+String.valueOf(approved)+")");
+ try {
+ final Jid jid = Jid.fromSessionID(id);
+ Conversation conversation = this.mXmppConnectionService.find(this.account,jid);
+ if (conversation!=null) {
+ if (approved) {
+ conversation.getContact().addOtrFingerprint(fingerprint);
+ }
+ conversation.smp().hint = null;
+ conversation.smp().status = Conversation.Smp.STATUS_VERIFIED;
+ mXmppConnectionService.updateConversationUi();
+ mXmppConnectionService.syncRosterToDisk(conversation.getAccount());
+ }
+ } catch (final InvalidJidException ignored) {
+ }
+ }
+
+ @Override
+ public FragmenterInstructions getFragmenterInstructions(SessionID sessionID) {
+ return null;
+ }
+
+}
diff --git a/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java b/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java
new file mode 100644
index 00000000..5afbe5c4
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java
@@ -0,0 +1,162 @@
+package eu.siacs.conversations.crypto;
+
+import android.app.PendingIntent;
+
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.ui.UiCallback;
+
+public class PgpDecryptionService {
+
+ private final XmppConnectionService xmppConnectionService;
+ private final ConcurrentHashMap<String, List<Message>> messages = new ConcurrentHashMap<>();
+ private final ConcurrentHashMap<String, Boolean> decryptingMessages = new ConcurrentHashMap<>();
+ private Boolean keychainLocked = false;
+ private final Object keychainLockedLock = new Object();
+
+ public PgpDecryptionService(XmppConnectionService xmppConnectionService) {
+ this.xmppConnectionService = xmppConnectionService;
+ }
+
+ public void add(Message message) {
+ if (isRunning()) {
+ decryptDirectly(message);
+ } else {
+ store(message);
+ }
+ }
+
+ public void addAll(List<Message> messagesList) {
+ if (!messagesList.isEmpty()) {
+ String conversationUuid = messagesList.get(0).getConversation().getUuid();
+ if (!messages.containsKey(conversationUuid)) {
+ List<Message> list = Collections.synchronizedList(new LinkedList<Message>());
+ messages.put(conversationUuid, list);
+ }
+ synchronized (messages.get(conversationUuid)) {
+ messages.get(conversationUuid).addAll(messagesList);
+ }
+ decryptAllMessages();
+ }
+ }
+
+ public void onKeychainUnlocked() {
+ synchronized (keychainLockedLock) {
+ keychainLocked = false;
+ }
+ decryptAllMessages();
+ }
+
+ public void onKeychainLocked() {
+ synchronized (keychainLockedLock) {
+ keychainLocked = true;
+ }
+ xmppConnectionService.updateConversationUi();
+ }
+
+ public void onOpenPgpServiceBound() {
+ decryptAllMessages();
+ }
+
+ public boolean isRunning() {
+ synchronized (keychainLockedLock) {
+ return !keychainLocked;
+ }
+ }
+
+ private void store(Message message) {
+ if (messages.containsKey(message.getConversation().getUuid())) {
+ messages.get(message.getConversation().getUuid()).add(message);
+ } else {
+ List<Message> messageList = Collections.synchronizedList(new LinkedList<Message>());
+ messageList.add(message);
+ messages.put(message.getConversation().getUuid(), messageList);
+ }
+ }
+
+ private void decryptAllMessages() {
+ for (String uuid : messages.keySet()) {
+ decryptMessages(uuid);
+ }
+ }
+
+ private void decryptMessages(final String uuid) {
+ synchronized (decryptingMessages) {
+ Boolean decrypting = decryptingMessages.get(uuid);
+ if ((decrypting != null && !decrypting) || decrypting == null) {
+ decryptingMessages.put(uuid, true);
+ decryptMessage(uuid);
+ }
+ }
+ }
+
+ private void decryptMessage(final String uuid) {
+ Message message = null;
+ synchronized (messages.get(uuid)) {
+ while (!messages.get(uuid).isEmpty()) {
+ if (messages.get(uuid).get(0).getEncryption() == Message.ENCRYPTION_PGP) {
+ if (isRunning()) {
+ message = messages.get(uuid).remove(0);
+ }
+ break;
+ } else {
+ messages.get(uuid).remove(0);
+ }
+ }
+ if (message != null && xmppConnectionService.getPgpEngine() != null) {
+ xmppConnectionService.getPgpEngine().decrypt(message, new UiCallback<Message>() {
+
+ @Override
+ public void userInputRequried(PendingIntent pi, Message message) {
+ messages.get(uuid).add(0, message);
+ decryptingMessages.put(uuid, false);
+ }
+
+ @Override
+ public void success(Message message) {
+ xmppConnectionService.updateConversationUi();
+ decryptMessage(uuid);
+ }
+
+ @Override
+ public void error(int error, Message message) {
+ message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
+ xmppConnectionService.updateConversationUi();
+ decryptMessage(uuid);
+ }
+ });
+ } else {
+ decryptingMessages.put(uuid, false);
+ }
+ }
+ }
+
+ private void decryptDirectly(final Message message) {
+ if (message.getEncryption() == Message.ENCRYPTION_PGP && xmppConnectionService.getPgpEngine() != null) {
+ xmppConnectionService.getPgpEngine().decrypt(message, new UiCallback<Message>() {
+
+ @Override
+ public void userInputRequried(PendingIntent pi, Message message) {
+ store(message);
+ }
+
+ @Override
+ public void success(Message message) {
+ xmppConnectionService.updateConversationUi();
+ xmppConnectionService.getNotificationService().updateNotification(false);
+ }
+
+ @Override
+ public void error(int error, Message message) {
+ message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
+ xmppConnectionService.updateConversationUi();
+ }
+ });
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java b/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java
new file mode 100644
index 00000000..4ad01089
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java
@@ -0,0 +1,406 @@
+package eu.siacs.conversations.crypto;
+
+import android.app.PendingIntent;
+import android.content.Intent;
+
+import org.openintents.openpgp.OpenPgpSignatureResult;
+import org.openintents.openpgp.util.OpenPgpApi;
+import org.openintents.openpgp.util.OpenPgpApi.IOpenPgpCallback;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import de.thedevstack.conversationsplus.ConversationsPlusPreferences;
+import de.thedevstack.conversationsplus.utils.StreamUtil;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.http.HttpConnectionManager;
+import eu.siacs.conversations.persistance.FileBackend;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.ui.UiCallback;
+
+public class PgpEngine {
+ private OpenPgpApi api;
+ private XmppConnectionService mXmppConnectionService;
+ private static PgpEngine INSTANCE;
+
+ public PgpEngine(OpenPgpApi api, XmppConnectionService service) {
+ this.api = api;
+ this.mXmppConnectionService = service;
+ INSTANCE = this;
+ }
+
+ public static PgpEngine getInstance() {
+ return INSTANCE;
+ }
+
+ public void decrypt(final Message message, final UiCallback<Message> callback) {
+ Intent params = new Intent();
+ params.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY);
+ final String uuid = message.getUuid();
+ if (message.getType() == Message.TYPE_TEXT) {
+ InputStream is = new ByteArrayInputStream(message.getBody().getBytes());
+ final OutputStream os = new ByteArrayOutputStream();
+ api.executeApiAsync(params, is, os, new IOpenPgpCallback() {
+
+ @Override
+ public void onReturn(Intent result) {
+ notifyPgpDecryptionService(message.getConversation().getAccount(), OpenPgpApi.ACTION_DECRYPT_VERIFY, result);
+ switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
+ case OpenPgpApi.RESULT_CODE_SUCCESS:
+ try {
+ os.flush();
+ if (message.getEncryption() == Message.ENCRYPTION_PGP
+ && message.getUuid().equals(uuid)) {
+ message.setBody(os.toString());
+ message.setEncryption(Message.ENCRYPTION_DECRYPTED);
+ if (message.trusted()
+ && message.treatAsDownloadable() != Message.Decision.NEVER
+ && (message.isHttpUploaded() || ConversationsPlusPreferences.autoDownloadFileLink())
+ && ConversationsPlusPreferences.autoAcceptFileSize() > 0) {
+ HttpConnectionManager.createNewDownloadConnection(message);
+ }
+ mXmppConnectionService.updateMessage(message);
+ callback.success(message);
+ }
+ } catch (IOException e) {
+ callback.error(R.string.openpgp_error, message);
+ return;
+ }
+
+ return;
+ case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
+ callback.userInputRequried((PendingIntent) result
+ .getParcelableExtra(OpenPgpApi.RESULT_INTENT),
+ message);
+ return;
+ case OpenPgpApi.RESULT_CODE_ERROR:
+ callback.error(R.string.openpgp_error, message);
+ }
+ }
+ });
+ } else if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) {
+ try {
+ final DownloadableFile inputFile = FileBackend.getFile(message, false);
+ final DownloadableFile outputFile = FileBackend.getFile(message, true);
+ outputFile.getParentFile().mkdirs();
+ outputFile.createNewFile();
+ InputStream is = new FileInputStream(inputFile);
+ OutputStream os = new FileOutputStream(outputFile);
+ api.executeApiAsync(params, is, os, new IOpenPgpCallback() {
+
+ @Override
+ public void onReturn(Intent result) {
+ notifyPgpDecryptionService(message.getConversation().getAccount(), OpenPgpApi.ACTION_DECRYPT_VERIFY, result);
+ switch (result.getIntExtra(OpenPgpApi.RESULT_CODE,
+ OpenPgpApi.RESULT_CODE_ERROR)) {
+ case OpenPgpApi.RESULT_CODE_SUCCESS:
+ message.setEncryption(Message.ENCRYPTION_DECRYPTED);
+ PgpEngine.this.mXmppConnectionService
+ .updateMessage(message);
+ inputFile.delete();
+ FileBackend.updateMediaScanner(outputFile, mXmppConnectionService);
+ callback.success(message);
+ return;
+ case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
+ callback.userInputRequried(
+ (PendingIntent) result
+ .getParcelableExtra(OpenPgpApi.RESULT_INTENT),
+ message);
+ return;
+ case OpenPgpApi.RESULT_CODE_ERROR:
+ callback.error(R.string.openpgp_error, message);
+ }
+ }
+ });
+ } catch (final IOException e) {
+ callback.error(R.string.error_decrypting_file, message);
+ }
+
+ }
+ }
+
+ public void encrypt(final Message message, final UiCallback<Message> callback) {
+ Intent params = new Intent();
+ params.setAction(OpenPgpApi.ACTION_ENCRYPT);
+ final Conversation conversation = message.getConversation();
+ if (conversation.getMode() == Conversation.MODE_SINGLE) {
+ long[] keys = {
+ conversation.getContact().getPgpKeyId(),
+ conversation.getAccount().getPgpId()
+ };
+ params.putExtra(OpenPgpApi.EXTRA_KEY_IDS, keys);
+ } else {
+ params.putExtra(OpenPgpApi.EXTRA_KEY_IDS, conversation.getMucOptions().getPgpKeyIds());
+ }
+
+ if (!message.needsUploading()) {
+ params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true);
+ String body = message.getBody();
+ InputStream is = new ByteArrayInputStream(body.getBytes());
+ final OutputStream os = new ByteArrayOutputStream();
+ api.executeApiAsync(params, is, os, new IOpenPgpCallback() {
+
+ @Override
+ public void onReturn(Intent result) {
+ notifyPgpDecryptionService(message.getConversation().getAccount(), OpenPgpApi.ACTION_ENCRYPT, result);
+ switch (result.getIntExtra(OpenPgpApi.RESULT_CODE,
+ OpenPgpApi.RESULT_CODE_ERROR)) {
+ case OpenPgpApi.RESULT_CODE_SUCCESS:
+ try {
+ os.flush();
+ StringBuilder encryptedMessageBody = new StringBuilder();
+ String[] lines = os.toString().split("\n");
+ for (int i = 2; i < lines.length - 1; ++i) {
+ if (!lines[i].contains("Version")) {
+ encryptedMessageBody.append(lines[i]);
+ }
+ }
+ message.setEncryptedBody(encryptedMessageBody
+ .toString());
+ callback.success(message);
+ } catch (IOException e) {
+ callback.error(R.string.openpgp_error, message);
+ }
+
+ break;
+ case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
+ callback.userInputRequried((PendingIntent) result
+ .getParcelableExtra(OpenPgpApi.RESULT_INTENT),
+ message);
+ break;
+ case OpenPgpApi.RESULT_CODE_ERROR:
+ callback.error(R.string.openpgp_error, message);
+ break;
+ }
+ }
+ });
+ } else {
+ try {
+ DownloadableFile inputFile = FileBackend.getFile(message, true);
+ DownloadableFile outputFile = FileBackend.getFile(message, false);
+ outputFile.getParentFile().mkdirs();
+ outputFile.createNewFile();
+ final InputStream is = new FileInputStream(inputFile);
+ final OutputStream os = new FileOutputStream(outputFile);
+ api.executeApiAsync(params, is, os, new IOpenPgpCallback() {
+
+ @Override
+ public void onReturn(Intent result) {
+ notifyPgpDecryptionService(message.getConversation().getAccount(), OpenPgpApi.ACTION_ENCRYPT, result);
+ switch (result.getIntExtra(OpenPgpApi.RESULT_CODE,
+ OpenPgpApi.RESULT_CODE_ERROR)) {
+ case OpenPgpApi.RESULT_CODE_SUCCESS:
+ try {
+ os.flush();
+ } catch (IOException ignored) {
+ //ignored
+ }
+ StreamUtil.close(os);
+ callback.success(message);
+ break;
+ case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
+ callback.userInputRequried(
+ (PendingIntent) result
+ .getParcelableExtra(OpenPgpApi.RESULT_INTENT),
+ message);
+ break;
+ case OpenPgpApi.RESULT_CODE_ERROR:
+ callback.error(R.string.openpgp_error, message);
+ break;
+ }
+ }
+ });
+ } catch (final IOException e) {
+ callback.error(R.string.openpgp_error, message);
+ }
+ }
+ }
+
+ public long fetchKeyId(Account account, String status, String signature) {
+ if ((signature == null) || (api == null)) {
+ return 0;
+ }
+ if (status == null) {
+ status = "";
+ }
+ final StringBuilder pgpSig = new StringBuilder();
+ pgpSig.append("-----BEGIN PGP SIGNED MESSAGE-----");
+ pgpSig.append('\n');
+ pgpSig.append('\n');
+ pgpSig.append(status);
+ pgpSig.append('\n');
+ pgpSig.append("-----BEGIN PGP SIGNATURE-----");
+ pgpSig.append('\n');
+ pgpSig.append('\n');
+ pgpSig.append(signature.replace("\n", ""));
+ pgpSig.append('\n');
+ pgpSig.append("-----END PGP SIGNATURE-----");
+ Intent params = new Intent();
+ params.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY);
+ params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true);
+ InputStream is = new ByteArrayInputStream(pgpSig.toString().getBytes());
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ Intent result = api.executeApi(params, is, os);
+ notifyPgpDecryptionService(account, OpenPgpApi.ACTION_DECRYPT_VERIFY, result);
+ switch (result.getIntExtra(OpenPgpApi.RESULT_CODE,
+ OpenPgpApi.RESULT_CODE_ERROR)) {
+ case OpenPgpApi.RESULT_CODE_SUCCESS:
+ OpenPgpSignatureResult sigResult = result
+ .getParcelableExtra(OpenPgpApi.RESULT_SIGNATURE);
+ if (sigResult != null) {
+ return sigResult.getKeyId();
+ } else {
+ return 0;
+ }
+ case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
+ return 0;
+ case OpenPgpApi.RESULT_CODE_ERROR:
+ return 0;
+ }
+ return 0;
+ }
+
+ public void chooseKey(final Account account, final UiCallback<Account> callback) {
+ Intent p = new Intent();
+ p.setAction(OpenPgpApi.ACTION_GET_SIGN_KEY_ID);
+ api.executeApiAsync(p, null, null, new IOpenPgpCallback() {
+
+ @Override
+ public void onReturn(Intent result) {
+ switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) {
+ case OpenPgpApi.RESULT_CODE_SUCCESS:
+ callback.success(account);
+ return;
+ case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
+ callback.userInputRequried((PendingIntent) result
+ .getParcelableExtra(OpenPgpApi.RESULT_INTENT),
+ account);
+ return;
+ case OpenPgpApi.RESULT_CODE_ERROR:
+ callback.error(R.string.openpgp_error, account);
+ }
+ }
+ });
+ }
+
+ public void generateSignature(final Account account, String status,
+ final UiCallback<Account> callback) {
+ if (account.getPgpId() == -1) {
+ return;
+ }
+ Intent params = new Intent();
+ params.setAction(OpenPgpApi.ACTION_CLEARTEXT_SIGN);
+ params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true);
+ params.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, account.getPgpId());
+ InputStream is = new ByteArrayInputStream(status.getBytes());
+ final OutputStream os = new ByteArrayOutputStream();
+ api.executeApiAsync(params, is, os, new IOpenPgpCallback() {
+
+ @Override
+ public void onReturn(Intent result) {
+ notifyPgpDecryptionService(account, OpenPgpApi.ACTION_SIGN, result);
+ switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) {
+ case OpenPgpApi.RESULT_CODE_SUCCESS:
+ StringBuilder signatureBuilder = new StringBuilder();
+ try {
+ os.flush();
+ String[] lines = os.toString().split("\n");
+ boolean sig = false;
+ for (String line : lines) {
+ if (sig) {
+ if (line.contains("END PGP SIGNATURE")) {
+ sig = false;
+ } else {
+ if (!line.contains("Version")) {
+ signatureBuilder.append(line);
+ }
+ }
+ }
+ if (line.contains("BEGIN PGP SIGNATURE")) {
+ sig = true;
+ }
+ }
+ } catch (IOException e) {
+ callback.error(R.string.openpgp_error, account);
+ return;
+ }
+ account.setPgpSignature(signatureBuilder.toString());
+ callback.success(account);
+ return;
+ case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
+ callback.userInputRequried((PendingIntent) result
+ .getParcelableExtra(OpenPgpApi.RESULT_INTENT),
+ account);
+ return;
+ case OpenPgpApi.RESULT_CODE_ERROR:
+ callback.error(R.string.openpgp_error, account);
+ }
+ }
+ });
+ }
+
+ public void hasKey(final Contact contact, final UiCallback<Contact> callback) {
+ Intent params = new Intent();
+ params.setAction(OpenPgpApi.ACTION_GET_KEY);
+ params.putExtra(OpenPgpApi.EXTRA_KEY_ID, contact.getPgpKeyId());
+ api.executeApiAsync(params, null, null, new IOpenPgpCallback() {
+
+ @Override
+ public void onReturn(Intent result) {
+ switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) {
+ case OpenPgpApi.RESULT_CODE_SUCCESS:
+ callback.success(contact);
+ return;
+ case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
+ callback.userInputRequried((PendingIntent) result
+ .getParcelableExtra(OpenPgpApi.RESULT_INTENT),
+ contact);
+ return;
+ case OpenPgpApi.RESULT_CODE_ERROR:
+ callback.error(R.string.openpgp_error, contact);
+ }
+ }
+ });
+ }
+
+ public PendingIntent getIntentForKey(Contact contact) {
+ Intent params = new Intent();
+ params.setAction(OpenPgpApi.ACTION_GET_KEY);
+ params.putExtra(OpenPgpApi.EXTRA_KEY_ID, contact.getPgpKeyId());
+ Intent result = api.executeApi(params, null, null);
+ return (PendingIntent) result
+ .getParcelableExtra(OpenPgpApi.RESULT_INTENT);
+ }
+
+ public PendingIntent getIntentForKey(Account account, long pgpKeyId) {
+ Intent params = new Intent();
+ params.setAction(OpenPgpApi.ACTION_GET_KEY);
+ params.putExtra(OpenPgpApi.EXTRA_KEY_ID, pgpKeyId);
+ Intent result = api.executeApi(params, null, null);
+ return (PendingIntent) result
+ .getParcelableExtra(OpenPgpApi.RESULT_INTENT);
+ }
+
+ private void notifyPgpDecryptionService(Account account, String action, final Intent result) {
+ switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) {
+ case OpenPgpApi.RESULT_CODE_SUCCESS:
+ if (OpenPgpApi.ACTION_SIGN.equals(action)) {
+ account.getPgpDecryptionService().onKeychainUnlocked();
+ }
+ break;
+ case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
+ account.getPgpDecryptionService().onKeychainLocked();
+ break;
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/crypto/XmppDomainVerifier.java b/src/main/java/eu/siacs/conversations/crypto/XmppDomainVerifier.java
new file mode 100644
index 00000000..1fca865e
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/crypto/XmppDomainVerifier.java
@@ -0,0 +1,127 @@
+package eu.siacs.conversations.crypto;
+
+import android.util.Log;
+import android.util.Pair;
+
+import org.bouncycastle.asn1.ASN1Primitive;
+import org.bouncycastle.asn1.DERIA5String;
+import org.bouncycastle.asn1.DERTaggedObject;
+import org.bouncycastle.asn1.DERUTF8String;
+import org.bouncycastle.asn1.DLSequence;
+import org.bouncycastle.asn1.x500.RDN;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x500.style.BCStyle;
+import org.bouncycastle.asn1.x500.style.IETFUtils;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
+
+import java.io.IOException;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLSession;
+
+public class XmppDomainVerifier implements HostnameVerifier {
+
+ private static final String LOGTAG = "XmppDomainVerifier";
+
+ private final String SRVName = "1.3.6.1.5.5.7.8.7";
+ private final String xmppAddr = "1.3.6.1.5.5.7.8.5";
+
+ @Override
+ public boolean verify(String domain, SSLSession sslSession) {
+ try {
+ Certificate[] chain = sslSession.getPeerCertificates();
+ if (chain.length == 0 || !(chain[0] instanceof X509Certificate)) {
+ return false;
+ }
+ X509Certificate certificate = (X509Certificate) chain[0];
+ Collection<List<?>> alternativeNames = certificate.getSubjectAlternativeNames();
+ List<String> xmppAddrs = new ArrayList<>();
+ List<String> srvNames = new ArrayList<>();
+ List<String> domains = new ArrayList<>();
+ if (alternativeNames != null) {
+ for (List<?> san : alternativeNames) {
+ Integer type = (Integer) san.get(0);
+ if (type == 0) {
+ Pair<String, String> otherName = parseOtherName((byte[]) san.get(1));
+ if (otherName != null) {
+ switch (otherName.first) {
+ case SRVName:
+ srvNames.add(otherName.second);
+ break;
+ case xmppAddr:
+ xmppAddrs.add(otherName.second);
+ break;
+ default:
+ Log.d(LOGTAG, "oid: " + otherName.first + " value: " + otherName.second);
+ }
+ }
+ } else if (type == 2) {
+ Object value = san.get(1);
+ if (value instanceof String) {
+ domains.add((String) value);
+ }
+ }
+ }
+ }
+ if (srvNames.size() == 0 && xmppAddrs.size() == 0 && domains.size() == 0) {
+ X500Name x500name = new JcaX509CertificateHolder(certificate).getSubject();
+ RDN[] rdns = x500name.getRDNs(BCStyle.CN);
+ for (int i = 0; i < rdns.length; ++i) {
+ domains.add(IETFUtils.valueToString(x500name.getRDNs(BCStyle.CN)[i].getFirst().getValue()));
+ }
+ }
+ Log.d(LOGTAG, "searching for " + domain + " in srvNames: " + srvNames + " xmppAddrs: " + xmppAddrs + " domains:" + domains);
+ return xmppAddrs.contains(domain) || srvNames.contains("_xmpp-client." + domain) || matchDomain(domain, domains);
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ private static Pair<String, String> parseOtherName(byte[] otherName) {
+ try {
+ ASN1Primitive asn1Primitive = ASN1Primitive.fromByteArray(otherName);
+ if (asn1Primitive instanceof DERTaggedObject) {
+ ASN1Primitive inner = ((DERTaggedObject) asn1Primitive).getObject();
+ if (inner instanceof DLSequence) {
+ DLSequence sequence = (DLSequence) inner;
+ if (sequence.size() >= 2 && sequence.getObjectAt(1) instanceof DERTaggedObject) {
+ String oid = sequence.getObjectAt(0).toString();
+ ASN1Primitive value = ((DERTaggedObject) sequence.getObjectAt(1)).getObject();
+ if (value instanceof DERUTF8String) {
+ return new Pair<>(oid, ((DERUTF8String) value).getString());
+ } else if (value instanceof DERIA5String) {
+ return new Pair<>(oid, ((DERIA5String) value).getString());
+ }
+ }
+ }
+ }
+ return null;
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ private static boolean matchDomain(String needle, List<String> haystack) {
+ for (String entry : haystack) {
+ if (entry.startsWith("*.")) {
+ int i = needle.indexOf('.');
+ Log.d(LOGTAG, "comparing " + needle.substring(i) + " and " + entry.substring(1));
+ if (i != -1 && needle.substring(i).equals(entry.substring(1))) {
+ Log.d(LOGTAG, "domain " + needle + " matched " + entry);
+ return true;
+ }
+ } else {
+ if (entry.equals(needle)) {
+ Log.d(LOGTAG, "domain " + needle + " matched " + entry);
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java
new file mode 100644
index 00000000..a066faad
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java
@@ -0,0 +1,120 @@
+package eu.siacs.conversations.crypto.axolotl;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import org.whispersystems.libaxolotl.AxolotlAddress;
+import org.whispersystems.libaxolotl.IdentityKey;
+import org.whispersystems.libaxolotl.state.PreKeyRecord;
+import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
+
+import java.security.cert.X509Certificate;
+import java.util.List;
+import java.util.Set;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+/**
+ * Created by tzur on 02.03.2016.
+ */
+public interface AxolotlService extends OnAdvancedStreamFeaturesLoaded {
+
+ String LOGPREFIX = "AxolotlService";
+
+ String PEP_PREFIX = "eu.siacs.conversations.axolotl";
+ String PEP_DEVICE_LIST = PEP_PREFIX + ".devicelist";
+ String PEP_BUNDLES = PEP_PREFIX + ".bundles";
+ String PEP_VERIFICATION = PEP_PREFIX + ".verification";
+
+ int NUM_KEYS_TO_PUBLISH = 100;
+
+ enum FetchStatus {
+ PENDING,
+ SUCCESS,
+ SUCCESS_VERIFIED,
+ TIMEOUT,
+ ERROR
+ }
+
+ String getOwnFingerprint();
+
+ Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust);
+
+
+ Set<String> getFingerprintsForOwnSessions();
+
+ Set<String> getFingerprintsForContact(Contact contact);
+
+ boolean isPepBroken();
+
+ void regenerateKeys(boolean wipeOther);
+
+ int getOwnDeviceId();
+
+ Set<Integer> getOwnDeviceIds();
+
+ void registerDevices(Jid jid, @NonNull Set<Integer> deviceIds);
+
+ void wipeOtherPepDevices();
+
+ void purgeKey(String fingerprint);
+
+ void publishOwnDeviceIdIfNeeded();
+
+ void publishOwnDeviceId(Set<Integer> deviceIds);
+
+ void publishDeviceVerificationAndBundle(SignedPreKeyRecord signedPreKeyRecord,
+ Set<PreKeyRecord> preKeyRecords,
+ boolean announceAfter,
+ boolean wipe);
+
+ void publishBundlesIfNeeded(boolean announce, boolean wipe);
+
+ XmppAxolotlSession.Trust getFingerprintTrust(String fingerprint);
+
+ X509Certificate getFingerprintCertificate(String fingerprint);
+
+ void setFingerprintTrust(String fingerprint, XmppAxolotlSession.Trust trust);
+
+ Set<AxolotlAddress> findDevicesWithoutSession(Conversation conversation);
+
+ boolean createSessionsIfNeeded(Conversation conversation);
+
+ boolean trustedSessionVerified(Conversation conversation);
+
+ @Nullable
+ XmppAxolotlMessage encrypt(Message message);
+
+ void preparePayloadMessage(Message message, boolean delay);
+
+ XmppAxolotlMessage fetchAxolotlMessageFromCache(Message message);
+
+ XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceivingPayloadMessage(XmppAxolotlMessage message);
+
+ XmppAxolotlMessage.XmppAxolotlKeyTransportMessage processReceivingKeyTransportMessage(XmppAxolotlMessage message);
+
+ boolean fetchMapHasErrors(List<Jid> jids);
+
+ Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust, Jid jid);
+
+ Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust, List<Jid> jids);
+
+ long getNumTrustedKeys(Jid jid);
+
+ boolean anyTargetHasNoTrustedKeys(List<Jid> jids);
+
+ boolean isConversationAxolotlCapable(Conversation conversation);
+
+ List<Jid> getCryptoTargets(Conversation conversation);
+
+ boolean hasPendingKeyFetches(Account account, List<Jid> jids);
+
+ void prepareKeyTransportMessage(Conversation conversation, OnMessageCreatedCallback onMessageCreatedCallback);
+
+ void resetBrokenness();
+}
diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlServiceImpl.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlServiceImpl.java
new file mode 100644
index 00000000..f91dc066
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlServiceImpl.java
@@ -0,0 +1,1047 @@
+package eu.siacs.conversations.crypto.axolotl;
+
+import android.os.Bundle;
+import android.security.KeyChain;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.util.Log;
+import android.util.Pair;
+
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.whispersystems.libaxolotl.AxolotlAddress;
+import org.whispersystems.libaxolotl.IdentityKey;
+import org.whispersystems.libaxolotl.IdentityKeyPair;
+import org.whispersystems.libaxolotl.InvalidKeyException;
+import org.whispersystems.libaxolotl.InvalidKeyIdException;
+import org.whispersystems.libaxolotl.SessionBuilder;
+import org.whispersystems.libaxolotl.UntrustedIdentityException;
+import org.whispersystems.libaxolotl.ecc.ECPublicKey;
+import org.whispersystems.libaxolotl.state.PreKeyBundle;
+import org.whispersystems.libaxolotl.state.PreKeyRecord;
+import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
+import org.whispersystems.libaxolotl.util.KeyHelper;
+
+import java.security.PrivateKey;
+import java.security.Security;
+import java.security.Signature;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.Set;
+
+import de.thedevstack.conversationsplus.ConversationsPlusApplication;
+import de.thedevstack.conversationsplus.utils.MessageUtil;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.parser.IqParser;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded;
+import eu.siacs.conversations.xmpp.OnIqPacketReceived;
+import eu.siacs.conversations.xmpp.jid.InvalidJidException;
+import eu.siacs.conversations.xmpp.jid.Jid;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+public class AxolotlServiceImpl implements OnAdvancedStreamFeaturesLoaded, AxolotlService {
+
+ public static final int publishTriesThreshold = 3;
+
+ private final Account account;
+ private final XmppConnectionService mXmppConnectionService;
+ private final SQLiteAxolotlStore axolotlStore;
+ private final SessionMap sessions;
+ private final Map<Jid, Set<Integer>> deviceIds;
+ private final Map<String, XmppAxolotlMessage> messageCache;
+ private final FetchStatusMap fetchStatusMap;
+ private final SerialSingleThreadExecutor executor;
+ private int numPublishTriesOnEmptyPep = 0;
+ private boolean pepBroken = false;
+
+ @Override
+ public void onAdvancedStreamFeaturesAvailable(Account account) {
+ if (account.getXmppConnection() != null && account.getXmppConnection().getFeatures().pep()) {
+ publishBundlesIfNeeded(true, false);
+ } else {
+ Log.d(Config.LOGTAG,account.getJid().toBareJid()+": skipping OMEMO initialization");
+ }
+ }
+
+ @Override
+ public boolean fetchMapHasErrors(List<Jid> jids) {
+ for(Jid jid : jids) {
+ if (deviceIds.get(jid) != null) {
+ for (Integer foreignId : this.deviceIds.get(jid)) {
+ AxolotlAddress address = new AxolotlAddress(jid.toString(), foreignId);
+ if (fetchStatusMap.getAll(address).containsValue(FetchStatus.ERROR)) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ private static class AxolotlAddressMap<T> {
+ protected Map<String, Map<Integer, T>> map;
+ protected final Object MAP_LOCK = new Object();
+
+ public AxolotlAddressMap() {
+ this.map = new HashMap<>();
+ }
+
+ public void put(AxolotlAddress address, T value) {
+ synchronized (MAP_LOCK) {
+ Map<Integer, T> devices = map.get(address.getName());
+ if (devices == null) {
+ devices = new HashMap<>();
+ map.put(address.getName(), devices);
+ }
+ devices.put(address.getDeviceId(), value);
+ }
+ }
+
+ public T get(AxolotlAddress address) {
+ synchronized (MAP_LOCK) {
+ Map<Integer, T> devices = map.get(address.getName());
+ if (devices == null) {
+ return null;
+ }
+ return devices.get(address.getDeviceId());
+ }
+ }
+
+ public Map<Integer, T> getAll(AxolotlAddress address) {
+ synchronized (MAP_LOCK) {
+ Map<Integer, T> devices = map.get(address.getName());
+ if (devices == null) {
+ return new HashMap<>();
+ }
+ return devices;
+ }
+ }
+
+ public boolean hasAny(AxolotlAddress address) {
+ synchronized (MAP_LOCK) {
+ Map<Integer, T> devices = map.get(address.getName());
+ return devices != null && !devices.isEmpty();
+ }
+ }
+
+ public void clear() {
+ map.clear();
+ }
+
+ }
+
+ private static class SessionMap extends AxolotlAddressMap<XmppAxolotlSession> {
+ private final XmppConnectionService xmppConnectionService;
+ private final Account account;
+
+ public SessionMap(XmppConnectionService service, SQLiteAxolotlStore store, Account account) {
+ super();
+ this.xmppConnectionService = service;
+ this.account = account;
+ this.fillMap(store);
+ }
+
+ private void putDevicesForJid(String bareJid, List<Integer> deviceIds, SQLiteAxolotlStore store) {
+ for (Integer deviceId : deviceIds) {
+ AxolotlAddress axolotlAddress = new AxolotlAddress(bareJid, deviceId);
+ Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Building session for remote address: " + axolotlAddress.toString());
+ IdentityKey identityKey = store.loadSession(axolotlAddress).getSessionState().getRemoteIdentityKey();
+ if(Config.X509_VERIFICATION) {
+ X509Certificate certificate = store.getFingerprintCertificate(identityKey.getFingerprint().replaceAll("\\s", ""));
+ if (certificate != null) {
+ Bundle information = CryptoHelper.extractCertificateInformation(certificate);
+ try {
+ final String cn = information.getString("subject_cn");
+ final Jid jid = Jid.fromString(bareJid);
+ Log.d(Config.LOGTAG,"setting common name for "+jid+" to "+cn);
+ account.getRoster().getContact(jid).setCommonName(cn);
+ } catch (final InvalidJidException ignored) {
+ //ignored
+ }
+ }
+ }
+ this.put(axolotlAddress, new XmppAxolotlSession(account, store, axolotlAddress, identityKey));
+ }
+ }
+
+ private void fillMap(SQLiteAxolotlStore store) {
+ List<Integer> deviceIds = store.getSubDeviceSessions(account.getJid().toBareJid().toString());
+ putDevicesForJid(account.getJid().toBareJid().toString(), deviceIds, store);
+ for (Contact contact : account.getRoster().getContacts()) {
+ Jid bareJid = contact.getJid().toBareJid();
+ String address = bareJid.toString();
+ deviceIds = store.getSubDeviceSessions(address);
+ putDevicesForJid(address, deviceIds, store);
+ }
+
+ }
+
+ @Override
+ public void put(AxolotlAddress address, XmppAxolotlSession value) {
+ super.put(address, value);
+ value.setNotFresh();
+ xmppConnectionService.syncRosterToDisk(account);
+ }
+
+ public void put(XmppAxolotlSession session) {
+ this.put(session.getRemoteAddress(), session);
+ }
+ }
+
+ private static class FetchStatusMap extends AxolotlAddressMap<FetchStatus> {
+
+ }
+
+ public static String getLogprefix(Account account) {
+ return LOGPREFIX + " (" + account.getJid().toBareJid().toString() + "): ";
+ }
+
+ public AxolotlServiceImpl(Account account, XmppConnectionService connectionService) {
+ if (Security.getProvider("BC") == null) {
+ Security.addProvider(new BouncyCastleProvider());
+ }
+ this.mXmppConnectionService = connectionService;
+ this.account = account;
+ this.axolotlStore = new SQLiteAxolotlStore(this.account, this.mXmppConnectionService);
+ this.deviceIds = new HashMap<>();
+ this.messageCache = new HashMap<>();
+ this.sessions = new SessionMap(mXmppConnectionService, axolotlStore, account);
+ this.fetchStatusMap = new FetchStatusMap();
+ this.executor = new SerialSingleThreadExecutor();
+ }
+
+ public String getOwnFingerprint() {
+ return axolotlStore.getIdentityKeyPair().getPublicKey().getFingerprint().replaceAll("\\s", "");
+ }
+
+ @Override
+ public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust) {
+ return axolotlStore.getContactKeysWithTrust(account.getJid().toBareJid().toString(), trust);
+ }
+
+ @Override
+ public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust, Jid jid) {
+ return axolotlStore.getContactKeysWithTrust(jid.toBareJid().toString(), trust);
+ }
+
+ @Override
+ public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust, List<Jid> jids) {
+ Set<IdentityKey> keys = new HashSet<>();
+ for(Jid jid : jids) {
+ keys.addAll(axolotlStore.getContactKeysWithTrust(jid.toString(), trust));
+ }
+ return keys;
+ }
+
+ @Override
+ public long getNumTrustedKeys(Jid jid) {
+ return axolotlStore.getContactNumTrustedKeys(jid.toBareJid().toString());
+ }
+
+ @Override
+ public boolean anyTargetHasNoTrustedKeys(List<Jid> jids) {
+ for(Jid jid : jids) {
+ if (axolotlStore.getContactNumTrustedKeys(jid.toBareJid().toString()) == 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private AxolotlAddress getAddressForJid(Jid jid) {
+ return new AxolotlAddress(jid.toString(), 0);
+ }
+
+ private Set<XmppAxolotlSession> findOwnSessions() {
+ AxolotlAddress ownAddress = getAddressForJid(account.getJid().toBareJid());
+ return new HashSet<>(this.sessions.getAll(ownAddress).values());
+ }
+
+ private Set<XmppAxolotlSession> findSessionsForContact(Contact contact) {
+ AxolotlAddress contactAddress = getAddressForJid(contact.getJid());
+ return new HashSet<>(this.sessions.getAll(contactAddress).values());
+ }
+
+ private Set<XmppAxolotlSession> findSessionsForConversation(Conversation conversation) {
+ HashSet<XmppAxolotlSession> sessions = new HashSet<>();
+ for(Jid jid : conversation.getAcceptedCryptoTargets()) {
+ sessions.addAll(this.sessions.getAll(getAddressForJid(jid)).values());
+ }
+ 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) {
+ return sessions.hasAny(getAddressForJid(jid));
+ }
+
+ public boolean isPepBroken() {
+ return this.pepBroken;
+ }
+
+ public void resetBrokenness() {
+ this.pepBroken = false;
+ numPublishTriesOnEmptyPep = 0;
+ }
+
+ public void regenerateKeys(boolean wipeOther) {
+ axolotlStore.regenerate();
+ sessions.clear();
+ fetchStatusMap.clear();
+ publishBundlesIfNeeded(true, wipeOther);
+ }
+
+ public int getOwnDeviceId() {
+ return axolotlStore.getLocalRegistrationId();
+ }
+
+ public Set<Integer> getOwnDeviceIds() {
+ 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().toString(), 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) {
+ if (jid.toBareJid().equals(account.getJid().toBareJid())) {
+ if (!deviceIds.isEmpty()) {
+ Log.d(Config.LOGTAG, getLogprefix(account) + "Received non-empty own device list. Resetting publish attemps and pepBroken status.");
+ pepBroken = false;
+ numPublishTriesOnEmptyPep = 0;
+ }
+ if (deviceIds.contains(getOwnDeviceId())) {
+ deviceIds.remove(getOwnDeviceId());
+ } else {
+ publishOwnDeviceId(deviceIds);
+ }
+ for (Integer deviceId : deviceIds) {
+ AxolotlAddress ownDeviceAddress = new AxolotlAddress(jid.toBareJid().toString(), deviceId);
+ if (sessions.get(ownDeviceAddress) == null) {
+ buildSessionFromPEP(ownDeviceAddress);
+ }
+ }
+ }
+ Set<Integer> expiredDevices = new HashSet<>(axolotlStore.getSubDeviceSessions(jid.toBareJid().toString()));
+ 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);
+ mXmppConnectionService.keyStatusUpdated(null);
+ }
+
+ public void wipeOtherPepDevices() {
+ if (pepBroken) {
+ Log.d(Config.LOGTAG, getLogprefix(account) + "wipeOtherPepDevices called, but PEP is broken. Ignoring... ");
+ return;
+ }
+ Set<Integer> deviceIds = new HashSet<>();
+ deviceIds.add(getOwnDeviceId());
+ IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds);
+ Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Wiping all other devices from Pep:" + publish);
+ mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ // TODO: implement this!
+ }
+ });
+ }
+
+ public void purgeKey(final String fingerprint) {
+ axolotlStore.setFingerprintTrust(fingerprint.replaceAll("\\s", ""), XmppAxolotlSession.Trust.COMPROMISED);
+ }
+
+ public void publishOwnDeviceIdIfNeeded() {
+ if (pepBroken) {
+ Log.d(Config.LOGTAG, getLogprefix(account) + "publishOwnDeviceIdIfNeeded called, but PEP is broken. Ignoring... ");
+ return;
+ }
+ IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveDeviceIds(account.getJid().toBareJid());
+ mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE.TIMEOUT) {
+ Log.d(Config.LOGTAG, getLogprefix(account) + "Timeout received while retrieving own Device Ids.");
+ } else {
+ Element item = mXmppConnectionService.getIqParser().getItem(packet);
+ Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item);
+ if (!deviceIds.contains(getOwnDeviceId())) {
+ publishOwnDeviceId(deviceIds);
+ }
+ }
+ }
+ });
+ }
+
+ public void publishOwnDeviceId(Set<Integer> deviceIds) {
+ Set<Integer> deviceIdsCopy = new HashSet<>(deviceIds);
+ if (!deviceIdsCopy.contains(getOwnDeviceId())) {
+ Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Own device " + getOwnDeviceId() + " not in PEP devicelist.");
+ if (deviceIdsCopy.isEmpty()) {
+ if (numPublishTriesOnEmptyPep >= publishTriesThreshold) {
+ Log.w(Config.LOGTAG, getLogprefix(account) + "Own device publish attempt threshold exceeded, aborting...");
+ pepBroken = true;
+ 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,
+ final Set<PreKeyRecord> preKeyRecords,
+ final boolean announceAfter,
+ final boolean wipe) {
+ try {
+ IdentityKey axolotlPublicKey = axolotlStore.getIdentityKeyPair().getPublicKey();
+ PrivateKey x509PrivateKey = KeyChain.getPrivateKey(mXmppConnectionService, account.getPrivateKeyAlias());
+ X509Certificate[] chain = KeyChain.getCertificateChain(mXmppConnectionService, account.getPrivateKeyAlias());
+ Signature verifier = Signature.getInstance("sha256WithRSA");
+ verifier.initSign(x509PrivateKey, ConversationsPlusApplication.getSecureRandom());
+ verifier.update(axolotlPublicKey.serialize());
+ byte[] signature = verifier.sign();
+ IqPacket packet = mXmppConnectionService.getIqGenerator().publishVerification(signature, chain, getOwnDeviceId());
+ Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + ": publish verification for device "+getOwnDeviceId());
+ mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe);
+ }
+ });
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void publishBundlesIfNeeded(final boolean announce, final boolean wipe) {
+ if (pepBroken) {
+ Log.d(Config.LOGTAG, getLogprefix(account) + "publishBundlesIfNeeded called, but PEP is broken. Ignoring... ");
+ return;
+ }
+ IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(account.getJid().toBareJid(), getOwnDeviceId());
+ mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+
+ if (packet.getType() == IqPacket.TYPE.TIMEOUT) {
+ return; //ignore timeout. do nothing
+ }
+
+ if (packet.getType() == IqPacket.TYPE.ERROR) {
+ Element error = packet.findChild("error");
+ if (error == null || !error.hasChild("item-not-found")) {
+ pepBroken = true;
+ Log.w(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "request for device bundles came back with something other than item-not-found" + packet);
+ return;
+ }
+ }
+
+ PreKeyBundle bundle = mXmppConnectionService.getIqParser().bundle(packet);
+ Map<Integer, ECPublicKey> keys = mXmppConnectionService.getIqParser().preKeyPublics(packet);
+ boolean flush = false;
+ if (bundle == null) {
+ Log.w(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Received invalid bundle:" + packet);
+ bundle = new PreKeyBundle(-1, -1, -1, null, -1, null, null, null);
+ flush = true;
+ }
+ if (keys == null) {
+ Log.w(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Received invalid prekeys:" + packet);
+ }
+ try {
+ boolean changed = false;
+ // Validate IdentityKey
+ IdentityKeyPair identityKeyPair = axolotlStore.getIdentityKeyPair();
+ if (flush || !identityKeyPair.getPublicKey().equals(bundle.getIdentityKey())) {
+ Log.i(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Adding own IdentityKey " + identityKeyPair.getPublicKey() + " to PEP.");
+ changed = true;
+ }
+
+ // Validate signedPreKeyRecord + ID
+ SignedPreKeyRecord signedPreKeyRecord;
+ int numSignedPreKeys = axolotlStore.loadSignedPreKeys().size();
+ try {
+ signedPreKeyRecord = axolotlStore.loadSignedPreKey(bundle.getSignedPreKeyId());
+ if (flush
+ || !bundle.getSignedPreKey().equals(signedPreKeyRecord.getKeyPair().getPublicKey())
+ || !Arrays.equals(bundle.getSignedPreKeySignature(), signedPreKeyRecord.getSignature())) {
+ Log.i(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP.");
+ signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1);
+ axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord);
+ changed = true;
+ }
+ } catch (InvalidKeyIdException e) {
+ Log.i(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP.");
+ signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1);
+ axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord);
+ changed = true;
+ }
+
+ // Validate PreKeys
+ Set<PreKeyRecord> preKeyRecords = new HashSet<>();
+ if (keys != null) {
+ for (Integer id : keys.keySet()) {
+ try {
+ PreKeyRecord preKeyRecord = axolotlStore.loadPreKey(id);
+ if (preKeyRecord.getKeyPair().getPublicKey().equals(keys.get(id))) {
+ preKeyRecords.add(preKeyRecord);
+ }
+ } catch (InvalidKeyIdException ignored) {
+ }
+ }
+ }
+ int newKeys = NUM_KEYS_TO_PUBLISH - preKeyRecords.size();
+ if (newKeys > 0) {
+ List<PreKeyRecord> newRecords = KeyHelper.generatePreKeys(
+ axolotlStore.getCurrentPreKeyId() + 1, newKeys);
+ preKeyRecords.addAll(newRecords);
+ for (PreKeyRecord record : newRecords) {
+ axolotlStore.storePreKey(record.getId(), record);
+ }
+ changed = true;
+ Log.i(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Adding " + newKeys + " new preKeys to PEP.");
+ }
+
+
+ if (changed) {
+ if (account.getPrivateKeyAlias() != null && Config.X509_VERIFICATION) {
+ mXmppConnectionService.publishDisplayName(account);
+ publishDeviceVerificationAndBundle(signedPreKeyRecord, preKeyRecords, announce, wipe);
+ } else {
+ publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announce, wipe);
+ }
+ } else {
+ Log.d(Config.LOGTAG, getLogprefix(account) + "Bundle " + getOwnDeviceId() + " in PEP was current");
+ if (wipe) {
+ wipeOtherPepDevices();
+ } else if (announce) {
+ Log.d(Config.LOGTAG, getLogprefix(account) + "Announcing device " + getOwnDeviceId());
+ publishOwnDeviceIdIfNeeded();
+ }
+ }
+ } catch (InvalidKeyException e) {
+ Log.e(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage());
+ }
+ }
+ });
+ }
+
+ private void publishDeviceBundle(SignedPreKeyRecord signedPreKeyRecord,
+ Set<PreKeyRecord> preKeyRecords,
+ final boolean announceAfter,
+ final boolean wipe) {
+ IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundles(
+ signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(),
+ preKeyRecords, getOwnDeviceId());
+ Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + ": Bundle " + getOwnDeviceId() + " in PEP not current. Publishing: " + publish);
+ mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE.RESULT) {
+ Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Successfully published bundle. ");
+ if (wipe) {
+ wipeOtherPepDevices();
+ } else if (announceAfter) {
+ Log.d(Config.LOGTAG, getLogprefix(account) + "Announcing device " + getOwnDeviceId());
+ publishOwnDeviceIdIfNeeded();
+ }
+ } else if (packet.getType() == IqPacket.TYPE.ERROR) {
+ pepBroken = true;
+ Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing bundle: " + packet.findChild("error"));
+ }
+ }
+ });
+ }
+
+ @Override
+ public boolean isConversationAxolotlCapable(Conversation conversation) {
+ final List<Jid> jids = getCryptoTargets(conversation);
+ for(Jid jid : jids) {
+ if (!hasAny(jid) && (!deviceIds.containsKey(jid) || deviceIds.get(jid).isEmpty())) {
+ return false;
+ }
+ }
+ return jids.size() > 0;
+ }
+
+ @Override
+ public List<Jid> getCryptoTargets(Conversation conversation) {
+ final List<Jid> jids;
+ if (conversation.getMode() == Conversation.MODE_SINGLE) {
+ jids = Arrays.asList(conversation.getJid().toBareJid());
+ } else {
+ jids = conversation.getMucOptions().getMembers();
+ jids.remove(account.getJid().toBareJid());
+ }
+ return jids;
+ }
+
+ public XmppAxolotlSession.Trust getFingerprintTrust(String fingerprint) {
+ return axolotlStore.getFingerprintTrust(fingerprint);
+ }
+
+ public X509Certificate getFingerprintCertificate(String fingerprint) {
+ return axolotlStore.getFingerprintCertificate(fingerprint);
+ }
+
+ public void setFingerprintTrust(String fingerprint, XmppAxolotlSession.Trust trust) {
+ axolotlStore.setFingerprintTrust(fingerprint, trust);
+ }
+
+ private void verifySessionWithPEP(final XmppAxolotlSession session) {
+ Log.d(Config.LOGTAG, "trying to verify fresh session (" + session.getRemoteAddress().getName() + ") with pep");
+ final AxolotlAddress address = session.getRemoteAddress();
+ final IdentityKey identityKey = session.getIdentityKey();
+ try {
+ IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveVerificationForDevice(Jid.fromString(address.getName()), address.getDeviceId());
+ mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ Pair<X509Certificate[],byte[]> verification = mXmppConnectionService.getIqParser().verification(packet);
+ if (verification != null) {
+ try {
+ Signature verifier = Signature.getInstance("sha256WithRSA");
+ verifier.initVerify(verification.first[0]);
+ verifier.update(identityKey.serialize());
+ if (verifier.verify(verification.second)) {
+ try {
+ ConversationsPlusApplication.getMemorizingTrustManager().getNonInteractive().checkClientTrusted(verification.first, "RSA");
+ String fingerprint = session.getFingerprint();
+ Log.d(Config.LOGTAG, "verified session with x.509 signature. fingerprint was: "+fingerprint);
+ setFingerprintTrust(fingerprint, XmppAxolotlSession.Trust.TRUSTED_X509);
+ axolotlStore.setFingerprintCertificate(fingerprint, verification.first[0]);
+ fetchStatusMap.put(address, FetchStatus.SUCCESS_VERIFIED);
+ Bundle information = CryptoHelper.extractCertificateInformation(verification.first[0]);
+ try {
+ final String cn = information.getString("subject_cn");
+ final Jid jid = Jid.fromString(address.getName());
+ Log.d(Config.LOGTAG,"setting common name for "+jid+" to "+cn);
+ account.getRoster().getContact(jid).setCommonName(cn);
+ } catch (final InvalidJidException ignored) {
+ //ignored
+ }
+ finishBuildingSessionsFromPEP(address);
+ return;
+ } catch (Exception e) {
+ Log.d(Config.LOGTAG,"could not verify certificate");
+ }
+ }
+ } catch (Exception e) {
+ Log.d(Config.LOGTAG, "error during verification " + e.getMessage());
+ }
+ } else {
+ Log.d(Config.LOGTAG,"no verification found");
+ }
+ fetchStatusMap.put(address, FetchStatus.SUCCESS);
+ finishBuildingSessionsFromPEP(address);
+ }
+ });
+ } catch (InvalidJidException e) {
+ fetchStatusMap.put(address, FetchStatus.SUCCESS);
+ finishBuildingSessionsFromPEP(address);
+ }
+ }
+
+ private void finishBuildingSessionsFromPEP(final AxolotlAddress address) {
+ AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toString(), 0);
+ if (!fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING)
+ && !fetchStatusMap.getAll(address).containsValue(FetchStatus.PENDING)) {
+ FetchStatus report = null;
+ if (fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.SUCCESS_VERIFIED)
+ | fetchStatusMap.getAll(address).containsValue(FetchStatus.SUCCESS_VERIFIED)) {
+ report = FetchStatus.SUCCESS_VERIFIED;
+ } else if (fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.ERROR)
+ || fetchStatusMap.getAll(address).containsValue(FetchStatus.ERROR)) {
+ report = FetchStatus.ERROR;
+ }
+ mXmppConnectionService.keyStatusUpdated(report);
+ }
+ }
+
+ private void buildSessionFromPEP(final AxolotlAddress address) {
+ Log.i(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Building new sesstion for " + address.toString());
+ if (address.getDeviceId() == getOwnDeviceId()) {
+ throw new AssertionError("We should NEVER build a session with ourselves. What happened here?!");
+ }
+
+ try {
+ IqPacket bundlesPacket = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(
+ Jid.fromString(address.getName()), address.getDeviceId());
+ Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Retrieving bundle: " + bundlesPacket);
+ mXmppConnectionService.sendIqPacket(account, bundlesPacket, new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE.TIMEOUT) {
+ fetchStatusMap.put(address, FetchStatus.TIMEOUT);
+ } else if (packet.getType() == IqPacket.TYPE.RESULT) {
+ Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Received preKey IQ packet, processing...");
+ final IqParser parser = mXmppConnectionService.getIqParser();
+ final List<PreKeyBundle> preKeyBundleList = parser.preKeys(packet);
+ final PreKeyBundle bundle = parser.bundle(packet);
+ if (preKeyBundleList.isEmpty() || bundle == null) {
+ Log.e(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "preKey IQ packet invalid: " + packet);
+ fetchStatusMap.put(address, FetchStatus.ERROR);
+ finishBuildingSessionsFromPEP(address);
+ return;
+ }
+ Random random = new Random();
+ final PreKeyBundle preKey = preKeyBundleList.get(random.nextInt(preKeyBundleList.size()));
+ if (preKey == null) {
+ //should never happen
+ fetchStatusMap.put(address, FetchStatus.ERROR);
+ finishBuildingSessionsFromPEP(address);
+ return;
+ }
+
+ final PreKeyBundle preKeyBundle = new PreKeyBundle(0, address.getDeviceId(),
+ preKey.getPreKeyId(), preKey.getPreKey(),
+ bundle.getSignedPreKeyId(), bundle.getSignedPreKey(),
+ bundle.getSignedPreKeySignature(), bundle.getIdentityKey());
+
+ try {
+ SessionBuilder builder = new SessionBuilder(axolotlStore, address);
+ builder.process(preKeyBundle);
+ XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, bundle.getIdentityKey());
+ sessions.put(address, session);
+ if (Config.X509_VERIFICATION) {
+ verifySessionWithPEP(session);
+ } else {
+ fetchStatusMap.put(address, FetchStatus.SUCCESS);
+ finishBuildingSessionsFromPEP(address);
+ }
+ } catch (UntrustedIdentityException | InvalidKeyException e) {
+ Log.e(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Error building session for " + address + ": "
+ + e.getClass().getName() + ", " + e.getMessage());
+ fetchStatusMap.put(address, FetchStatus.ERROR);
+ finishBuildingSessionsFromPEP(address);
+ }
+ } else {
+ fetchStatusMap.put(address, FetchStatus.ERROR);
+ Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while building session:" + packet.findChild("error"));
+ finishBuildingSessionsFromPEP(address);
+ }
+ }
+ });
+ } catch (InvalidJidException e) {
+ Log.e(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Got address with invalid jid: " + address.getName());
+ }
+ }
+
+ public Set<AxolotlAddress> findDevicesWithoutSession(final Conversation conversation) {
+ Set<AxolotlAddress> addresses = new HashSet<>();
+ for(Jid jid : getCryptoTargets(conversation)) {
+ Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Finding devices without session for " + jid);
+ if (deviceIds.get(jid) != null) {
+ for (Integer foreignId : this.deviceIds.get(jid)) {
+ AxolotlAddress address = new AxolotlAddress(jid.toString(), foreignId);
+ if (sessions.get(address) == null) {
+ IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey();
+ if (identityKey != null) {
+ Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache...");
+ XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey);
+ sessions.put(address, session);
+ } else {
+ Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Found device " + jid + ":" + foreignId);
+ if (fetchStatusMap.get(address) != FetchStatus.ERROR) {
+ addresses.add(address);
+ } else {
+ Log.d(Config.LOGTAG, getLogprefix(account) + "skipping over " + address + " because it's broken");
+ }
+ }
+ }
+ }
+ } else {
+ Log.w(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Have no target devices in PEP!");
+ }
+ }
+ if (deviceIds.get(account.getJid().toBareJid()) != null) {
+ for (Integer ownId : this.deviceIds.get(account.getJid().toBareJid())) {
+ AxolotlAddress address = new AxolotlAddress(account.getJid().toBareJid().toString(), ownId);
+ if (sessions.get(address) == null) {
+ IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey();
+ if (identityKey != null) {
+ Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache...");
+ XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey);
+ sessions.put(address, session);
+ } else {
+ Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Found device " + account.getJid().toBareJid() + ":" + ownId);
+ if (fetchStatusMap.get(address) != FetchStatus.ERROR) {
+ addresses.add(address);
+ } else {
+ Log.d(Config.LOGTAG,getLogprefix(account)+"skipping over "+address+" because it's broken");
+ }
+ }
+ }
+ }
+ }
+
+ return addresses;
+ }
+
+ public boolean createSessionsIfNeeded(final Conversation conversation) {
+ Log.i(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Creating axolotl sessions if needed...");
+ boolean newSessions = false;
+ Set<AxolotlAddress> addresses = findDevicesWithoutSession(conversation);
+ for (AxolotlAddress address : addresses) {
+ Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Processing device: " + address.toString());
+ FetchStatus status = fetchStatusMap.get(address);
+ if (status == null || status == FetchStatus.TIMEOUT) {
+ fetchStatusMap.put(address, FetchStatus.PENDING);
+ this.buildSessionFromPEP(address);
+ newSessions = true;
+ } else if (status == FetchStatus.PENDING) {
+ newSessions = true;
+ } else {
+ Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Already fetching bundle for " + address.toString());
+ }
+ }
+
+ return newSessions;
+ }
+
+ public boolean trustedSessionVerified(final Conversation conversation) {
+ Set<XmppAxolotlSession> sessions = findSessionsForConversation(conversation);
+ sessions.addAll(findOwnSessions());
+ boolean verified = false;
+ for(XmppAxolotlSession session : sessions) {
+ if (session.getTrust().trusted()) {
+ if (session.getTrust() == XmppAxolotlSession.Trust.TRUSTED_X509) {
+ verified = true;
+ } else {
+ return false;
+ }
+ }
+ }
+ return verified;
+ }
+
+ @Override
+ public boolean hasPendingKeyFetches(Account account, List<Jid> jids) {
+ AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toString(), 0);
+ if (fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING)) {
+ return true;
+ }
+ for(Jid jid : jids) {
+ AxolotlAddress foreignAddress = new AxolotlAddress(jid.toBareJid().toString(), 0);
+ if (fetchStatusMap.getAll(foreignAddress).containsValue(FetchStatus.PENDING)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Nullable
+ private XmppAxolotlMessage buildHeader(Conversation conversation) {
+ final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(
+ account.getJid().toBareJid(), getOwnDeviceId());
+
+ Set<XmppAxolotlSession> remoteSessions = findSessionsForConversation(conversation);
+ Set<XmppAxolotlSession> ownSessions = findOwnSessions();
+ if (remoteSessions.isEmpty()) {
+ return null;
+ }
+ Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Building axolotl foreign keyElements...");
+ for (XmppAxolotlSession session : remoteSessions) {
+ Log.v(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + session.getRemoteAddress().toString());
+ axolotlMessage.addDevice(session);
+ }
+ Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Building axolotl own keyElements...");
+ for (XmppAxolotlSession session : ownSessions) {
+ Log.v(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + session.getRemoteAddress().toString());
+ axolotlMessage.addDevice(session);
+ }
+
+ return axolotlMessage;
+ }
+
+ @Nullable
+ public XmppAxolotlMessage encrypt(Message message) {
+ XmppAxolotlMessage axolotlMessage = buildHeader(message.getConversation());
+
+ if (axolotlMessage != null) {
+ final String content = message.getBody();
+ try {
+ axolotlMessage.encrypt(content);
+ } catch (CryptoFailedException e) {
+ Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to encrypt message: " + e.getMessage());
+ return null;
+ }
+ }
+
+ return axolotlMessage;
+ }
+
+ public void preparePayloadMessage(final Message message, final boolean delay) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ XmppAxolotlMessage axolotlMessage = encrypt(message);
+ if (axolotlMessage == null) {
+ MessageUtil.markMessage(message, Message.STATUS_SEND_FAILED);
+ //mXmppConnectionService.updateConversationUi();
+ } else {
+ Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Generated message, caching: " + message.getUuid());
+ messageCache.put(message.getUuid(), axolotlMessage);
+ mXmppConnectionService.resendMessage(message, delay);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void prepareKeyTransportMessage(final Conversation conversation, final OnMessageCreatedCallback onMessageCreatedCallback) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ XmppAxolotlMessage axolotlMessage = buildHeader(conversation);
+ onMessageCreatedCallback.run(axolotlMessage);
+ }
+ });
+ }
+
+ public XmppAxolotlMessage fetchAxolotlMessageFromCache(Message message) {
+ XmppAxolotlMessage axolotlMessage = messageCache.get(message.getUuid());
+ if (axolotlMessage != null) {
+ Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Cache hit: " + message.getUuid());
+ messageCache.remove(message.getUuid());
+ } else {
+ Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Cache miss: " + message.getUuid());
+ }
+ return axolotlMessage;
+ }
+
+ private XmppAxolotlSession recreateUncachedSession(AxolotlAddress address) {
+ IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey();
+ return (identityKey != null)
+ ? new XmppAxolotlSession(account, axolotlStore, address, identityKey)
+ : null;
+ }
+
+ private XmppAxolotlSession getReceivingSession(XmppAxolotlMessage message) {
+ AxolotlAddress senderAddress = new AxolotlAddress(message.getFrom().toString(),
+ message.getSenderDeviceId());
+ XmppAxolotlSession session = sessions.get(senderAddress);
+ if (session == null) {
+ Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Account: " + account.getJid() + " No axolotl session found while parsing received message " + message);
+ session = recreateUncachedSession(senderAddress);
+ if (session == null) {
+ session = new XmppAxolotlSession(account, axolotlStore, senderAddress);
+ }
+ }
+ return session;
+ }
+
+ public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceivingPayloadMessage(XmppAxolotlMessage message) {
+ XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = null;
+
+ XmppAxolotlSession session = getReceivingSession(message);
+ try {
+ plaintextMessage = message.decrypt(session, getOwnDeviceId());
+ Integer preKeyId = session.getPreKeyId();
+ if (preKeyId != null) {
+ publishBundlesIfNeeded(false, false);
+ session.resetPreKeyId();
+ }
+ } catch (CryptoFailedException e) {
+ Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to decrypt message: " + e.getMessage());
+ }
+
+ if (session.isFresh() && plaintextMessage != null) {
+ putFreshSession(session);
+ }
+
+ return plaintextMessage;
+ }
+
+ public XmppAxolotlMessage.XmppAxolotlKeyTransportMessage processReceivingKeyTransportMessage(XmppAxolotlMessage message) {
+ XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage;
+
+ XmppAxolotlSession session = getReceivingSession(message);
+ keyTransportMessage = message.getParameters(session, getOwnDeviceId());
+
+ if (session.isFresh() && keyTransportMessage != null) {
+ putFreshSession(session);
+ }
+
+ return keyTransportMessage;
+ }
+
+ private void putFreshSession(XmppAxolotlSession session) {
+ Log.d(Config.LOGTAG,"put fresh session");
+ sessions.put(session);
+ if (Config.X509_VERIFICATION) {
+ if (session.getIdentityKey() != null) {
+ verifySessionWithPEP(session);
+ } else {
+ Log.e(Config.LOGTAG,account.getJid().toBareJid()+": identity key was empty after reloading for x509 verification");
+ }
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlServiceStub.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlServiceStub.java
new file mode 100644
index 00000000..0152d02a
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlServiceStub.java
@@ -0,0 +1,212 @@
+package eu.siacs.conversations.crypto.axolotl;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import org.whispersystems.libaxolotl.AxolotlAddress;
+import org.whispersystems.libaxolotl.IdentityKey;
+import org.whispersystems.libaxolotl.state.PreKeyRecord;
+import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
+
+import java.security.cert.X509Certificate;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+/**
+ * Axolotl Service Stub implementation to avoid axolotl usage.
+ */
+public class AxolotlServiceStub implements AxolotlService {
+
+ @Override
+ public String getOwnFingerprint() {
+ return null;
+ }
+
+ @Override
+ public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust) {
+ return Collections.emptySet();
+ }
+
+ @Override
+ public Set<String> getFingerprintsForOwnSessions() {
+ return Collections.emptySet();
+ }
+
+ @Override
+ public Set<String> getFingerprintsForContact(Contact contact) {
+ return Collections.emptySet();
+ }
+
+ @Override
+ public boolean isPepBroken() {
+ return true;
+ }
+
+ @Override
+ public void regenerateKeys(boolean wipeOther) {
+
+ }
+
+ @Override
+ public int getOwnDeviceId() {
+ return 0;
+ }
+
+ @Override
+ public Set<Integer> getOwnDeviceIds() {
+ return Collections.emptySet();
+ }
+
+ @Override
+ public void registerDevices(Jid jid, @NonNull Set<Integer> deviceIds) {
+
+ }
+
+ @Override
+ public void wipeOtherPepDevices() {
+
+ }
+
+ @Override
+ public void purgeKey(String fingerprint) {
+
+ }
+
+ @Override
+ public void publishOwnDeviceIdIfNeeded() {
+
+ }
+
+ @Override
+ public void publishOwnDeviceId(Set<Integer> deviceIds) {
+
+ }
+
+ @Override
+ public void publishDeviceVerificationAndBundle(SignedPreKeyRecord signedPreKeyRecord, Set<PreKeyRecord> preKeyRecords, boolean announceAfter, boolean wipe) {
+
+ }
+
+ @Override
+ public void publishBundlesIfNeeded(boolean announce, boolean wipe) {
+
+ }
+
+ @Override
+ public XmppAxolotlSession.Trust getFingerprintTrust(String fingerprint) {
+ return XmppAxolotlSession.Trust.TRUSTED;
+ }
+
+ @Override
+ public X509Certificate getFingerprintCertificate(String fingerprint) {
+ return null;
+ }
+
+ @Override
+ public void setFingerprintTrust(String fingerprint, XmppAxolotlSession.Trust trust) {
+
+ }
+
+ @Override
+ public Set<AxolotlAddress> findDevicesWithoutSession(Conversation conversation) {
+ return Collections.emptySet();
+ }
+
+ @Override
+ public boolean createSessionsIfNeeded(Conversation conversation) {
+ return false;
+ }
+
+ @Override
+ public boolean trustedSessionVerified(Conversation conversation) {
+ return false;
+ }
+
+ @Nullable
+ @Override
+ public XmppAxolotlMessage encrypt(Message message) {
+ return null;
+ }
+
+ @Override
+ public void preparePayloadMessage(Message message, boolean delay) {
+
+ }
+
+ @Override
+ public XmppAxolotlMessage fetchAxolotlMessageFromCache(Message message) {
+ return null;
+ }
+
+ @Override
+ public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceivingPayloadMessage(XmppAxolotlMessage message) {
+ return null;
+ }
+
+ @Override
+ public XmppAxolotlMessage.XmppAxolotlKeyTransportMessage processReceivingKeyTransportMessage(XmppAxolotlMessage message) {
+ return null;
+ }
+
+ @Override
+ public boolean fetchMapHasErrors(List<Jid> jids) {
+ return false;
+ }
+
+ @Override
+ public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust, Jid jid) {
+ return Collections.emptySet();
+ }
+
+ @Override
+ public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust, List<Jid> jids) {
+ return Collections.emptySet();
+ }
+
+ @Override
+ public long getNumTrustedKeys(Jid jid) {
+ return 0;
+ }
+
+ @Override
+ public boolean anyTargetHasNoTrustedKeys(List<Jid> jids) {
+ return false;
+ }
+
+ @Override
+ public boolean isConversationAxolotlCapable(Conversation conversation) {
+ return false;
+ }
+
+ @Override
+ public List<Jid> getCryptoTargets(Conversation conversation) {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public boolean hasPendingKeyFetches(Account account, List<Jid> jids) {
+ return false;
+ }
+
+ @Override
+ public void prepareKeyTransportMessage(Conversation conversation, OnMessageCreatedCallback onMessageCreatedCallback) {
+
+ }
+
+ @Override
+ public void onAdvancedStreamFeaturesAvailable(Account account) {
+
+ }
+
+ @Override
+ public void resetBrokenness() {
+
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/CryptoFailedException.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/CryptoFailedException.java
new file mode 100644
index 00000000..5796ef30
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/CryptoFailedException.java
@@ -0,0 +1,7 @@
+package eu.siacs.conversations.crypto.axolotl;
+
+public class CryptoFailedException extends Exception {
+ public CryptoFailedException(Exception e){
+ super(e);
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/NoSessionsCreatedException.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/NoSessionsCreatedException.java
new file mode 100644
index 00000000..663b42b5
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/NoSessionsCreatedException.java
@@ -0,0 +1,4 @@
+package eu.siacs.conversations.crypto.axolotl;
+
+public class NoSessionsCreatedException extends Throwable{
+}
diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/OnMessageCreatedCallback.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/OnMessageCreatedCallback.java
new file mode 100644
index 00000000..3d40a408
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/OnMessageCreatedCallback.java
@@ -0,0 +1,5 @@
+package eu.siacs.conversations.crypto.axolotl;
+
+public interface OnMessageCreatedCallback {
+ void run(XmppAxolotlMessage message);
+}
diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/SQLiteAxolotlStore.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/SQLiteAxolotlStore.java
new file mode 100644
index 00000000..c634d877
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/SQLiteAxolotlStore.java
@@ -0,0 +1,429 @@
+package eu.siacs.conversations.crypto.axolotl;
+
+import android.util.Log;
+import android.util.LruCache;
+
+import org.whispersystems.libaxolotl.AxolotlAddress;
+import org.whispersystems.libaxolotl.IdentityKey;
+import org.whispersystems.libaxolotl.IdentityKeyPair;
+import org.whispersystems.libaxolotl.InvalidKeyIdException;
+import org.whispersystems.libaxolotl.ecc.Curve;
+import org.whispersystems.libaxolotl.ecc.ECKeyPair;
+import org.whispersystems.libaxolotl.state.AxolotlStore;
+import org.whispersystems.libaxolotl.state.PreKeyRecord;
+import org.whispersystems.libaxolotl.state.SessionRecord;
+import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
+import org.whispersystems.libaxolotl.util.KeyHelper;
+
+import java.security.cert.X509Certificate;
+import java.util.List;
+import java.util.Set;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.services.XmppConnectionService;
+
+public class SQLiteAxolotlStore implements AxolotlStore {
+
+ public static final String PREKEY_TABLENAME = "prekeys";
+ public static final String SIGNED_PREKEY_TABLENAME = "signed_prekeys";
+ public static final String SESSION_TABLENAME = "sessions";
+ public static final String IDENTITIES_TABLENAME = "identities";
+ public static final String ACCOUNT = "account";
+ public static final String DEVICE_ID = "device_id";
+ public static final String ID = "id";
+ public static final String KEY = "key";
+ public static final String FINGERPRINT = "fingerprint";
+ public static final String NAME = "name";
+ public static final String TRUSTED = "trusted";
+ public static final String OWN = "ownkey";
+ public static final String CERTIFICATE = "certificate";
+
+ public static final String JSONKEY_REGISTRATION_ID = "axolotl_reg_id";
+ public static final String JSONKEY_CURRENT_PREKEY_ID = "axolotl_cur_prekey_id";
+
+ private static final int NUM_TRUSTS_TO_CACHE = 100;
+
+ private final Account account;
+ private final XmppConnectionService mXmppConnectionService;
+
+ private IdentityKeyPair identityKeyPair;
+ private int localRegistrationId;
+ private int currentPreKeyId = 0;
+
+ private final LruCache<String, XmppAxolotlSession.Trust> trustCache =
+ new LruCache<String, XmppAxolotlSession.Trust>(NUM_TRUSTS_TO_CACHE) {
+ @Override
+ protected XmppAxolotlSession.Trust create(String fingerprint) {
+ return mXmppConnectionService.databaseBackend.isIdentityKeyTrusted(account, fingerprint);
+ }
+ };
+
+ private static IdentityKeyPair generateIdentityKeyPair() {
+ Log.i(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + "Generating axolotl IdentityKeyPair...");
+ ECKeyPair identityKeyPairKeys = Curve.generateKeyPair();
+ return new IdentityKeyPair(new IdentityKey(identityKeyPairKeys.getPublicKey()),
+ identityKeyPairKeys.getPrivateKey());
+ }
+
+ private static int generateRegistrationId() {
+ Log.i(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + "Generating axolotl registration ID...");
+ return KeyHelper.generateRegistrationId(true);
+ }
+
+ public SQLiteAxolotlStore(Account account, XmppConnectionService service) {
+ this.account = account;
+ this.mXmppConnectionService = service;
+ this.localRegistrationId = loadRegistrationId();
+ this.currentPreKeyId = loadCurrentPreKeyId();
+ for (SignedPreKeyRecord record : loadSignedPreKeys()) {
+ Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Got Axolotl signed prekey record:" + record.getId());
+ }
+ }
+
+ public int getCurrentPreKeyId() {
+ return currentPreKeyId;
+ }
+
+ // --------------------------------------
+ // IdentityKeyStore
+ // --------------------------------------
+
+ private IdentityKeyPair loadIdentityKeyPair() {
+ IdentityKeyPair ownKey = mXmppConnectionService.databaseBackend.loadOwnIdentityKeyPair(account);
+
+ if (ownKey != null) {
+ return ownKey;
+ } else {
+ Log.i(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Could not retrieve own IdentityKeyPair");
+ ownKey = generateIdentityKeyPair();
+ mXmppConnectionService.databaseBackend.storeOwnIdentityKeyPair(account, ownKey);
+ }
+ return ownKey;
+ }
+
+ private int loadRegistrationId() {
+ return loadRegistrationId(false);
+ }
+
+ private int loadRegistrationId(boolean regenerate) {
+ String regIdString = this.account.getKey(JSONKEY_REGISTRATION_ID);
+ int reg_id;
+ if (!regenerate && regIdString != null) {
+ reg_id = Integer.valueOf(regIdString);
+ } else {
+ Log.i(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Could not retrieve axolotl registration id for account " + account.getJid());
+ reg_id = generateRegistrationId();
+ boolean success = this.account.setKey(JSONKEY_REGISTRATION_ID, Integer.toString(reg_id));
+ if (success) {
+ mXmppConnectionService.databaseBackend.updateAccount(account);
+ } else {
+ Log.e(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Failed to write new key to the database!");
+ }
+ }
+ return reg_id;
+ }
+
+ private int loadCurrentPreKeyId() {
+ String regIdString = this.account.getKey(JSONKEY_CURRENT_PREKEY_ID);
+ int reg_id;
+ if (regIdString != null) {
+ reg_id = Integer.valueOf(regIdString);
+ } else {
+ Log.w(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Could not retrieve current prekey id for account " + account.getJid());
+ reg_id = 0;
+ }
+ return reg_id;
+ }
+
+ public void regenerate() {
+ mXmppConnectionService.databaseBackend.wipeAxolotlDb(account);
+ trustCache.evictAll();
+ account.setKey(JSONKEY_CURRENT_PREKEY_ID, Integer.toString(0));
+ identityKeyPair = loadIdentityKeyPair();
+ localRegistrationId = loadRegistrationId(true);
+ currentPreKeyId = 0;
+ mXmppConnectionService.updateAccountUi();
+ }
+
+ /**
+ * Get the local client's identity key pair.
+ *
+ * @return The local client's persistent identity key pair.
+ */
+ @Override
+ public IdentityKeyPair getIdentityKeyPair() {
+ if (identityKeyPair == null) {
+ identityKeyPair = loadIdentityKeyPair();
+ }
+ return identityKeyPair;
+ }
+
+ /**
+ * Return the local client's registration ID.
+ * <p/>
+ * Clients should maintain a registration ID, a random number
+ * between 1 and 16380 that's generated once at install time.
+ *
+ * @return the local client's registration ID.
+ */
+ @Override
+ public int getLocalRegistrationId() {
+ return localRegistrationId;
+ }
+
+ /**
+ * Save a remote client's identity key
+ * <p/>
+ * Store a remote client's identity key as trusted.
+ *
+ * @param name The name of the remote client.
+ * @param identityKey The remote client's identity key.
+ */
+ @Override
+ public void saveIdentity(String name, IdentityKey identityKey) {
+ if (!mXmppConnectionService.databaseBackend.loadIdentityKeys(account, name).contains(identityKey)) {
+ mXmppConnectionService.databaseBackend.storeIdentityKey(account, name, identityKey);
+ }
+ }
+
+ /**
+ * Verify a remote client's identity key.
+ * <p/>
+ * Determine whether a remote client's identity is trusted. Convention is
+ * that the TextSecure protocol is 'trust on first use.' This means that
+ * an identity key is considered 'trusted' if there is no entry for the recipient
+ * in the local store, or if it matches the saved key for a recipient in the local
+ * store. Only if it mismatches an entry in the local store is it considered
+ * 'untrusted.'
+ *
+ * @param name The name of the remote client.
+ * @param identityKey The identity key to verify.
+ * @return true if trusted, false if untrusted.
+ */
+ @Override
+ public boolean isTrustedIdentity(String name, IdentityKey identityKey) {
+ return true;
+ }
+
+ public XmppAxolotlSession.Trust getFingerprintTrust(String fingerprint) {
+ return (fingerprint == null)? null : trustCache.get(fingerprint);
+ }
+
+ public void setFingerprintTrust(String fingerprint, XmppAxolotlSession.Trust trust) {
+ mXmppConnectionService.databaseBackend.setIdentityKeyTrust(account, fingerprint, trust);
+ trustCache.remove(fingerprint);
+ }
+
+ public void setFingerprintCertificate(String fingerprint, X509Certificate x509Certificate) {
+ mXmppConnectionService.databaseBackend.setIdentityKeyCertificate(account, fingerprint, x509Certificate);
+ }
+
+ public X509Certificate getFingerprintCertificate(String fingerprint) {
+ return mXmppConnectionService.databaseBackend.getIdentityKeyCertifcate(account, fingerprint);
+ }
+
+ public Set<IdentityKey> getContactKeysWithTrust(String bareJid, XmppAxolotlSession.Trust trust) {
+ return mXmppConnectionService.databaseBackend.loadIdentityKeys(account, bareJid, trust);
+ }
+
+ public long getContactNumTrustedKeys(String bareJid) {
+ return mXmppConnectionService.databaseBackend.numTrustedKeys(account, bareJid);
+ }
+
+ // --------------------------------------
+ // SessionStore
+ // --------------------------------------
+
+ /**
+ * Returns a copy of the {@link SessionRecord} corresponding to the recipientId + deviceId tuple,
+ * or a new SessionRecord if one does not currently exist.
+ * <p/>
+ * It is important that implementations return a copy of the current durable information. The
+ * returned SessionRecord may be modified, but those changes should not have an effect on the
+ * durable session state (what is returned by subsequent calls to this method) without the
+ * store method being called here first.
+ *
+ * @param address The name and device ID of the remote client.
+ * @return a copy of the SessionRecord corresponding to the recipientId + deviceId tuple, or
+ * a new SessionRecord if one does not currently exist.
+ */
+ @Override
+ public SessionRecord loadSession(AxolotlAddress address) {
+ SessionRecord session = mXmppConnectionService.databaseBackend.loadSession(this.account, address);
+ return (session != null) ? session : new SessionRecord();
+ }
+
+ /**
+ * Returns all known devices with active sessions for a recipient
+ *
+ * @param name the name of the client.
+ * @return all known sub-devices with active sessions.
+ */
+ @Override
+ public List<Integer> getSubDeviceSessions(String name) {
+ return mXmppConnectionService.databaseBackend.getSubDeviceSessions(account,
+ new AxolotlAddress(name, 0));
+ }
+
+ /**
+ * Commit to storage the {@link SessionRecord} for a given recipientId + deviceId tuple.
+ *
+ * @param address the address of the remote client.
+ * @param record the current SessionRecord for the remote client.
+ */
+ @Override
+ public void storeSession(AxolotlAddress address, SessionRecord record) {
+ mXmppConnectionService.databaseBackend.storeSession(account, address, record);
+ }
+
+ /**
+ * Determine whether there is a committed {@link SessionRecord} for a recipientId + deviceId tuple.
+ *
+ * @param address the address of the remote client.
+ * @return true if a {@link SessionRecord} exists, false otherwise.
+ */
+ @Override
+ public boolean containsSession(AxolotlAddress address) {
+ return mXmppConnectionService.databaseBackend.containsSession(account, address);
+ }
+
+ /**
+ * Remove a {@link SessionRecord} for a recipientId + deviceId tuple.
+ *
+ * @param address the address of the remote client.
+ */
+ @Override
+ public void deleteSession(AxolotlAddress address) {
+ mXmppConnectionService.databaseBackend.deleteSession(account, address);
+ }
+
+ /**
+ * Remove the {@link SessionRecord}s corresponding to all devices of a recipientId.
+ *
+ * @param name the name of the remote client.
+ */
+ @Override
+ public void deleteAllSessions(String name) {
+ AxolotlAddress address = new AxolotlAddress(name, 0);
+ mXmppConnectionService.databaseBackend.deleteAllSessions(account,
+ address);
+ }
+
+ // --------------------------------------
+ // PreKeyStore
+ // --------------------------------------
+
+ /**
+ * Load a local PreKeyRecord.
+ *
+ * @param preKeyId the ID of the local PreKeyRecord.
+ * @return the corresponding PreKeyRecord.
+ * @throws InvalidKeyIdException when there is no corresponding PreKeyRecord.
+ */
+ @Override
+ public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException {
+ PreKeyRecord record = mXmppConnectionService.databaseBackend.loadPreKey(account, preKeyId);
+ if (record == null) {
+ throw new InvalidKeyIdException("No such PreKeyRecord: " + preKeyId);
+ }
+ return record;
+ }
+
+ /**
+ * Store a local PreKeyRecord.
+ *
+ * @param preKeyId the ID of the PreKeyRecord to store.
+ * @param record the PreKeyRecord.
+ */
+ @Override
+ public void storePreKey(int preKeyId, PreKeyRecord record) {
+ mXmppConnectionService.databaseBackend.storePreKey(account, record);
+ currentPreKeyId = preKeyId;
+ boolean success = this.account.setKey(JSONKEY_CURRENT_PREKEY_ID, Integer.toString(preKeyId));
+ if (success) {
+ mXmppConnectionService.databaseBackend.updateAccount(account);
+ } else {
+ Log.e(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Failed to write new prekey id to the database!");
+ }
+ }
+
+ /**
+ * @param preKeyId A PreKeyRecord ID.
+ * @return true if the store has a record for the preKeyId, otherwise false.
+ */
+ @Override
+ public boolean containsPreKey(int preKeyId) {
+ return mXmppConnectionService.databaseBackend.containsPreKey(account, preKeyId);
+ }
+
+ /**
+ * Delete a PreKeyRecord from local storage.
+ *
+ * @param preKeyId The ID of the PreKeyRecord to remove.
+ */
+ @Override
+ public void removePreKey(int preKeyId) {
+ mXmppConnectionService.databaseBackend.deletePreKey(account, preKeyId);
+ }
+
+ // --------------------------------------
+ // SignedPreKeyStore
+ // --------------------------------------
+
+ /**
+ * Load a local SignedPreKeyRecord.
+ *
+ * @param signedPreKeyId the ID of the local SignedPreKeyRecord.
+ * @return the corresponding SignedPreKeyRecord.
+ * @throws InvalidKeyIdException when there is no corresponding SignedPreKeyRecord.
+ */
+ @Override
+ public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException {
+ SignedPreKeyRecord record = mXmppConnectionService.databaseBackend.loadSignedPreKey(account, signedPreKeyId);
+ if (record == null) {
+ throw new InvalidKeyIdException("No such SignedPreKeyRecord: " + signedPreKeyId);
+ }
+ return record;
+ }
+
+ /**
+ * Load all local SignedPreKeyRecords.
+ *
+ * @return All stored SignedPreKeyRecords.
+ */
+ @Override
+ public List<SignedPreKeyRecord> loadSignedPreKeys() {
+ return mXmppConnectionService.databaseBackend.loadSignedPreKeys(account);
+ }
+
+ /**
+ * Store a local SignedPreKeyRecord.
+ *
+ * @param signedPreKeyId the ID of the SignedPreKeyRecord to store.
+ * @param record the SignedPreKeyRecord.
+ */
+ @Override
+ public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) {
+ mXmppConnectionService.databaseBackend.storeSignedPreKey(account, record);
+ }
+
+ /**
+ * @param signedPreKeyId A SignedPreKeyRecord ID.
+ * @return true if the store has a record for the signedPreKeyId, otherwise false.
+ */
+ @Override
+ public boolean containsSignedPreKey(int signedPreKeyId) {
+ return mXmppConnectionService.databaseBackend.containsSignedPreKey(account, signedPreKeyId);
+ }
+
+ /**
+ * Delete a SignedPreKeyRecord from local storage.
+ *
+ * @param signedPreKeyId The ID of the SignedPreKeyRecord to remove.
+ */
+ @Override
+ public void removeSignedPreKey(int signedPreKeyId) {
+ mXmppConnectionService.databaseBackend.deleteSignedPreKey(account, signedPreKeyId);
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java
new file mode 100644
index 00000000..cf950d6d
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java
@@ -0,0 +1,249 @@
+package eu.siacs.conversations.crypto.axolotl;
+
+import android.util.Base64;
+import android.util.Log;
+
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.SecureRandom;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.KeyGenerator;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+public class XmppAxolotlMessage {
+ public static final String CONTAINERTAG = "encrypted";
+ public static final String HEADER = "header";
+ public static final String SOURCEID = "sid";
+ public static final String KEYTAG = "key";
+ public static final String REMOTEID = "rid";
+ public static final String IVTAG = "iv";
+ public static final String PAYLOAD = "payload";
+
+ private static final String KEYTYPE = "AES";
+ private static final String CIPHERMODE = "AES/GCM/NoPadding";
+ private static final String PROVIDER = "BC";
+
+ private byte[] innerKey;
+ private byte[] ciphertext = null;
+ private byte[] iv = null;
+ private final Map<Integer, byte[]> keys;
+ private final Jid from;
+ private final int sourceDeviceId;
+
+ public static class XmppAxolotlPlaintextMessage {
+ private final String plaintext;
+ private final String fingerprint;
+
+ public XmppAxolotlPlaintextMessage(String plaintext, String fingerprint) {
+ this.plaintext = plaintext;
+ this.fingerprint = fingerprint;
+ }
+
+ public String getPlaintext() {
+ return plaintext;
+ }
+
+
+ public String getFingerprint() {
+ return fingerprint;
+ }
+ }
+
+ public static class XmppAxolotlKeyTransportMessage {
+ private final String fingerprint;
+ private final byte[] key;
+ private final byte[] iv;
+
+ public XmppAxolotlKeyTransportMessage(String fingerprint, byte[] key, byte[] iv) {
+ this.fingerprint = fingerprint;
+ this.key = key;
+ this.iv = iv;
+ }
+
+ public String getFingerprint() {
+ return fingerprint;
+ }
+
+ public byte[] getKey() {
+ return key;
+ }
+
+ public byte[] getIv() {
+ return iv;
+ }
+ }
+
+ private XmppAxolotlMessage(final Element axolotlMessage, final Jid from) throws IllegalArgumentException {
+ this.from = from;
+ Element header = axolotlMessage.findChild(HEADER);
+ this.sourceDeviceId = Integer.parseInt(header.getAttribute(SOURCEID));
+ List<Element> keyElements = header.getChildren();
+ this.keys = new HashMap<>(keyElements.size());
+ for (Element keyElement : keyElements) {
+ switch (keyElement.getName()) {
+ case KEYTAG:
+ try {
+ Integer recipientId = Integer.parseInt(keyElement.getAttribute(REMOTEID));
+ byte[] key = Base64.decode(keyElement.getContent(), Base64.DEFAULT);
+ this.keys.put(recipientId, key);
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException(e);
+ }
+ break;
+ case IVTAG:
+ if (this.iv != null) {
+ throw new IllegalArgumentException("Duplicate iv entry");
+ }
+ iv = Base64.decode(keyElement.getContent(), Base64.DEFAULT);
+ break;
+ default:
+ Log.w(Config.LOGTAG, "Unexpected element in header: " + keyElement.toString());
+ break;
+ }
+ }
+ Element payloadElement = axolotlMessage.findChild(PAYLOAD);
+ if (payloadElement != null) {
+ ciphertext = Base64.decode(payloadElement.getContent(), Base64.DEFAULT);
+ }
+ }
+
+ public XmppAxolotlMessage(Jid from, int sourceDeviceId) {
+ this.from = from;
+ this.sourceDeviceId = sourceDeviceId;
+ this.keys = new HashMap<>();
+ this.iv = generateIv();
+ this.innerKey = generateKey();
+ }
+
+ public static XmppAxolotlMessage fromElement(Element element, Jid from) {
+ return new XmppAxolotlMessage(element, from);
+ }
+
+ private static byte[] generateKey() {
+ try {
+ KeyGenerator generator = KeyGenerator.getInstance(KEYTYPE);
+ generator.init(128);
+ return generator.generateKey().getEncoded();
+ } catch (NoSuchAlgorithmException e) {
+ Log.e(Config.LOGTAG, e.getMessage());
+ return null;
+ }
+ }
+
+ private static byte[] generateIv() {
+ SecureRandom random = new SecureRandom();
+ byte[] iv = new byte[16];
+ random.nextBytes(iv);
+ return iv;
+ }
+
+ public void encrypt(String plaintext) throws CryptoFailedException {
+ try {
+ SecretKey secretKey = new SecretKeySpec(innerKey, KEYTYPE);
+ IvParameterSpec ivSpec = new IvParameterSpec(iv);
+ Cipher cipher = Cipher.getInstance(CIPHERMODE, PROVIDER);
+ cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec);
+ this.innerKey = secretKey.getEncoded();
+ this.ciphertext = cipher.doFinal(plaintext.getBytes());
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
+ | IllegalBlockSizeException | BadPaddingException | NoSuchProviderException
+ | InvalidAlgorithmParameterException e) {
+ throw new CryptoFailedException(e);
+ }
+ }
+
+ public Jid getFrom() {
+ return this.from;
+ }
+
+ public int getSenderDeviceId() {
+ return sourceDeviceId;
+ }
+
+ public byte[] getCiphertext() {
+ return ciphertext;
+ }
+
+ public void addDevice(XmppAxolotlSession session) {
+ byte[] key = session.processSending(innerKey);
+ if (key != null) {
+ keys.put(session.getRemoteAddress().getDeviceId(), key);
+ }
+ }
+
+ public byte[] getInnerKey() {
+ return innerKey;
+ }
+
+ public byte[] getIV() {
+ return this.iv;
+ }
+
+ public Element toElement() {
+ Element encryptionElement = new Element(CONTAINERTAG, AxolotlService.PEP_PREFIX);
+ Element headerElement = encryptionElement.addChild(HEADER);
+ headerElement.setAttribute(SOURCEID, sourceDeviceId);
+ for (Map.Entry<Integer, byte[]> keyEntry : keys.entrySet()) {
+ Element keyElement = new Element(KEYTAG);
+ keyElement.setAttribute(REMOTEID, keyEntry.getKey());
+ keyElement.setContent(Base64.encodeToString(keyEntry.getValue(), Base64.DEFAULT));
+ headerElement.addChild(keyElement);
+ }
+ headerElement.addChild(IVTAG).setContent(Base64.encodeToString(iv, Base64.DEFAULT));
+ if (ciphertext != null) {
+ Element payload = encryptionElement.addChild(PAYLOAD);
+ payload.setContent(Base64.encodeToString(ciphertext, Base64.DEFAULT));
+ }
+ return encryptionElement;
+ }
+
+ private byte[] unpackKey(XmppAxolotlSession session, Integer sourceDeviceId) {
+ byte[] encryptedKey = keys.get(sourceDeviceId);
+ return (encryptedKey != null) ? session.processReceiving(encryptedKey) : null;
+ }
+
+ public XmppAxolotlKeyTransportMessage getParameters(XmppAxolotlSession session, Integer sourceDeviceId) {
+ byte[] key = unpackKey(session, sourceDeviceId);
+ return (key != null)
+ ? new XmppAxolotlKeyTransportMessage(session.getFingerprint(), key, getIV())
+ : null;
+ }
+
+ public XmppAxolotlPlaintextMessage decrypt(XmppAxolotlSession session, Integer sourceDeviceId) throws CryptoFailedException {
+ XmppAxolotlPlaintextMessage plaintextMessage = null;
+ byte[] key = unpackKey(session, sourceDeviceId);
+ if (key != null) {
+ try {
+ Cipher cipher = Cipher.getInstance(CIPHERMODE, PROVIDER);
+ SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE);
+ IvParameterSpec ivSpec = new IvParameterSpec(iv);
+
+ cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
+
+ String plaintext = new String(cipher.doFinal(ciphertext));
+ plaintextMessage = new XmppAxolotlPlaintextMessage(plaintext, session.getFingerprint());
+
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
+ | InvalidAlgorithmParameterException | IllegalBlockSizeException
+ | BadPaddingException | NoSuchProviderException e) {
+ throw new CryptoFailedException(e);
+ }
+ }
+ return plaintextMessage;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlSession.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlSession.java
new file mode 100644
index 00000000..93ed32a2
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlSession.java
@@ -0,0 +1,221 @@
+package eu.siacs.conversations.crypto.axolotl;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.util.Log;
+
+import org.whispersystems.libaxolotl.AxolotlAddress;
+import org.whispersystems.libaxolotl.DuplicateMessageException;
+import org.whispersystems.libaxolotl.IdentityKey;
+import org.whispersystems.libaxolotl.InvalidKeyException;
+import org.whispersystems.libaxolotl.InvalidKeyIdException;
+import org.whispersystems.libaxolotl.InvalidMessageException;
+import org.whispersystems.libaxolotl.InvalidVersionException;
+import org.whispersystems.libaxolotl.LegacyMessageException;
+import org.whispersystems.libaxolotl.NoSessionException;
+import org.whispersystems.libaxolotl.SessionCipher;
+import org.whispersystems.libaxolotl.UntrustedIdentityException;
+import org.whispersystems.libaxolotl.protocol.CiphertextMessage;
+import org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage;
+import org.whispersystems.libaxolotl.protocol.WhisperMessage;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
+
+public class XmppAxolotlSession {
+ private final SessionCipher cipher;
+ private final SQLiteAxolotlStore sqLiteAxolotlStore;
+ private final AxolotlAddress remoteAddress;
+ private final Account account;
+ private IdentityKey identityKey;
+ private Integer preKeyId = null;
+ 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) {
+ this(account, store, remoteAddress);
+ this.identityKey = identityKey;
+ }
+
+ public XmppAxolotlSession(Account account, SQLiteAxolotlStore store, AxolotlAddress remoteAddress) {
+ this.cipher = new SessionCipher(store, remoteAddress);
+ this.remoteAddress = remoteAddress;
+ this.sqLiteAxolotlStore = store;
+ this.account = account;
+ }
+
+ public Integer getPreKeyId() {
+ return preKeyId;
+ }
+
+ public void resetPreKeyId() {
+
+ preKeyId = null;
+ }
+
+ public String getFingerprint() {
+ return identityKey == null ? null : identityKey.getFingerprint().replaceAll("\\s", "");
+ }
+
+ public IdentityKey getIdentityKey() {
+ return identityKey;
+ }
+
+ public AxolotlAddress getRemoteAddress() {
+ return remoteAddress;
+ }
+
+ public boolean isFresh() {
+ return fresh;
+ }
+
+ public void setNotFresh() {
+ this.fresh = false;
+ }
+
+ protected void setTrust(Trust trust) {
+ sqLiteAxolotlStore.setFingerprintTrust(getFingerprint(), trust);
+ }
+
+ protected Trust getTrust() {
+ Trust trust = sqLiteAxolotlStore.getFingerprintTrust(getFingerprint());
+ return (trust == null) ? Trust.UNDECIDED : trust;
+ }
+
+ @Nullable
+ public byte[] processReceiving(byte[] encryptedKey) {
+ byte[] plaintext = null;
+ Trust trust = getTrust();
+ switch (trust) {
+ case INACTIVE_TRUSTED:
+ case UNDECIDED:
+ case UNTRUSTED:
+ case TRUSTED:
+ case INACTIVE_TRUSTED_X509:
+ case TRUSTED_X509:
+ try {
+ try {
+ PreKeyWhisperMessage message = new PreKeyWhisperMessage(encryptedKey);
+ Log.i(Config.LOGTAG, AxolotlServiceImpl.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, AxolotlServiceImpl.getLogprefix(account) + "Had session with fingerprint " + this.getFingerprint() + ", received message with fingerprint " + msgIdentityKey.getFingerprint());
+ } else {
+ this.identityKey = msgIdentityKey;
+ plaintext = cipher.decrypt(message);
+ if (message.getPreKeyId().isPresent()) {
+ preKeyId = message.getPreKeyId().get();
+ }
+ }
+ } catch (InvalidMessageException | InvalidVersionException e) {
+ Log.i(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "WhisperMessage received");
+ WhisperMessage message = new WhisperMessage(encryptedKey);
+ plaintext = cipher.decrypt(message);
+ } catch (InvalidKeyException | InvalidKeyIdException | UntrustedIdentityException e) {
+ Log.w(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Error decrypting axolotl header, " + e.getClass().getName() + ": " + e.getMessage());
+ }
+ } catch (LegacyMessageException | InvalidMessageException | DuplicateMessageException | NoSessionException e) {
+ Log.w(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Error decrypting axolotl header, " + e.getClass().getName() + ": " + e.getMessage());
+ }
+
+ if (plaintext != null) {
+ if (trust == Trust.INACTIVE_TRUSTED) {
+ setTrust(Trust.TRUSTED);
+ } else if (trust == Trust.INACTIVE_TRUSTED_X509) {
+ setTrust(Trust.TRUSTED_X509);
+ }
+ }
+
+ break;
+
+ case COMPROMISED:
+ default:
+ // ignore
+ break;
+ }
+ return plaintext;
+ }
+
+ @Nullable
+ public byte[] processSending(@NonNull byte[] outgoingMessage) {
+ Trust trust = getTrust();
+ if (trust.trusted()) {
+ CiphertextMessage ciphertextMessage = cipher.encrypt(outgoingMessage);
+ return ciphertextMessage.serialize();
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/DigestMd5.java b/src/main/java/eu/siacs/conversations/crypto/sasl/DigestMd5.java
new file mode 100644
index 00000000..8b16215b
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/crypto/sasl/DigestMd5.java
@@ -0,0 +1,91 @@
+package eu.siacs.conversations.crypto.sasl;
+
+import android.util.Base64;
+
+import java.math.BigInteger;
+import java.nio.charset.Charset;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.xml.TagWriter;
+
+public class DigestMd5 extends SaslMechanism {
+ public DigestMd5(final TagWriter tagWriter, final Account account, final SecureRandom rng) {
+ super(tagWriter, account, rng);
+ }
+
+ @Override
+ public int getPriority() {
+ return 10;
+ }
+
+ @Override
+ public String getMechanism() {
+ return "DIGEST-MD5";
+ }
+
+ private State state = State.INITIAL;
+
+ @Override
+ public String getResponse(final String challenge) throws AuthenticationException {
+ switch (state) {
+ case INITIAL:
+ state = State.RESPONSE_SENT;
+ final String encodedResponse;
+ try {
+ final Tokenizer tokenizer = new Tokenizer(Base64.decode(challenge, Base64.DEFAULT));
+ String nonce = "";
+ for (final String token : tokenizer) {
+ final String[] parts = token.split("=", 2);
+ if (parts[0].equals("nonce")) {
+ nonce = parts[1].replace("\"", "");
+ } else if (parts[0].equals("rspauth")) {
+ return "";
+ }
+ }
+ final String digestUri = "xmpp/" + account.getServer();
+ final String nonceCount = "00000001";
+ final String x = account.getUsername() + ":" + account.getServer() + ":"
+ + account.getPassword();
+ final MessageDigest md = MessageDigest.getInstance("MD5");
+ final byte[] y = md.digest(x.getBytes(Charset.defaultCharset()));
+ final String cNonce = new BigInteger(100, rng).toString(32);
+ final byte[] a1 = CryptoHelper.concatenateByteArrays(y,
+ (":" + nonce + ":" + cNonce).getBytes(Charset.defaultCharset()));
+ final String a2 = "AUTHENTICATE:" + digestUri;
+ final String ha1 = CryptoHelper.bytesToHex(md.digest(a1));
+ final String ha2 = CryptoHelper.bytesToHex(md.digest(a2.getBytes(Charset
+ .defaultCharset())));
+ final String kd = ha1 + ":" + nonce + ":" + nonceCount + ":" + cNonce
+ + ":auth:" + ha2;
+ final String response = CryptoHelper.bytesToHex(md.digest(kd.getBytes(Charset
+ .defaultCharset())));
+ final String saslString = "username=\"" + account.getUsername()
+ + "\",realm=\"" + account.getServer() + "\",nonce=\""
+ + nonce + "\",cnonce=\"" + cNonce + "\",nc=" + nonceCount
+ + ",qop=auth,digest-uri=\"" + digestUri + "\",response="
+ + response + ",charset=utf-8";
+ encodedResponse = Base64.encodeToString(
+ saslString.getBytes(Charset.defaultCharset()),
+ Base64.NO_WRAP);
+ } catch (final NoSuchAlgorithmException e) {
+ throw new AuthenticationException(e);
+ }
+
+ return encodedResponse;
+ case RESPONSE_SENT:
+ state = State.VALID_SERVER_RESPONSE;
+ break;
+ case VALID_SERVER_RESPONSE:
+ if (challenge==null) {
+ return null; //everything is fine
+ }
+ default:
+ throw new InvalidStateException(state);
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/External.java b/src/main/java/eu/siacs/conversations/crypto/sasl/External.java
new file mode 100644
index 00000000..8fd91cf4
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/crypto/sasl/External.java
@@ -0,0 +1,30 @@
+package eu.siacs.conversations.crypto.sasl;
+
+import android.util.Base64;
+
+import java.security.SecureRandom;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.xml.TagWriter;
+
+public class External extends SaslMechanism {
+
+ public External(TagWriter tagWriter, Account account, SecureRandom rng) {
+ super(tagWriter, account, rng);
+ }
+
+ @Override
+ public int getPriority() {
+ return 25;
+ }
+
+ @Override
+ public String getMechanism() {
+ return "EXTERNAL";
+ }
+
+ @Override
+ public String getClientFirstMessage() {
+ return Base64.encodeToString(account.getJid().toBareJid().toString().getBytes(),Base64.NO_WRAP);
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java b/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java
new file mode 100644
index 00000000..40a55151
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java
@@ -0,0 +1,30 @@
+package eu.siacs.conversations.crypto.sasl;
+
+import android.util.Base64;
+
+import java.nio.charset.Charset;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.xml.TagWriter;
+
+public class Plain extends SaslMechanism {
+ public Plain(final TagWriter tagWriter, final Account account) {
+ super(tagWriter, account, null);
+ }
+
+ @Override
+ public int getPriority() {
+ return 10;
+ }
+
+ @Override
+ public String getMechanism() {
+ return "PLAIN";
+ }
+
+ @Override
+ public String getClientFirstMessage() {
+ final String sasl = '\u0000' + account.getUsername() + '\u0000' + account.getPassword();
+ return Base64.encodeToString(sasl.getBytes(Charset.defaultCharset()), Base64.NO_WRAP);
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java
new file mode 100644
index 00000000..5b4b99ef
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java
@@ -0,0 +1,62 @@
+package eu.siacs.conversations.crypto.sasl;
+
+import java.security.SecureRandom;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.xml.TagWriter;
+
+public abstract class SaslMechanism {
+
+ final protected TagWriter tagWriter;
+ final protected Account account;
+ final protected SecureRandom rng;
+
+ protected enum State {
+ INITIAL,
+ AUTH_TEXT_SENT,
+ RESPONSE_SENT,
+ VALID_SERVER_RESPONSE,
+ }
+
+ public static class AuthenticationException extends Exception {
+ public AuthenticationException(final String message) {
+ super(message);
+ }
+
+ public AuthenticationException(final Exception inner) {
+ super(inner);
+ }
+ }
+
+ public static class InvalidStateException extends AuthenticationException {
+ public InvalidStateException(final String message) {
+ super(message);
+ }
+
+ public InvalidStateException(final State state) {
+ this("Invalid state: " + state.toString());
+ }
+ }
+
+ public SaslMechanism(final TagWriter tagWriter, final Account account, final SecureRandom rng) {
+ this.tagWriter = tagWriter;
+ this.account = account;
+ this.rng = rng;
+ }
+
+ /**
+ * The priority is used to pin the authentication mechanism. If authentication fails, it MAY be retried with another
+ * mechanism of the same priority, but MUST NOT be tried with a mechanism of lower priority (to prevent downgrade
+ * attacks).
+ * @return An arbitrary int representing the priority
+ */
+ public abstract int getPriority();
+
+ public abstract String getMechanism();
+ public String getClientFirstMessage() {
+ return "";
+ }
+ public String getResponse(final String challenge) throws AuthenticationException {
+ return "";
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java
new file mode 100644
index 00000000..3a05446c
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java
@@ -0,0 +1,234 @@
+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.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;
+
+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 {
+ DIGEST = 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) {
+ super(tagWriter, account, rng);
+
+ // This nonce should be different for each authentication attempt.
+ clientNonce = new BigInteger(100, this.rng).toString(32);
+ clientFirstMessageBare = "";
+ }
+
+ @Override
+ public int getPriority() {
+ return 20;
+ }
+
+ @Override
+ public String getMechanism() {
+ 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:
+ final String clientCalculatedServerFinalMessage = "v=" +
+ Base64.encodeToString(serverSignature, Base64.NO_WRAP);
+ if (challenge == null || !clientCalculatedServerFinalMessage.equals(new String(Base64.decode(challenge, Base64.DEFAULT)))) {
+ throw new AuthenticationException("Server final message does not match calculated final message");
+ }
+ state = State.VALID_SERVER_RESPONSE;
+ return "";
+ 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;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/Tokenizer.java b/src/main/java/eu/siacs/conversations/crypto/sasl/Tokenizer.java
new file mode 100644
index 00000000..e37e0fa7
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/crypto/sasl/Tokenizer.java
@@ -0,0 +1,78 @@
+package eu.siacs.conversations.crypto.sasl;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+
+/**
+ * A tokenizer for GS2 header strings
+ */
+public final class Tokenizer implements Iterator<String>, Iterable<String> {
+ private final List<String> parts;
+ private int index;
+
+ public Tokenizer(final byte[] challenge) {
+ final String challengeString = new String(challenge);
+ parts = new ArrayList<>(Arrays.asList(challengeString.split(",")));
+ // Trim parts.
+ for (int i = 0; i < parts.size(); i++) {
+ parts.set(i, parts.get(i).trim());
+ }
+ index = 0;
+ }
+
+ /**
+ * Returns true if there is at least one more element, false otherwise.
+ *
+ * @see #next
+ */
+ @Override
+ public boolean hasNext() {
+ return parts.size() != index + 1;
+ }
+
+ /**
+ * Returns the next object and advances the iterator.
+ *
+ * @return the next object.
+ * @throws java.util.NoSuchElementException if there are no more elements.
+ * @see #hasNext
+ */
+ @Override
+ public String next() {
+ if (hasNext()) {
+ return parts.get(index++);
+ } else {
+ throw new NoSuchElementException("No such element. Size is: " + parts.size());
+ }
+ }
+
+ /**
+ * Removes the last object returned by {@code next} from the collection.
+ * This method can only be called once between each call to {@code next}.
+ *
+ * @throws UnsupportedOperationException if removing is not supported by the collection being
+ * iterated.
+ * @throws IllegalStateException if {@code next} has not been called, or {@code remove} has
+ * already been called after the last call to {@code next}.
+ */
+ @Override
+ public void remove() {
+ if(index <= 0) {
+ throw new IllegalStateException("You can't delete an element before first next() method call");
+ }
+ parts.remove(--index);
+ }
+
+ /**
+ * Returns an {@link java.util.Iterator} for the elements in this object.
+ *
+ * @return An {@code Iterator} instance.
+ */
+ @Override
+ public Iterator<String> iterator() {
+ return parts.iterator();
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/entities/AbstractEntity.java b/src/main/java/eu/siacs/conversations/entities/AbstractEntity.java
new file mode 100644
index 00000000..957b0a14
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/entities/AbstractEntity.java
@@ -0,0 +1,20 @@
+package eu.siacs.conversations.entities;
+
+import android.content.ContentValues;
+
+public abstract class AbstractEntity {
+
+ public static final String UUID = "uuid";
+
+ protected String uuid;
+
+ public String getUuid() {
+ return this.uuid;
+ }
+
+ public abstract ContentValues getContentValues();
+
+ public boolean equals(AbstractEntity entity) {
+ return this.getUuid().equals(entity.getUuid());
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java
new file mode 100644
index 00000000..9b4198c8
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/entities/Account.java
@@ -0,0 +1,559 @@
+package eu.siacs.conversations.entities;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.os.SystemClock;
+import android.util.Pair;
+
+import net.java.otr4j.crypto.OtrCryptoEngineImpl;
+import net.java.otr4j.crypto.OtrCryptoException;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.security.PublicKey;
+import java.security.interfaces.DSAPublicKey;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+import de.thedevstack.conversationsplus.ConversationsPlusPreferences;
+import de.thedevstack.conversationsplus.utils.SimpleCryptoUtil;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.crypto.OtrService;
+import eu.siacs.conversations.crypto.PgpDecryptionService;
+import eu.siacs.conversations.crypto.axolotl.AxolotlService;
+import eu.siacs.conversations.crypto.axolotl.AxolotlServiceImpl;
+import eu.siacs.conversations.crypto.axolotl.AxolotlServiceStub;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import eu.siacs.conversations.xmpp.jid.InvalidJidException;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+public class Account extends AbstractEntity {
+
+ public static final String TABLENAME = "accounts";
+
+ public static final String USERNAME = "username";
+ public static final String SERVER = "server";
+ public static final String PASSWORD = "password";
+ public static final String OPTIONS = "options";
+ public static final String ROSTERVERSION = "rosterversion";
+ public static final String KEYS = "keys";
+ public static final String AVATAR = "avatar";
+ public static final String DISPLAY_NAME = "display_name";
+ public static final String HOSTNAME = "hostname";
+ public static final String PORT = "port";
+
+ public static final String PINNED_MECHANISM_KEY = "pinned_mechanism";
+
+ public static final int OPTION_USETLS = 0;
+ public static final int OPTION_DISABLED = 1;
+ public static final int OPTION_REGISTER = 2;
+ public static final int OPTION_USECOMPRESSION = 3;
+ public final HashSet<Pair<String, String>> inProgressDiscoFetches = new HashSet<>();
+
+ public boolean httpUploadAvailable(long filesize) {
+ return xmppConnection != null && xmppConnection.getFeatures().httpUpload(filesize);
+ }
+
+ public boolean httpUploadAvailable() {
+ return httpUploadAvailable(0);
+ }
+
+ public void setDisplayName(String displayName) {
+ this.displayName = displayName;
+ }
+
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ public XmppConnection.Identity getServerIdentity() {
+ if (xmppConnection == null) {
+ return XmppConnection.Identity.UNKNOWN;
+ } else {
+ return xmppConnection.getServerIdentity();
+ }
+ }
+
+ public enum State {
+ DISABLED,
+ OFFLINE,
+ CONNECTING,
+ ONLINE,
+ NO_INTERNET,
+ UNAUTHORIZED(true),
+ SERVER_NOT_FOUND(true),
+ REGISTRATION_FAILED(true),
+ REGISTRATION_CONFLICT(true),
+ REGISTRATION_SUCCESSFUL,
+ REGISTRATION_NOT_SUPPORTED(true),
+ SECURITY_ERROR(true),
+ INCOMPATIBLE_SERVER(true),
+ TOR_NOT_AVAILABLE(true);
+
+ private final boolean isError;
+
+ public boolean isError() {
+ return this.isError;
+ }
+
+ private State(final boolean isError) {
+ this.isError = isError;
+ }
+
+ private State() {
+ this(false);
+ }
+
+ public int getReadableId() {
+ switch (this) {
+ case DISABLED:
+ return R.string.account_status_disabled;
+ case ONLINE:
+ return R.string.account_status_online;
+ case CONNECTING:
+ return R.string.account_status_connecting;
+ case OFFLINE:
+ return R.string.account_status_offline;
+ case UNAUTHORIZED:
+ return R.string.account_status_unauthorized;
+ case SERVER_NOT_FOUND:
+ return R.string.account_status_not_found;
+ case NO_INTERNET:
+ return R.string.account_status_no_internet;
+ case REGISTRATION_FAILED:
+ return R.string.account_status_regis_fail;
+ case REGISTRATION_CONFLICT:
+ return R.string.account_status_regis_conflict;
+ case REGISTRATION_SUCCESSFUL:
+ return R.string.account_status_regis_success;
+ case REGISTRATION_NOT_SUPPORTED:
+ return R.string.account_status_regis_not_sup;
+ case SECURITY_ERROR:
+ return R.string.account_status_security_error;
+ case INCOMPATIBLE_SERVER:
+ return R.string.account_status_incompatible_server;
+ case TOR_NOT_AVAILABLE:
+ return R.string.account_status_tor_unavailable;
+ default:
+ return R.string.account_status_unknown;
+ }
+ }
+ }
+
+ public List<Conversation> pendingConferenceJoins = new CopyOnWriteArrayList<>();
+ public List<Conversation> pendingConferenceLeaves = new CopyOnWriteArrayList<>();
+
+ private static final String KEY_PGP_SIGNATURE = "pgp_signature";
+ private static final String KEY_PGP_ID = "pgp_id";
+
+ protected Jid jid;
+ protected String password;
+ protected int options = 0;
+ protected String rosterVersion;
+ protected State status = State.OFFLINE;
+ protected JSONObject keys = new JSONObject();
+ protected String avatar;
+ protected String displayName = null;
+ protected String hostname = null;
+ protected int port = 5222;
+ protected boolean online = false;
+ private OtrService mOtrService = null;
+ private AxolotlService axolotlService = null;
+ private PgpDecryptionService pgpDecryptionService = null;
+ private XmppConnection xmppConnection = null;
+ private long mEndGracePeriod = 0L;
+ private String otrFingerprint;
+ private final Roster roster = new Roster(this);
+ private List<Bookmark> bookmarks = new CopyOnWriteArrayList<>();
+ private final Collection<Jid> blocklist = new CopyOnWriteArraySet<>();
+
+ public static final String PW_SEED = "sadjgdiahsdkhashp3zt98edAFSFIOKZUIUOz23ejj12ezhez2398iehz";
+
+ public Account() {
+ this.uuid = "0";
+ }
+
+ public Account(final Jid jid, final String password) {
+ this(java.util.UUID.randomUUID().toString(), jid,
+ password, 0, null, "", null, null, null, 5222);
+ }
+
+ private Account(final String uuid, final Jid jid,
+ final String password, final int options, final String rosterVersion, final String keys,
+ final String avatar, String displayName, String hostname, int port) {
+ this.uuid = uuid;
+ this.jid = jid;
+ if (jid.isBareJid()) {
+ this.setResource("mobile");
+ }
+ this.password = password;
+ this.options = options;
+ this.rosterVersion = rosterVersion;
+ try {
+ this.keys = new JSONObject(keys);
+ } catch (final JSONException ignored) {
+ this.keys = new JSONObject();
+ }
+ this.avatar = avatar;
+ this.displayName = displayName;
+ this.hostname = hostname;
+ this.port = port;
+ }
+
+ public static Account fromCursor(final Cursor cursor) {
+ Jid jid = null;
+ try {
+ jid = Jid.fromParts(cursor.getString(cursor.getColumnIndex(USERNAME)),
+ cursor.getString(cursor.getColumnIndex(SERVER)), "mobile");
+ } catch (final InvalidJidException ignored) {
+ }
+ String password = SimpleCryptoUtil.decrypt(PW_SEED, cursor.getString(cursor.getColumnIndex(PASSWORD)));
+ return new Account(cursor.getString(cursor.getColumnIndex(UUID)),
+ jid,
+ password,
+ cursor.getInt(cursor.getColumnIndex(OPTIONS)),
+ cursor.getString(cursor.getColumnIndex(ROSTERVERSION)),
+ cursor.getString(cursor.getColumnIndex(KEYS)),
+ cursor.getString(cursor.getColumnIndex(AVATAR)),
+ cursor.getString(cursor.getColumnIndex(DISPLAY_NAME)),
+ cursor.getString(cursor.getColumnIndex(HOSTNAME)),
+ cursor.getInt(cursor.getColumnIndex(PORT)));
+ }
+
+ public boolean isOptionSet(final int option) {
+ return ((options & (1 << option)) != 0);
+ }
+
+ public void setOption(final int option, final boolean value) {
+ if (value) {
+ this.options |= 1 << option;
+ } else {
+ this.options &= ~(1 << option);
+ }
+ }
+
+ public String getUsername() {
+ return jid.getLocalpart();
+ }
+
+ public void setJid(final Jid jid) {
+ this.jid = jid;
+ }
+
+ public Jid getServer() {
+ return jid.toDomainJid();
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(final String password) {
+ this.password = password;
+ }
+
+ public void setHostname(String hostname) {
+ this.hostname = hostname;
+ }
+
+ public String getHostname() {
+ return this.hostname == null ? "" : this.hostname;
+ }
+
+ public void setPort(int port) {
+ this.port = port;
+ }
+
+ public int getPort() {
+ return this.port;
+ }
+
+ public State getStatus() {
+ if (isOptionSet(OPTION_DISABLED)) {
+ return State.DISABLED;
+ } else {
+ return this.status;
+ }
+ }
+
+ public void setStatus(final State status) {
+ this.status = status;
+ }
+
+ public boolean errorStatus() {
+ return getStatus().isError();
+ }
+
+ public boolean hasErrorStatus() {
+ return getXmppConnection() != null && getStatus().isError() && getXmppConnection().getAttempt() >= 3;
+ }
+
+ public String getResource() {
+ return jid.getResourcepart();
+ }
+
+ public boolean setResource(final String resource) {
+ final String oldResource = jid.getResourcepart();
+ if (oldResource == null || !oldResource.equals(resource)) {
+ try {
+ jid = Jid.fromParts(jid.getLocalpart(), jid.getDomainpart(), resource);
+ return true;
+ } catch (final InvalidJidException ignored) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public Jid getJid() {
+ return jid;
+ }
+
+ public JSONObject getKeys() {
+ return keys;
+ }
+
+ public String getKey(final String name) {
+ return this.keys.optString(name, null);
+ }
+
+ public boolean setKey(final String keyName, final String keyValue) {
+ try {
+ this.keys.put(keyName, keyValue);
+ return true;
+ } catch (final JSONException e) {
+ return false;
+ }
+ }
+
+ public boolean setPrivateKeyAlias(String alias) {
+ return setKey("private_key_alias", alias);
+ }
+
+ public String getPrivateKeyAlias() {
+ return getKey("private_key_alias");
+ }
+
+ @Override
+ public ContentValues getContentValues() {
+ final ContentValues values = new ContentValues();
+ values.put(UUID, uuid);
+ values.put(USERNAME, jid.getLocalpart());
+ values.put(SERVER, jid.getDomainpart());
+ values.put(PASSWORD, SimpleCryptoUtil.encrypt(PW_SEED, password));
+ values.put(OPTIONS, options);
+ values.put(KEYS, this.keys.toString());
+ values.put(ROSTERVERSION, rosterVersion);
+ values.put(AVATAR, avatar);
+ values.put(DISPLAY_NAME, displayName);
+ values.put(HOSTNAME, hostname);
+ values.put(PORT, port);
+ return values;
+ }
+
+ public AxolotlService getAxolotlService() {
+ return axolotlService;
+ }
+
+ public void initAccountServices(final XmppConnectionService context) {
+ this.mOtrService = new OtrService(context, this);
+ if (ConversationsPlusPreferences.omemoEnabled()) {
+ this.axolotlService = new AxolotlServiceImpl(this, context);
+ if (xmppConnection != null) {
+ xmppConnection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService);
+ }
+ } else {
+ this.axolotlService = new AxolotlServiceStub();
+ }
+
+ this.pgpDecryptionService = new PgpDecryptionService(context);
+ }
+
+ public OtrService getOtrService() {
+ return this.mOtrService;
+ }
+
+ public PgpDecryptionService getPgpDecryptionService() {
+ return pgpDecryptionService;
+ }
+
+ public XmppConnection getXmppConnection() {
+ return this.xmppConnection;
+ }
+
+ public void setXmppConnection(final XmppConnection connection) {
+ this.xmppConnection = connection;
+ }
+
+ public String getOtrFingerprint() {
+ if (this.otrFingerprint == null) {
+ try {
+ if (this.mOtrService == null) {
+ return null;
+ }
+ final PublicKey publicKey = this.mOtrService.getPublicKey();
+ if (publicKey == null || !(publicKey instanceof DSAPublicKey)) {
+ return null;
+ }
+ this.otrFingerprint = new OtrCryptoEngineImpl().getFingerprint(publicKey);
+ return this.otrFingerprint;
+ } catch (final OtrCryptoException ignored) {
+ return null;
+ }
+ } else {
+ return this.otrFingerprint;
+ }
+ }
+
+ public String getRosterVersion() {
+ if (this.rosterVersion == null) {
+ return "";
+ } else {
+ return this.rosterVersion;
+ }
+ }
+
+ public void setRosterVersion(final String version) {
+ this.rosterVersion = version;
+ }
+
+ public int countPresences() {
+ return this.getRoster().getContact(this.getJid().toBareJid()).getPresences().size();
+ }
+
+ public String getPgpSignature() {
+ try {
+ if (keys.has(KEY_PGP_SIGNATURE) && !"null".equals(keys.getString(KEY_PGP_SIGNATURE))) {
+ return keys.getString(KEY_PGP_SIGNATURE);
+ } else {
+ return null;
+ }
+ } catch (final JSONException e) {
+ return null;
+ }
+ }
+
+ public boolean setPgpSignature(String signature) {
+ try {
+ keys.put(KEY_PGP_SIGNATURE, signature);
+ } catch (JSONException e) {
+ return false;
+ }
+ return true;
+ }
+
+ public boolean unsetPgpSignature() {
+ try {
+ keys.put(KEY_PGP_SIGNATURE, JSONObject.NULL);
+ } catch (JSONException e) {
+ return false;
+ }
+ return true;
+ }
+
+ public long getPgpId() {
+ if (keys.has(KEY_PGP_ID)) {
+ try {
+ return keys.getLong(KEY_PGP_ID);
+ } catch (JSONException e) {
+ return -1;
+ }
+ } else {
+ return -1;
+ }
+ }
+
+ public boolean setPgpSignId(long pgpID) {
+ try {
+ keys.put(KEY_PGP_ID, pgpID);
+ } catch (JSONException e) {
+ return false;
+ }
+ return true;
+ }
+
+ public Roster getRoster() {
+ return this.roster;
+ }
+
+ public List<Bookmark> getBookmarks() {
+ return this.bookmarks;
+ }
+
+ public void setBookmarks(final List<Bookmark> bookmarks) {
+ this.bookmarks = bookmarks;
+ }
+
+ public boolean hasBookmarkFor(final Jid conferenceJid) {
+ for (final Bookmark bookmark : this.bookmarks) {
+ final Jid jid = bookmark.getJid();
+ if (jid != null && jid.equals(conferenceJid.toBareJid())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public boolean setAvatar(final String filename) {
+ if (this.avatar != null && this.avatar.equals(filename)) {
+ return false;
+ } else {
+ this.avatar = filename;
+ return true;
+ }
+ }
+
+ public String getAvatar() {
+ return this.avatar;
+ }
+
+ public void activateGracePeriod() {
+ this.mEndGracePeriod = SystemClock.elapsedRealtime()
+ + (Config.CARBON_GRACE_PERIOD * 1000);
+ }
+
+ public void deactivateGracePeriod() {
+ this.mEndGracePeriod = 0L;
+ }
+
+ public boolean inGracePeriod() {
+ return SystemClock.elapsedRealtime() < this.mEndGracePeriod;
+ }
+
+ public String getShareableUri() {
+ final String fingerprint = this.getOtrFingerprint();
+ if (fingerprint != null) {
+ return "xmpp:" + this.getJid().toBareJid().toString() + "?otr-fingerprint="+fingerprint;
+ } else {
+ return "xmpp:" + this.getJid().toBareJid().toString();
+ }
+ }
+
+ public boolean isBlocked(final ListItem contact) {
+ final Jid jid = contact.getJid();
+ return jid != null && (blocklist.contains(jid.toBareJid()) || blocklist.contains(jid.toDomainJid()));
+ }
+
+ public boolean isBlocked(final Jid jid) {
+ return jid != null && blocklist.contains(jid.toBareJid());
+ }
+
+ public Collection<Jid> getBlocklist() {
+ return this.blocklist;
+ }
+
+ public void clearBlocklist() {
+ getBlocklist().clear();
+ }
+
+ public boolean isOnlineAndConnected() {
+ return this.getStatus() == State.ONLINE && this.getXmppConnection() != null;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/entities/Blockable.java b/src/main/java/eu/siacs/conversations/entities/Blockable.java
new file mode 100644
index 00000000..dbcd55c4
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/entities/Blockable.java
@@ -0,0 +1,11 @@
+package eu.siacs.conversations.entities;
+
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+public interface Blockable {
+ public boolean isBlocked();
+ public boolean isDomainBlocked();
+ public Jid getBlockedJid();
+ public Jid getJid();
+ public Account getAccount();
+}
diff --git a/src/main/java/eu/siacs/conversations/entities/Bookmark.java b/src/main/java/eu/siacs/conversations/entities/Bookmark.java
new file mode 100644
index 00000000..fa30443d
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/entities/Bookmark.java
@@ -0,0 +1,176 @@
+package eu.siacs.conversations.entities;
+
+import android.graphics.Color;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.utils.UIHelper;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+public class Bookmark extends Element implements ListItem {
+
+ private Account account;
+ private Conversation mJoinedConversation;
+
+ public Bookmark(final Account account, final Jid jid) {
+ super("conference");
+ this.setAttribute("jid", jid.toString());
+ this.account = account;
+ }
+
+ private Bookmark(Account account) {
+ super("conference");
+ this.account = account;
+ }
+
+ public static Bookmark parse(Element element, Account account) {
+ Bookmark bookmark = new Bookmark(account);
+ bookmark.setAttributes(element.getAttributes());
+ bookmark.setChildren(element.getChildren());
+ return bookmark;
+ }
+
+ public void setAutojoin(boolean autojoin) {
+ if (autojoin) {
+ this.setAttribute("autojoin", "true");
+ } else {
+ this.setAttribute("autojoin", "false");
+ }
+ }
+
+ @Override
+ public int compareTo(final ListItem another) {
+ return this.getDisplayName().compareToIgnoreCase(
+ another.getDisplayName());
+ }
+
+ @Override
+ public String getDisplayName() {
+ if (this.mJoinedConversation != null
+ && (this.mJoinedConversation.getMucOptions().getSubject() != null)) {
+ return this.mJoinedConversation.getMucOptions().getSubject();
+ } else if (getBookmarkName() != null) {
+ return getBookmarkName();
+ } else {
+ return this.getJid().getLocalpart();
+ }
+ }
+
+ @Override
+ public String getDisplayJid() {
+ Jid jid = getJid();
+ if (Config.LOCK_DOMAINS_IN_CONVERSATIONS && jid != null && jid.getDomainpart().equals(Config.CONFERENCE_DOMAIN_LOCK)) {
+ return jid.getLocalpart();
+ } else if (jid != null) {
+ return jid.toString();
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public Jid getJid() {
+ return this.getAttributeAsJid("jid");
+ }
+
+ @Override
+ public List<Tag> getTags() {
+ ArrayList<Tag> tags = new ArrayList<Tag>();
+ for (Element element : getChildren()) {
+ if (element.getName().equals("group") && element.getContent() != null) {
+ String group = element.getContent();
+ tags.add(new Tag(group, UIHelper.getColorForName(group)));
+ }
+ }
+ return tags;
+ }
+
+ @Override
+ public int getStatusColor() {
+ return Color.parseColor("#259B23");
+ }
+
+ public String getNick() {
+ return this.findChildContent("nick");
+ }
+
+ public void setNick(String nick) {
+ Element element = this.findChild("nick");
+ if (element == null) {
+ element = this.addChild("nick");
+ }
+ element.setContent(nick);
+ }
+
+ public boolean autojoin() {
+ return this.getAttributeAsBoolean("autojoin");
+ }
+
+ public String getPassword() {
+ return this.findChildContent("password");
+ }
+
+ public void setPassword(String password) {
+ Element element = this.findChild("password");
+ if (element != null) {
+ element.setContent(password);
+ }
+ }
+
+ public boolean match(String needle) {
+ if (needle == null) {
+ return true;
+ }
+ needle = needle.toLowerCase(Locale.US);
+ final Jid jid = getJid();
+ return (jid != null && jid.toString().contains(needle)) ||
+ getDisplayName().toLowerCase(Locale.US).contains(needle) ||
+ matchInTag(needle);
+ }
+
+ private boolean matchInTag(String needle) {
+ needle = needle.toLowerCase(Locale.US);
+ for (Tag tag : getTags()) {
+ if (tag.getName().toLowerCase(Locale.US).contains(needle)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public Account getAccount() {
+ return this.account;
+ }
+
+ public Conversation getConversation() {
+ return this.mJoinedConversation;
+ }
+
+ public void setConversation(Conversation conversation) {
+ this.mJoinedConversation = conversation;
+ }
+
+ public String getBookmarkName() {
+ return this.getAttribute("name");
+ }
+
+ public boolean setBookmarkName(String name) {
+ String before = getBookmarkName();
+ if (name != null && !name.equals(before)) {
+ this.setAttribute("name", name);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public void unregisterConversation() {
+ if (this.mJoinedConversation != null) {
+ this.mJoinedConversation.deregisterWithBookmark();
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java
new file mode 100644
index 00000000..9b4be366
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/entities/Contact.java
@@ -0,0 +1,562 @@
+package eu.siacs.conversations.entities;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.graphics.Color;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.utils.UIHelper;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.jid.InvalidJidException;
+import eu.siacs.conversations.xmpp.jid.Jid;
+import eu.siacs.conversations.xmpp.pep.Avatar;
+
+public class Contact implements ListItem, Blockable {
+ public static final String TABLENAME = "contacts";
+
+ public static final String SYSTEMNAME = "systemname";
+ public static final String SERVERNAME = "servername";
+ public static final String JID = "jid";
+ public static final String OPTIONS = "options";
+ public static final String SYSTEMACCOUNT = "systemaccount";
+ public static final String PHOTOURI = "photouri";
+ public static final String KEYS = "pgpkey";
+ public static final String ACCOUNT = "accountUuid";
+ public static final String AVATAR = "avatar";
+ public static final String LAST_PRESENCE = "last_presence";
+ public static final String LAST_TIME = "last_time";
+ public static final String GROUPS = "groups";
+ public Lastseen lastseen = new Lastseen();
+ protected String accountUuid;
+ protected String systemName;
+ protected String serverName;
+ protected String presenceName;
+ protected String commonName;
+ protected Jid jid;
+ protected int subscription = 0;
+ protected String systemAccount;
+ protected String photoUri;
+ protected JSONObject keys = new JSONObject();
+ protected JSONArray groups = new JSONArray();
+ protected final Presences presences = new Presences();
+ protected Account account;
+ protected Avatar avatar;
+
+ public Contact(final String account, final String systemName, final String serverName,
+ final Jid jid, final int subscription, final String photoUri,
+ final String systemAccount, final String keys, final String avatar, final Lastseen lastseen, final String groups) {
+ this.accountUuid = account;
+ this.systemName = systemName;
+ this.serverName = serverName;
+ this.jid = jid;
+ this.subscription = subscription;
+ this.photoUri = photoUri;
+ this.systemAccount = systemAccount;
+ try {
+ this.keys = (keys == null ? new JSONObject("") : new JSONObject(keys));
+ } catch (JSONException e) {
+ this.keys = new JSONObject();
+ }
+ if (avatar != null) {
+ this.avatar = new Avatar();
+ this.avatar.sha1sum = avatar;
+ this.avatar.origin = Avatar.Origin.VCARD; //always assume worst
+ }
+ try {
+ this.groups = (groups == null ? new JSONArray() : new JSONArray(groups));
+ } catch (JSONException e) {
+ this.groups = new JSONArray();
+ }
+ this.lastseen = lastseen;
+ }
+
+ public Contact(final Jid jid) {
+ this.jid = jid;
+ }
+
+ public static Contact fromCursor(final Cursor cursor) {
+ final Lastseen lastseen = new Lastseen(
+ cursor.getString(cursor.getColumnIndex(LAST_PRESENCE)),
+ cursor.getLong(cursor.getColumnIndex(LAST_TIME)));
+ final Jid jid;
+ try {
+ jid = Jid.fromString(cursor.getString(cursor.getColumnIndex(JID)), true);
+ } catch (final InvalidJidException e) {
+ // TODO: Borked DB... handle this somehow?
+ return null;
+ }
+ return new Contact(cursor.getString(cursor.getColumnIndex(ACCOUNT)),
+ cursor.getString(cursor.getColumnIndex(SYSTEMNAME)),
+ cursor.getString(cursor.getColumnIndex(SERVERNAME)),
+ jid,
+ cursor.getInt(cursor.getColumnIndex(OPTIONS)),
+ cursor.getString(cursor.getColumnIndex(PHOTOURI)),
+ cursor.getString(cursor.getColumnIndex(SYSTEMACCOUNT)),
+ cursor.getString(cursor.getColumnIndex(KEYS)),
+ cursor.getString(cursor.getColumnIndex(AVATAR)),
+ lastseen,
+ cursor.getString(cursor.getColumnIndex(GROUPS)));
+ }
+
+ public String getDisplayName() {
+ if (this.commonName != null && Config.X509_VERIFICATION) {
+ return this.commonName;
+ } else if (this.systemName != null) {
+ return this.systemName;
+ } else if (this.serverName != null) {
+ return this.serverName;
+ } else if (this.presenceName != null) {
+ return this.presenceName;
+ } else if (jid.hasLocalpart()) {
+ return jid.getLocalpart();
+ } else {
+ return jid.getDomainpart();
+ }
+ }
+
+ @Override
+ public String getDisplayJid() {
+ if (Config.LOCK_DOMAINS_IN_CONVERSATIONS && jid != null && jid.getDomainpart().equals(Config.DOMAIN_LOCK)) {
+ return jid.getLocalpart();
+ } else if (jid != null) {
+ return jid.toString();
+ } else {
+ return null;
+ }
+ }
+
+ public String getProfilePhoto() {
+ return this.photoUri;
+ }
+
+ public Jid getJid() {
+ return jid;
+ }
+
+ @Override
+ public List<Tag> getTags() {
+ final ArrayList<Tag> tags = new ArrayList<>();
+ for (final String group : getGroups()) {
+ tags.add(new Tag(group, UIHelper.getColorForName(group)));
+ }
+ switch (getMostAvailableStatus()) {
+ case CHAT:
+ case ONLINE:
+ tags.add(new Tag("online", 0xff259b24));
+ break;
+ case AWAY:
+ tags.add(new Tag("away", 0xffff9800));
+ break;
+ case XA:
+ tags.add(new Tag("not available", 0xfff44336));
+ break;
+ case DND:
+ tags.add(new Tag("dnd", 0xfff44336));
+ break;
+ }
+ if (isBlocked()) {
+ tags.add(new Tag("blocked", 0xff2e2f3b));
+ }
+ return tags;
+ }
+
+ @Override
+ public int getStatusColor() {
+ return UIHelper.getStatusColor(getMostAvailableStatus());
+ }
+
+ public boolean match(String needle) {
+ if (needle == null || needle.isEmpty()) {
+ return true;
+ }
+ needle = needle.toLowerCase(Locale.US);
+ String[] parts = needle.split("\\s+");
+ if (parts.length > 1) {
+ for(int i = 0; i < parts.length; ++i) {
+ if (!match(parts[i])) {
+ return false;
+ }
+ }
+ return true;
+ } else {
+ return jid.toString().contains(needle) ||
+ getDisplayName().toLowerCase(Locale.US).contains(needle) ||
+ matchInTag(needle);
+ }
+ }
+
+ private boolean matchInTag(String needle) {
+ needle = needle.toLowerCase(Locale.US);
+ for (Tag tag : getTags()) {
+ if (tag.getName().toLowerCase(Locale.US).contains(needle)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public ContentValues getContentValues() {
+ synchronized (this.keys) {
+ final ContentValues values = new ContentValues();
+ values.put(ACCOUNT, accountUuid);
+ values.put(SYSTEMNAME, systemName);
+ values.put(SERVERNAME, serverName);
+ values.put(JID, jid.toString());
+ values.put(OPTIONS, subscription);
+ values.put(SYSTEMACCOUNT, systemAccount);
+ values.put(PHOTOURI, photoUri);
+ values.put(KEYS, keys.toString());
+ values.put(AVATAR, avatar == null ? null : avatar.getFilename());
+ values.put(LAST_PRESENCE, lastseen.presence);
+ values.put(LAST_TIME, lastseen.time);
+ values.put(GROUPS, groups.toString());
+ return values;
+ }
+ }
+
+ public int getSubscription() {
+ return this.subscription;
+ }
+
+ public Account getAccount() {
+ return this.account;
+ }
+
+ public void setAccount(Account account) {
+ this.account = account;
+ this.accountUuid = account.getUuid();
+ }
+
+ public Presences getPresences() {
+ return this.presences;
+ }
+
+ public void updatePresence(final String resource, final Presence presence) {
+ this.presences.updatePresence(resource, presence);
+ }
+
+ public void removePresence(final String resource) {
+ this.presences.removePresence(resource);
+ }
+
+ public void clearPresences() {
+ this.presences.clearPresences();
+ this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
+ }
+
+ public Presence.Status getMostAvailableStatus() {
+ Presence p = this.presences.getMostAvailablePresence();
+ if (p == null) {
+ return Presence.Status.OFFLINE;
+ }
+
+ return p.getStatus();
+ }
+
+ public boolean setPhotoUri(String uri) {
+ if (uri != null && !uri.equals(this.photoUri)) {
+ this.photoUri = uri;
+ return true;
+ } else if (this.photoUri != null && uri == null) {
+ this.photoUri = null;
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public void setServerName(String serverName) {
+ this.serverName = serverName;
+ }
+
+ public void setSystemName(String systemName) {
+ this.systemName = systemName;
+ }
+
+ public void setPresenceName(String presenceName) {
+ this.presenceName = presenceName;
+ }
+
+ public String getSystemAccount() {
+ return systemAccount;
+ }
+
+ public void setSystemAccount(String account) {
+ this.systemAccount = account;
+ }
+
+ public List<String> getGroups() {
+ ArrayList<String> groups = new ArrayList<String>();
+ for (int i = 0; i < this.groups.length(); ++i) {
+ try {
+ groups.add(this.groups.getString(i));
+ } catch (final JSONException ignored) {
+ }
+ }
+ return groups;
+ }
+
+ public ArrayList<String> getOtrFingerprints() {
+ synchronized (this.keys) {
+ final ArrayList<String> fingerprints = new ArrayList<String>();
+ try {
+ if (this.keys.has("otr_fingerprints")) {
+ final JSONArray prints = this.keys.getJSONArray("otr_fingerprints");
+ for (int i = 0; i < prints.length(); ++i) {
+ final String print = prints.isNull(i) ? null : prints.getString(i);
+ if (print != null && !print.isEmpty()) {
+ fingerprints.add(prints.getString(i));
+ }
+ }
+ }
+ } catch (final JSONException ignored) {
+
+ }
+ return fingerprints;
+ }
+ }
+ public boolean addOtrFingerprint(String print) {
+ synchronized (this.keys) {
+ if (getOtrFingerprints().contains(print)) {
+ return false;
+ }
+ try {
+ JSONArray fingerprints;
+ if (!this.keys.has("otr_fingerprints")) {
+ fingerprints = new JSONArray();
+ } else {
+ fingerprints = this.keys.getJSONArray("otr_fingerprints");
+ }
+ fingerprints.put(print);
+ this.keys.put("otr_fingerprints", fingerprints);
+ return true;
+ } catch (final JSONException ignored) {
+ return false;
+ }
+ }
+ }
+
+ public long getPgpKeyId() {
+ synchronized (this.keys) {
+ if (this.keys.has("pgp_keyid")) {
+ try {
+ return this.keys.getLong("pgp_keyid");
+ } catch (JSONException e) {
+ return 0;
+ }
+ } else {
+ return 0;
+ }
+ }
+ }
+
+ public void setPgpKeyId(long keyId) {
+ synchronized (this.keys) {
+ try {
+ this.keys.put("pgp_keyid", keyId);
+ } catch (final JSONException ignored) {
+ }
+ }
+ }
+
+ public void setOption(int option) {
+ this.subscription |= 1 << option;
+ }
+
+ public void resetOption(int option) {
+ this.subscription &= ~(1 << option);
+ }
+
+ public boolean getOption(int option) {
+ return ((this.subscription & (1 << option)) != 0);
+ }
+
+ public boolean showInRoster() {
+ return (this.getOption(Contact.Options.IN_ROSTER) && (!this
+ .getOption(Contact.Options.DIRTY_DELETE)))
+ || (this.getOption(Contact.Options.DIRTY_PUSH));
+ }
+
+ public void parseSubscriptionFromElement(Element item) {
+ String ask = item.getAttribute("ask");
+ String subscription = item.getAttribute("subscription");
+
+ if (subscription != null) {
+ switch (subscription) {
+ case "to":
+ this.resetOption(Options.FROM);
+ this.setOption(Options.TO);
+ break;
+ case "from":
+ this.resetOption(Options.TO);
+ this.setOption(Options.FROM);
+ this.resetOption(Options.PREEMPTIVE_GRANT);
+ this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
+ break;
+ case "both":
+ this.setOption(Options.TO);
+ this.setOption(Options.FROM);
+ this.resetOption(Options.PREEMPTIVE_GRANT);
+ this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
+ break;
+ case "none":
+ this.resetOption(Options.FROM);
+ this.resetOption(Options.TO);
+ break;
+ }
+ }
+
+ // do NOT override asking if pending push request
+ if (!this.getOption(Contact.Options.DIRTY_PUSH)) {
+ if ((ask != null) && (ask.equals("subscribe"))) {
+ this.setOption(Contact.Options.ASKING);
+ } else {
+ this.resetOption(Contact.Options.ASKING);
+ }
+ }
+ }
+
+ public void parseGroupsFromElement(Element item) {
+ this.groups = new JSONArray();
+ for (Element element : item.getChildren()) {
+ if (element.getName().equals("group") && element.getContent() != null) {
+ this.groups.put(element.getContent());
+ }
+ }
+ }
+
+ public Element asElement() {
+ final Element item = new Element("item");
+ item.setAttribute("jid", this.jid.toString());
+ if (this.serverName != null) {
+ item.setAttribute("name", this.serverName);
+ }
+ for (String group : getGroups()) {
+ item.addChild("group").setContent(group);
+ }
+ return item;
+ }
+
+ @Override
+ public int compareTo(final ListItem another) {
+ return this.getDisplayName().compareToIgnoreCase(
+ another.getDisplayName());
+ }
+
+ public Jid getServer() {
+ return getJid().toDomainJid();
+ }
+
+ public boolean setAvatar(Avatar avatar) {
+ if (this.avatar != null && this.avatar.equals(avatar)) {
+ return false;
+ } else {
+ if (this.avatar != null && this.avatar.origin == Avatar.Origin.PEP && avatar.origin == Avatar.Origin.VCARD) {
+ return false;
+ }
+ this.avatar = avatar;
+ return true;
+ }
+ }
+
+ public String getAvatar() {
+ return avatar == null ? null : avatar.getFilename();
+ }
+
+ public boolean deleteOtrFingerprint(String fingerprint) {
+ synchronized (this.keys) {
+ boolean success = false;
+ try {
+ if (this.keys.has("otr_fingerprints")) {
+ JSONArray newPrints = new JSONArray();
+ JSONArray oldPrints = this.keys
+ .getJSONArray("otr_fingerprints");
+ for (int i = 0; i < oldPrints.length(); ++i) {
+ if (!oldPrints.getString(i).equals(fingerprint)) {
+ newPrints.put(oldPrints.getString(i));
+ } else {
+ success = true;
+ }
+ }
+ this.keys.put("otr_fingerprints", newPrints);
+ }
+ return success;
+ } catch (JSONException e) {
+ return false;
+ }
+ }
+ }
+
+ public boolean trusted() {
+ return getOption(Options.FROM) && getOption(Options.TO);
+ }
+
+ public String getShareableUri() {
+ if (getOtrFingerprints().size() >= 1) {
+ String otr = getOtrFingerprints().get(0);
+ return "xmpp:" + getJid().toBareJid().toString() + "?otr-fingerprint=" + otr;
+ } else {
+ return "xmpp:" + getJid().toBareJid().toString();
+ }
+ }
+
+ @Override
+ public boolean isBlocked() {
+ return getAccount().isBlocked(this);
+ }
+
+ @Override
+ public boolean isDomainBlocked() {
+ return getAccount().isBlocked(this.getJid().toDomainJid());
+ }
+
+ @Override
+ public Jid getBlockedJid() {
+ if (isDomainBlocked()) {
+ return getJid().toDomainJid();
+ } else {
+ return getJid();
+ }
+ }
+
+ public boolean isSelf() {
+ return account.getJid().toBareJid().equals(getJid().toBareJid());
+ }
+
+ public void setCommonName(String cn) {
+ this.commonName = cn;
+ }
+
+ public static class Lastseen {
+ public long time;
+ public String presence;
+
+ public Lastseen() {
+ this(null, 0);
+ }
+
+ public Lastseen(final String presence, final long time) {
+ this.presence = presence;
+ this.time = time;
+ }
+ }
+
+ public final class Options {
+ public static final int TO = 0;
+ public static final int FROM = 1;
+ public static final int ASKING = 2;
+ public static final int PREEMPTIVE_GRANT = 3;
+ public static final int IN_ROSTER = 4;
+ public static final int PENDING_SUBSCRIPTION_REQUEST = 5;
+ public static final int DIRTY_PUSH = 6;
+ public static final int DIRTY_DELETE = 7;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java
new file mode 100644
index 00000000..299c5f7d
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java
@@ -0,0 +1,947 @@
+package eu.siacs.conversations.entities;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+
+import net.java.otr4j.OtrException;
+import net.java.otr4j.crypto.OtrCryptoException;
+import net.java.otr4j.session.SessionID;
+import net.java.otr4j.session.SessionImpl;
+import net.java.otr4j.session.SessionStatus;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.security.interfaces.DSAPublicKey;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+
+import de.thedevstack.conversationsplus.ConversationsPlusPreferences;
+import de.thedevstack.conversationsplus.utils.MessageUtil;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.crypto.axolotl.AxolotlService;
+import eu.siacs.conversations.xmpp.chatstate.ChatState;
+import eu.siacs.conversations.xmpp.jid.InvalidJidException;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+public class Conversation extends AbstractEntity implements Blockable {
+ public static final String TABLENAME = "conversations";
+
+ public static final int STATUS_AVAILABLE = 0;
+ public static final int STATUS_ARCHIVED = 1;
+ public static final int STATUS_DELETED = 2;
+
+ public static final int MODE_MULTI = 1;
+ public static final int MODE_SINGLE = 0;
+
+ public static final String NAME = "name";
+ public static final String ACCOUNT = "accountUuid";
+ public static final String CONTACT = "contactUuid";
+ public static final String CONTACTJID = "contactJid";
+ public static final String STATUS = "status";
+ public static final String CREATED = "created";
+ public static final String MODE = "mode";
+ public static final String ATTRIBUTES = "attributes";
+
+ public static final String ATTRIBUTE_NEXT_ENCRYPTION = "next_encryption";
+ public static final String ATTRIBUTE_MUC_PASSWORD = "muc_password";
+ public static final String ATTRIBUTE_MUTED_TILL = "muted_till";
+ public static final String ATTRIBUTE_ALWAYS_NOTIFY = "always_notify";
+ public static final String ATTRIBUTE_CRYPTO_TARGETS = "crypto_targets";
+
+ private String name;
+ private String contactUuid;
+ private String accountUuid;
+ private Jid contactJid;
+ private int status;
+ private long created;
+ private int mode;
+
+ private JSONObject attributes = new JSONObject();
+
+ private Jid nextCounterpart;
+
+ protected final ArrayList<Message> messages = new ArrayList<>();
+ protected Account account = null;
+
+ private transient SessionImpl otrSession;
+
+ private transient String otrFingerprint = null;
+ private Smp mSmp = new Smp();
+
+ private String nextMessage;
+
+ private transient MucOptions mucOptions = null;
+
+ private byte[] symmetricKey;
+
+ private Bookmark bookmark;
+
+ private boolean messagesLeftOnServer = true;
+ private ChatState mOutgoingChatState = Config.DEFAULT_CHATSTATE;
+ private ChatState mIncomingChatState = Config.DEFAULT_CHATSTATE;
+ private String mLastReceivedOtrMessageId = null;
+ private String mFirstMamReference = null;
+ private Message correctingMessage;
+
+ public boolean hasMessagesLeftOnServer() {
+ return messagesLeftOnServer;
+ }
+
+ public void setHasMessagesLeftOnServer(boolean value) {
+ this.messagesLeftOnServer = value;
+ }
+
+ public Message findUnsentMessageWithUuid(String uuid) {
+ synchronized(this.messages) {
+ for (final Message message : this.messages) {
+ final int s = message.getStatus();
+ if ((s == Message.STATUS_UNSEND || s == Message.STATUS_WAITING) && message.getUuid().equals(uuid)) {
+ return message;
+ }
+ }
+ }
+ return null;
+ }
+
+ public void findWaitingMessages(OnMessageFound onMessageFound) {
+ synchronized (this.messages) {
+ for(Message message : this.messages) {
+ if (message.getStatus() == Message.STATUS_WAITING) {
+ onMessageFound.onMessageFound(message);
+ }
+ }
+ }
+ }
+
+ public void findUnreadMessages(OnMessageFound onMessageFound) {
+ synchronized (this.messages) {
+ for(Message message : this.messages) {
+ if (!message.isRead()) {
+ onMessageFound.onMessageFound(message);
+ }
+ }
+ }
+ }
+
+ public void findMessagesWithFiles(final OnMessageFound onMessageFound) {
+ synchronized (this.messages) {
+ for (final Message message : this.messages) {
+ if ((message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE)
+ && message.getEncryption() != Message.ENCRYPTION_PGP) {
+ onMessageFound.onMessageFound(message);
+ }
+ }
+ }
+ }
+
+ public Message findMessageWithFileAndUuid(final String uuid) {
+ // TODO Implement this method to find a message by a real filename - not uuid
+ synchronized (this.messages) {
+ for (final Message message : this.messages) {
+ if ((message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE)
+ && message.getEncryption() != Message.ENCRYPTION_PGP
+ && message.getUuid().equals(uuid)) {
+ return message;
+ }
+ }
+ }
+ return null;
+ }
+
+ public void clearMessages() {
+ synchronized (this.messages) {
+ this.messages.clear();
+ }
+ }
+
+ public boolean setIncomingChatState(ChatState state) {
+ if (this.mIncomingChatState == state) {
+ return false;
+ }
+ this.mIncomingChatState = state;
+ return true;
+ }
+
+ public ChatState getIncomingChatState() {
+ return this.mIncomingChatState;
+ }
+
+ public boolean setOutgoingChatState(ChatState state) {
+ if (mode == MODE_MULTI) {
+ return false;
+ }
+ if (this.mOutgoingChatState != state) {
+ this.mOutgoingChatState = state;
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public ChatState getOutgoingChatState() {
+ return this.mOutgoingChatState;
+ }
+
+ public void trim() {
+ synchronized (this.messages) {
+ final int size = messages.size();
+ final int maxsize = Config.PAGE_SIZE * Config.MAX_NUM_PAGES;
+ if (size > maxsize) {
+ this.messages.subList(0, size - maxsize).clear();
+ }
+ }
+ }
+
+ public void findUnsentMessagesWithEncryption(int encryptionType, OnMessageFound onMessageFound) {
+ synchronized (this.messages) {
+ for (Message message : this.messages) {
+ if ((message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_WAITING)
+ && (message.getEncryption() == encryptionType)) {
+ onMessageFound.onMessageFound(message);
+ }
+ }
+ }
+ }
+
+ public void findUnsentTextMessages(OnMessageFound onMessageFound) {
+ synchronized (this.messages) {
+ for (Message message : this.messages) {
+ if (message.getType() != Message.TYPE_IMAGE
+ && message.getStatus() == Message.STATUS_UNSEND) {
+ onMessageFound.onMessageFound(message);
+ }
+ }
+ }
+ }
+
+ public Message findSentMessageWithUuidOrRemoteId(String id) {
+ synchronized (this.messages) {
+ for (Message message : this.messages) {
+ if (id.equals(message.getUuid())
+ || (message.getStatus() >= Message.STATUS_SEND
+ && id.equals(message.getRemoteMsgId()))) {
+ return message;
+ }
+ }
+ }
+ return null;
+ }
+
+ public Message findMessageWithRemoteIdAndCounterpart(String id, Jid counterpart, boolean received, boolean carbon) {
+ synchronized (this.messages) {
+ for(int i = this.messages.size() - 1; i >= 0; --i) {
+ Message message = messages.get(i);
+ if (counterpart.equals(message.getCounterpart())
+ && ((message.getStatus() == Message.STATUS_RECEIVED) == received)
+ && (carbon == message.isCarbon() || received) ) {
+ if (id.equals(message.getRemoteMsgId())) {
+ return message;
+ } else {
+ return null;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ public Message findSentMessageWithUuid(String id) {
+ synchronized (this.messages) {
+ for (Message message : this.messages) {
+ if (id.equals(message.getUuid())) {
+ return message;
+ }
+ }
+ }
+ return null;
+ }
+
+ // TODO Check if this is really necessary
+ public void populateWithMessages(final List<Message> messages) {
+ synchronized (this.messages) {
+ messages.clear();
+ messages.addAll(this.messages);
+ }
+ }
+
+ @Override
+ public boolean isBlocked() {
+ return getContact().isBlocked();
+ }
+
+ @Override
+ public boolean isDomainBlocked() {
+ return getContact().isDomainBlocked();
+ }
+
+ @Override
+ public Jid getBlockedJid() {
+ return getContact().getBlockedJid();
+ }
+
+ public String getLastReceivedOtrMessageId() {
+ return this.mLastReceivedOtrMessageId;
+ }
+
+ public void setLastReceivedOtrMessageId(String id) {
+ this.mLastReceivedOtrMessageId = id;
+ }
+
+ public int countMessages() {
+ synchronized (this.messages) {
+ return this.messages.size();
+ }
+ }
+
+ public void setFirstMamReference(String reference) {
+ this.mFirstMamReference = reference;
+ }
+
+ public String getFirstMamReference() {
+ return this.mFirstMamReference;
+ }
+
+ public List<Jid> getAcceptedCryptoTargets() {
+ if (mode == MODE_SINGLE) {
+ return Arrays.asList(getJid().toBareJid());
+ } else {
+ return getJidListAttribute(ATTRIBUTE_CRYPTO_TARGETS);
+ }
+ }
+
+ public void setAcceptedCryptoTargets(List<Jid> acceptedTargets) {
+ setAttribute(ATTRIBUTE_CRYPTO_TARGETS, acceptedTargets);
+ }
+
+ public interface OnMessageFound {
+ void onMessageFound(final Message message);
+ }
+
+ public Conversation(final String name, final Account account, final Jid contactJid,
+ final int mode) {
+ this(java.util.UUID.randomUUID().toString(), name, null, account
+ .getUuid(), contactJid, System.currentTimeMillis(),
+ STATUS_AVAILABLE, mode, "");
+ this.account = account;
+ }
+
+ public Conversation(final String uuid, final String name, final String contactUuid,
+ final String accountUuid, final Jid contactJid, final long created, final int status,
+ final int mode, final String attributes) {
+ this.uuid = uuid;
+ this.name = name;
+ this.contactUuid = contactUuid;
+ this.accountUuid = accountUuid;
+ this.contactJid = contactJid;
+ this.created = created;
+ this.status = status;
+ this.mode = mode;
+ try {
+ this.attributes = new JSONObject(attributes == null ? "" : attributes);
+ } catch (JSONException e) {
+ this.attributes = new JSONObject();
+ }
+ }
+
+ public boolean isRead() {
+ return (this.messages.size() == 0) || this.messages.get(this.messages.size() - 1).isRead();
+ }
+
+ public List<Message> markRead() {
+ final List<Message> unread = new ArrayList<>();
+ synchronized (this.messages) {
+ for(Message message : this.messages) {
+ if (!message.isRead()) {
+ message.markRead();
+ unread.add(message);
+ }
+ }
+ }
+ return unread;
+ }
+
+ public Message getLatestMarkableMessage() {
+ for (int i = this.messages.size() - 1; i >= 0; --i) {
+ if (this.messages.get(i).getStatus() <= Message.STATUS_RECEIVED
+ && this.messages.get(i).markable) {
+ if (this.messages.get(i).isRead()) {
+ return null;
+ } else {
+ return this.messages.get(i);
+ }
+ }
+ }
+ return null;
+ }
+
+ public Message getLatestMessage() {
+ if (this.messages.size() == 0) {
+ Message message = new Message(this, "", Message.ENCRYPTION_NONE);
+ message.setTime(getCreated());
+ return message;
+ } else {
+ Message message = this.messages.get(this.messages.size() - 1);
+ message.setConversation(this);
+ return message;
+ }
+ }
+
+ public String getName() {
+ if (getMode() == MODE_MULTI) {
+ if (getMucOptions().getSubject() != null) {
+ return getMucOptions().getSubject();
+ } else if (bookmark != null && bookmark.getBookmarkName() != null) {
+ return bookmark.getBookmarkName();
+ } else {
+ String generatedName = getMucOptions().createNameFromParticipants();
+ if (generatedName != null) {
+ return generatedName;
+ } else {
+ return getJid().getLocalpart();
+ }
+ }
+ } else {
+ return this.getContact().getDisplayName();
+ }
+ }
+
+ public String getAccountUuid() {
+ return this.accountUuid;
+ }
+
+ public Account getAccount() {
+ return this.account;
+ }
+
+ public Contact getContact() {
+ return this.account.getRoster().getContact(this.contactJid);
+ }
+
+ public void setAccount(final Account account) {
+ this.account = account;
+ }
+
+ @Override
+ public Jid getJid() {
+ return this.contactJid;
+ }
+
+ public int getStatus() {
+ return this.status;
+ }
+
+ public long getCreated() {
+ return this.created;
+ }
+
+ public ContentValues getContentValues() {
+ ContentValues values = new ContentValues();
+ values.put(UUID, uuid);
+ values.put(NAME, name);
+ values.put(CONTACT, contactUuid);
+ values.put(ACCOUNT, accountUuid);
+ values.put(CONTACTJID, contactJid.toString());
+ values.put(CREATED, created);
+ values.put(STATUS, status);
+ values.put(MODE, mode);
+ values.put(ATTRIBUTES, attributes.toString());
+ return values;
+ }
+
+ public static Conversation fromCursor(Cursor cursor) {
+ Jid jid;
+ try {
+ jid = Jid.fromString(cursor.getString(cursor.getColumnIndex(CONTACTJID)), true);
+ } catch (final InvalidJidException e) {
+ // Borked DB..
+ jid = null;
+ }
+ return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)),
+ cursor.getString(cursor.getColumnIndex(NAME)),
+ cursor.getString(cursor.getColumnIndex(CONTACT)),
+ cursor.getString(cursor.getColumnIndex(ACCOUNT)),
+ jid,
+ cursor.getLong(cursor.getColumnIndex(CREATED)),
+ cursor.getInt(cursor.getColumnIndex(STATUS)),
+ cursor.getInt(cursor.getColumnIndex(MODE)),
+ cursor.getString(cursor.getColumnIndex(ATTRIBUTES)));
+ }
+
+ public void setStatus(int status) {
+ this.status = status;
+ }
+
+ public int getMode() {
+ return this.mode;
+ }
+
+ public void setMode(int mode) {
+ this.mode = mode;
+ }
+
+ public SessionImpl startOtrSession(String presence, boolean sendStart) {
+ if (this.otrSession != null) {
+ return this.otrSession;
+ } else {
+ final SessionID sessionId = new SessionID(this.getJid().toBareJid().toString(),
+ presence,
+ "xmpp");
+ this.otrSession = new SessionImpl(sessionId, getAccount().getOtrService());
+ try {
+ if (sendStart) {
+ this.otrSession.startSession();
+ return this.otrSession;
+ }
+ return this.otrSession;
+ } catch (OtrException e) {
+ return null;
+ }
+ }
+
+ }
+
+ public SessionImpl getOtrSession() {
+ return this.otrSession;
+ }
+
+ public void resetOtrSession() {
+ this.otrFingerprint = null;
+ this.otrSession = null;
+ this.mSmp.hint = null;
+ this.mSmp.secret = null;
+ this.mSmp.status = Smp.STATUS_NONE;
+ }
+
+ public Smp smp() {
+ return mSmp;
+ }
+
+ public boolean startOtrIfNeeded() {
+ if (this.otrSession != null && this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) {
+ try {
+ this.otrSession.startSession();
+ return true;
+ } catch (OtrException e) {
+ this.resetOtrSession();
+ return false;
+ }
+ } else {
+ return true;
+ }
+ }
+
+ public boolean endOtrIfNeeded() {
+ if (this.otrSession != null) {
+ if (this.otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) {
+ try {
+ this.otrSession.endSession();
+ this.resetOtrSession();
+ return true;
+ } catch (OtrException e) {
+ this.resetOtrSession();
+ return false;
+ }
+ } else {
+ this.resetOtrSession();
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ public boolean hasValidOtrSession() {
+ return this.otrSession != null;
+ }
+
+ public synchronized String getOtrFingerprint() {
+ if (this.otrFingerprint == null) {
+ try {
+ if (getOtrSession() == null || getOtrSession().getSessionStatus() != SessionStatus.ENCRYPTED) {
+ return null;
+ }
+ DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession().getRemotePublicKey();
+ this.otrFingerprint = getAccount().getOtrService().getFingerprint(remotePubKey);
+ } catch (final OtrCryptoException | UnsupportedOperationException ignored) {
+ return null;
+ }
+ }
+ return this.otrFingerprint;
+ }
+
+ public boolean verifyOtrFingerprint() {
+ final String fingerprint = getOtrFingerprint();
+ if (fingerprint != null) {
+ getContact().addOtrFingerprint(fingerprint);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public boolean isOtrFingerprintVerified() {
+ return getContact().getOtrFingerprints().contains(getOtrFingerprint());
+ }
+
+ /**
+ * short for is Private and Non-anonymous
+ */
+ private boolean isPnNA() {
+ return mode == MODE_SINGLE || (getMucOptions().membersOnly() && getMucOptions().nonanonymous());
+ }
+
+ public synchronized MucOptions getMucOptions() {
+ if (this.mucOptions == null) {
+ this.mucOptions = new MucOptions(this);
+ }
+ return this.mucOptions;
+ }
+
+ public void resetMucOptions() {
+ this.mucOptions = null;
+ }
+
+ public void setContactJid(final Jid jid) {
+ this.contactJid = jid;
+ }
+
+ public void setNextCounterpart(Jid jid) {
+ this.nextCounterpart = jid;
+ }
+
+ public Jid getNextCounterpart() {
+ return this.nextCounterpart;
+ }
+
+ private int getMostRecentlyUsedOutgoingEncryption() {
+ synchronized (this.messages) {
+ for(int i = this.messages.size() -1; i >= 0; --i) {
+ final Message m = this.messages.get(i);
+ if (!m.isCarbon() && 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;
+ }
+
+ 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() {
+ final AxolotlService axolotlService = getAccount().getAxolotlService();
+ int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1);
+ if (next == -1) {
+ if (Config.X509_VERIFICATION) {
+ if (axolotlService != null && axolotlService.isConversationAxolotlCapable(this)) {
+ return Message.ENCRYPTION_AXOLOTL;
+ } else {
+ return Message.ENCRYPTION_NONE;
+ }
+ }
+ int outgoing = this.getMostRecentlyUsedOutgoingEncryption();
+ if (outgoing == Message.ENCRYPTION_NONE) {
+ next = this.getMostRecentlyUsedIncomingEncryption();
+ } else {
+ next = outgoing;
+ }
+ }
+
+ if (!Config.supportUnencrypted() && next <= 0) {
+ if (Config.supportOmemo()
+ && (axolotlService != null && axolotlService.isConversationAxolotlCapable(this) || !Config.multipleEncryptionChoices())) {
+ return Message.ENCRYPTION_AXOLOTL;
+ } else if (Config.supportOtr() && mode == MODE_SINGLE) {
+ return Message.ENCRYPTION_OTR;
+ } else if (Config.supportOpenPgp()
+ && (mode == MODE_SINGLE) || !Config.multipleEncryptionChoices()) {
+ return Message.ENCRYPTION_PGP;
+ }
+ } else if (next == Message.ENCRYPTION_AXOLOTL
+ && (!Config.supportOmemo() || axolotlService == null || !axolotlService.isConversationAxolotlCapable(this))) {
+ next = Message.ENCRYPTION_NONE;
+ }
+ return next;
+ }
+
+ public void setNextEncryption(int encryption) {
+ this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, String.valueOf(encryption));
+ }
+
+ public String getNextMessage() {
+ if (this.nextMessage == null) {
+ return "";
+ } else {
+ return this.nextMessage;
+ }
+ }
+
+ public boolean smpRequested() {
+ return smp().status == Smp.STATUS_CONTACT_REQUESTED;
+ }
+
+ public void setNextMessage(String message) {
+ this.nextMessage = message;
+ }
+
+ public void setSymmetricKey(byte[] key) {
+ this.symmetricKey = key;
+ }
+
+ public byte[] getSymmetricKey() {
+ return this.symmetricKey;
+ }
+
+ public void setBookmark(Bookmark bookmark) {
+ this.bookmark = bookmark;
+ this.bookmark.setConversation(this);
+ }
+
+ public void deregisterWithBookmark() {
+ if (this.bookmark != null) {
+ this.bookmark.setConversation(null);
+ }
+ }
+
+ public Bookmark getBookmark() {
+ return this.bookmark;
+ }
+
+ public boolean hasDuplicateMessage(Message message) {
+ synchronized (this.messages) {
+ for (int i = this.messages.size() - 1; i >= 0; --i) {
+ if (this.messages.get(i).equals(message)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ public Message findSentMessageWithBody(String body) {
+ synchronized (this.messages) {
+ for (int i = this.messages.size() - 1; i >= 0; --i) {
+ Message message = this.messages.get(i);
+ if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) {
+ String otherBody = message.getBody();
+ if (otherBody != null && otherBody.equals(body)) {
+ return message;
+ }
+ }
+ }
+ return null;
+ }
+ }
+
+ public long getLastMessageTransmitted() {
+ synchronized (this.messages) {
+ for(int i = this.messages.size() - 1; i >= 0; --i) {
+ Message message = this.messages.get(i);
+ if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon()) {
+ return message.getTimeSent();
+ }
+ }
+ }
+ return 0;
+ }
+
+ public void setMutedTill(long value) {
+ this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
+ }
+
+ public boolean isMuted() {
+ return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
+ }
+
+ public boolean alwaysNotify() {
+ return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, true);
+ }
+
+ public boolean setAttribute(String key, String value) {
+ synchronized (this.attributes) {
+ try {
+ this.attributes.put(key, value);
+ return true;
+ } catch (JSONException e) {
+ return false;
+ }
+ }
+ }
+
+ public boolean setAttribute(String key, List<Jid> jids) {
+ JSONArray array = new JSONArray();
+ for(Jid jid : jids) {
+ array.put(jid.toBareJid().toString());
+ }
+ synchronized (this.attributes) {
+ try {
+ this.attributes.put(key, array);
+ return true;
+ } catch (JSONException e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+ }
+
+ public String getAttribute(String key) {
+ synchronized (this.attributes) {
+ try {
+ return this.attributes.getString(key);
+ } catch (JSONException e) {
+ return null;
+ }
+ }
+ }
+
+ public List<Jid> getJidListAttribute(String key) {
+ ArrayList<Jid> list = new ArrayList<>();
+ synchronized (this.attributes) {
+ try {
+ JSONArray array = this.attributes.getJSONArray(key);
+ for (int i = 0; i < array.length(); ++i) {
+ try {
+ list.add(Jid.fromString(array.getString(i)));
+ } catch (InvalidJidException e) {
+ //ignored
+ }
+ }
+ } catch (JSONException e) {
+ //ignored
+ }
+ }
+ return list;
+ }
+
+ public int getIntAttribute(String key, int defaultValue) {
+ String value = this.getAttribute(key);
+ if (value == null) {
+ return defaultValue;
+ } else {
+ try {
+ return Integer.parseInt(value);
+ } catch (NumberFormatException e) {
+ return defaultValue;
+ }
+ }
+ }
+
+ public long getLongAttribute(String key, long defaultValue) {
+ String value = this.getAttribute(key);
+ if (value == null) {
+ return defaultValue;
+ } else {
+ try {
+ return Long.parseLong(value);
+ } catch (NumberFormatException e) {
+ return defaultValue;
+ }
+ }
+ }
+
+ public boolean getBooleanAttribute(String key, boolean defaultValue) {
+ String value = this.getAttribute(key);
+ if (value == null) {
+ return defaultValue;
+ } else {
+ return Boolean.parseBoolean(value);
+ }
+ }
+
+ public void add(Message message) {
+ message.setConversation(this);
+ synchronized (this.messages) {
+ this.messages.add(message);
+ }
+ }
+
+ public void prepend(Message message) {
+ message.setConversation(this);
+ synchronized (this.messages) {
+ this.messages.add(0,message);
+ }
+ }
+
+ public void addAll(int index, List<Message> messages) {
+ synchronized (this.messages) {
+ this.messages.addAll(index, messages);
+ }
+ account.getPgpDecryptionService().addAll(messages);
+ }
+
+ public void sort() {
+ synchronized (this.messages) {
+ Collections.sort(this.messages, new Comparator<Message>() {
+ @Override
+ public int compare(Message left, Message right) {
+ if (left.getTimeSent() < right.getTimeSent()) {
+ return -1;
+ } else if (left.getTimeSent() > right.getTimeSent()) {
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+ });
+ for(Message message : this.messages) {
+ message.untie();
+ }
+ }
+ }
+
+ public int unreadCount() {
+ if (getLongAttribute(Conversation.ATTRIBUTE_MUTED_TILL,0) == Long.MAX_VALUE) {
+ return 0;
+ }
+ synchronized (this.messages) {
+ int count = 0;
+ for(int i = this.messages.size() - 1; i >= 0; --i) {
+ Message message = this.messages.get(i);
+ if (message.isRead()) {
+ return count;
+ }
+ if (alwaysNotify() || MessageUtil.wasHighlightedOrPrivate(message)) {
+ ++count;
+ }
+ }
+ return count;
+ }
+ }
+
+ public class Smp {
+ public static final int STATUS_NONE = 0;
+ public static final int STATUS_CONTACT_REQUESTED = 1;
+ public static final int STATUS_WE_REQUESTED = 2;
+ public static final int STATUS_FAILED = 3;
+ public static final int STATUS_VERIFIED = 4;
+
+ public String secret = null;
+ public String hint = null;
+ public int status = 0;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java b/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java
new file mode 100644
index 00000000..4e63a66a
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java
@@ -0,0 +1,83 @@
+package eu.siacs.conversations.entities;
+
+import java.io.File;
+
+import eu.siacs.conversations.utils.MimeUtils;
+
+public class DownloadableFile extends File {
+
+ private static final long serialVersionUID = 2247012619505115863L;
+
+ private long expectedSize = 0;
+ private String sha1sum;
+ private byte[] aeskey;
+
+ private byte[] iv = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
+ 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0xf };
+
+ public DownloadableFile(String path) {
+ super(path);
+ }
+
+ public long getSize() {
+ return super.length();
+ }
+
+ public long getExpectedSize() {
+ return this.expectedSize;
+ }
+
+ public String getMimeType() {
+ String path = this.getAbsolutePath();
+ int start = path.lastIndexOf('.') + 1;
+ if (start < path.length()) {
+ String mime = MimeUtils.guessMimeTypeFromExtension(path.substring(start));
+ return mime == null ? "" : mime;
+ } else {
+ return "";
+ }
+ }
+
+ public void setExpectedSize(long size) {
+ this.expectedSize = size;
+ }
+
+ public String getSha1Sum() {
+ return this.sha1sum;
+ }
+
+ public void setSha1Sum(String sum) {
+ this.sha1sum = sum;
+ }
+
+ public void setKeyAndIv(byte[] keyIvCombo) {
+ if (keyIvCombo.length == 48) {
+ this.aeskey = new byte[32];
+ this.iv = new byte[16];
+ System.arraycopy(keyIvCombo, 0, this.iv, 0, 16);
+ System.arraycopy(keyIvCombo, 16, this.aeskey, 0, 32);
+ } else if (keyIvCombo.length >= 32) {
+ this.aeskey = new byte[32];
+ System.arraycopy(keyIvCombo, 0, aeskey, 0, 32);
+ } else if (keyIvCombo.length >= 16) {
+ this.aeskey = new byte[16];
+ System.arraycopy(keyIvCombo, 0, this.aeskey, 0, 16);
+ }
+ }
+
+ public void setKey(byte[] key) {
+ this.aeskey = key;
+ }
+
+ public void setIv(byte[] iv) {
+ this.iv = iv;
+ }
+
+ public byte[] getKey() {
+ return this.aeskey;
+ }
+
+ public byte[] getIv() {
+ return this.iv;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/entities/ListItem.java b/src/main/java/eu/siacs/conversations/entities/ListItem.java
new file mode 100644
index 00000000..56804fbf
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/entities/ListItem.java
@@ -0,0 +1,37 @@
+package eu.siacs.conversations.entities;
+
+import java.util.List;
+
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+public interface ListItem extends Comparable<ListItem> {
+ String getDisplayName();
+
+ String getDisplayJid();
+
+ Jid getJid();
+
+ public int getStatusColor();
+
+ List<Tag> getTags();
+
+ final class Tag {
+ private final String name;
+ private final int color;
+
+ public Tag(final String name, final int color) {
+ this.name = name;
+ this.color = color;
+ }
+
+ public int getColor() {
+ return this.color;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+ }
+
+ boolean match(final String needle);
+}
diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java
new file mode 100644
index 00000000..f524fc58
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/entities/Message.java
@@ -0,0 +1,617 @@
+package eu.siacs.conversations.entities;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import de.thedevstack.conversationsplus.entities.FileParams;
+import de.thedevstack.conversationsplus.enums.FileStatus;
+
+import de.thedevstack.conversationsplus.utils.MessageUtil;
+import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
+import eu.siacs.conversations.utils.FileUtils;
+import eu.siacs.conversations.utils.MimeUtils;
+import eu.siacs.conversations.xmpp.jid.InvalidJidException;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+public class Message extends AbstractEntity {
+
+ public static final String TABLENAME = "messages";
+
+ public static final String MERGE_SEPARATOR = " \u200B\n\n";
+
+ public static final int STATUS_RECEIVED = 0;
+ public static final int STATUS_UNSEND = 1;
+ public static final int STATUS_SEND = 2;
+ public static final int STATUS_SEND_FAILED = 3;
+ public static final int STATUS_WAITING = 5;
+ public static final int STATUS_OFFERED = 6;
+ public static final int STATUS_SEND_RECEIVED = 7;
+ public static final int STATUS_SEND_DISPLAYED = 8;
+
+ public static final int ENCRYPTION_NONE = 0;
+ public static final int ENCRYPTION_PGP = 1;
+ public static final int ENCRYPTION_OTR = 2;
+ public static final int ENCRYPTION_DECRYPTED = 3;
+ public static final int ENCRYPTION_DECRYPTION_FAILED = 4;
+ public static final int ENCRYPTION_AXOLOTL = 5;
+
+ public static final int TYPE_TEXT = 0;
+ public static final int TYPE_IMAGE = 1;
+ public static final int TYPE_FILE = 2;
+ public static final int TYPE_STATUS = 3;
+ public static final int TYPE_PRIVATE = 4;
+
+ public static final String CONVERSATION = "conversationUuid";
+ public static final String COUNTERPART = "counterpart";
+ public static final String TRUE_COUNTERPART = "trueCounterpart";
+ public static final String BODY = "body";
+ public static final String TIME_SENT = "timeSent";
+ public static final String ENCRYPTION = "encryption";
+ public static final String STATUS = "status";
+ public static final String TYPE = "type";
+ public static final String CARBON = "carbon";
+ public static final String OOB = "oob";
+ public static final String EDITED = "edited";
+ public static final String REMOTE_MSG_ID = "remoteMsgId";
+ public static final String SERVER_MSG_ID = "serverMsgId";
+ public static final String RELATIVE_FILE_PATH = "relativeFilePath";
+ public static final String FINGERPRINT = "axolotl_fingerprint";
+ public static final String READ = "read";
+ public static final String ME_COMMAND = "/me ";
+
+
+ public boolean markable = false;
+ protected String conversationUuid;
+ protected Jid counterpart;
+ protected Jid trueCounterpart;
+ private String body;
+ protected String encryptedBody;
+ protected long timeSent;
+ protected int encryption;
+ protected int status;
+ protected int type;
+ protected boolean carbon = false;
+ protected boolean oob = false;
+ protected String edited = null;
+ protected String relativeFilePath;
+ protected boolean read = true;
+ protected String remoteMsgId = null;
+ protected String serverMsgId = null;
+ protected Conversation conversation = null;
+ protected Transferable transferable = null;
+ private Message mNextMessage = null;
+ private Message mPreviousMessage = null;
+ private String axolotlFingerprint = null;
+ private Decision mTreatAsDownloadAble = Decision.NOT_DECIDED;
+
+ private boolean httpUploaded;
+ private FileParams fileParams;
+
+ private Message() {
+
+ }
+
+ public Message(Conversation conversation, String body, int encryption) {
+ this(conversation, body, encryption, STATUS_UNSEND);
+ }
+
+ public Message(Conversation conversation, String body, int encryption, int status) {
+ this(java.util.UUID.randomUUID().toString(),
+ conversation.getUuid(),
+ conversation.getJid() == null ? null : conversation.getJid().toBareJid(),
+ null,
+ body,
+ System.currentTimeMillis(),
+ encryption,
+ status,
+ TYPE_TEXT,
+ false,
+ null,
+ null,
+ null,
+ null,
+ true,
+ null,
+ false);
+ this.conversation = conversation;
+ }
+
+ private Message(final String uuid, final String conversationUUid, final Jid counterpart,
+ final Jid trueCounterpart, final String body, final long timeSent,
+ final int encryption, final int status, final int type, final boolean carbon,
+ final String remoteMsgId, final String relativeFilePath,
+ final String serverMsgId, final String fingerprint, final boolean read,
+ final String edited, final boolean oob) {
+ this.uuid = uuid;
+ this.conversationUuid = conversationUUid;
+ this.counterpart = counterpart;
+ this.trueCounterpart = trueCounterpart;
+ this.body = body;
+ this.timeSent = timeSent;
+ this.encryption = encryption;
+ this.status = status;
+ this.type = type;
+ this.carbon = carbon;
+ this.remoteMsgId = remoteMsgId;
+ this.relativeFilePath = relativeFilePath;
+ this.serverMsgId = serverMsgId;
+ this.axolotlFingerprint = fingerprint;
+ this.read = read;
+ this.edited = edited;
+ this.oob = oob;
+ }
+
+ public static Message fromCursor(Cursor cursor) {
+ Jid jid;
+ try {
+ String value = cursor.getString(cursor.getColumnIndex(COUNTERPART));
+ if (value != null) {
+ jid = Jid.fromString(value, true);
+ } else {
+ jid = null;
+ }
+ } catch (InvalidJidException e) {
+ jid = null;
+ }
+ Jid trueCounterpart;
+ try {
+ String value = cursor.getString(cursor.getColumnIndex(TRUE_COUNTERPART));
+ if (value != null) {
+ trueCounterpart = Jid.fromString(value, true);
+ } else {
+ trueCounterpart = null;
+ }
+ } catch (InvalidJidException e) {
+ trueCounterpart = null;
+ }
+ return new Message(cursor.getString(cursor.getColumnIndex(UUID)),
+ cursor.getString(cursor.getColumnIndex(CONVERSATION)),
+ jid,
+ trueCounterpart,
+ cursor.getString(cursor.getColumnIndex(BODY)),
+ cursor.getLong(cursor.getColumnIndex(TIME_SENT)),
+ cursor.getInt(cursor.getColumnIndex(ENCRYPTION)),
+ cursor.getInt(cursor.getColumnIndex(STATUS)),
+ cursor.getInt(cursor.getColumnIndex(TYPE)),
+ cursor.getInt(cursor.getColumnIndex(CARBON)) > 0,
+ cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)),
+ cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)),
+ cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)),
+ cursor.getString(cursor.getColumnIndex(FINGERPRINT)),
+ cursor.getInt(cursor.getColumnIndex(READ)) > 0,
+ cursor.getString(cursor.getColumnIndex(EDITED)),
+ cursor.getInt(cursor.getColumnIndex(OOB)) > 0);
+ }
+
+ public static Message createStatusMessage(Conversation conversation, String body) {
+ final Message message = new Message();
+ message.setType(Message.TYPE_STATUS);
+ message.setConversation(conversation);
+ message.setBody(body);
+ return message;
+ }
+
+ @Override
+ public ContentValues getContentValues() {
+ ContentValues values = new ContentValues();
+ values.put(UUID, uuid);
+ values.put(CONVERSATION, conversationUuid);
+ if (counterpart == null) {
+ values.putNull(COUNTERPART);
+ } else {
+ values.put(COUNTERPART, counterpart.toString());
+ }
+ if (trueCounterpart == null) {
+ values.putNull(TRUE_COUNTERPART);
+ } else {
+ values.put(TRUE_COUNTERPART, trueCounterpart.toString());
+ }
+ values.put(BODY, body);
+ values.put(TIME_SENT, timeSent);
+ values.put(ENCRYPTION, encryption);
+ values.put(STATUS, status);
+ values.put(TYPE, type);
+ values.put(CARBON, carbon ? 1 : 0);
+ values.put(REMOTE_MSG_ID, remoteMsgId);
+ values.put(RELATIVE_FILE_PATH, relativeFilePath);
+ values.put(SERVER_MSG_ID, serverMsgId);
+ values.put(FINGERPRINT, axolotlFingerprint);
+ values.put(READ,read ? 1 : 0);
+ values.put(EDITED, edited);
+ values.put(OOB, oob ? 1 : 0);
+ return values;
+ }
+
+ public String getConversationUuid() {
+ return conversationUuid;
+ }
+
+ public Conversation getConversation() {
+ return this.conversation;
+ }
+
+ public void setConversation(Conversation conv) {
+ this.conversation = conv;
+ }
+
+ public Jid getCounterpart() {
+ return counterpart;
+ }
+
+ public void setCounterpart(final Jid counterpart) {
+ this.counterpart = counterpart;
+ }
+
+ public Contact getContact() {
+ if (this.conversation.getMode() == Conversation.MODE_SINGLE) {
+ return this.conversation.getContact();
+ } else {
+ if (this.trueCounterpart == null) {
+ return null;
+ } else {
+ return this.conversation.getAccount().getRoster()
+ .getContactFromRoster(this.trueCounterpart);
+ }
+ }
+ }
+
+ public String getBody() {
+ return body;
+ }
+
+ public void setBody(String body) {
+ this.body = body;
+ }
+
+ public long getTimeSent() {
+ return timeSent;
+ }
+
+ public int getEncryption() {
+ return encryption;
+ }
+
+ public void setEncryption(int encryption) {
+ this.encryption = encryption;
+ }
+
+ public int getStatus() {
+ return status;
+ }
+
+ public void setStatus(int status) {
+ this.status = status;
+ }
+
+ public String getRelativeFilePath() {
+ return this.relativeFilePath;
+ }
+
+ public void setRelativeFilePath(String path) {
+ this.relativeFilePath = path;
+ }
+
+ public String getRemoteMsgId() {
+ return this.remoteMsgId;
+ }
+
+ public void setRemoteMsgId(String id) {
+ this.remoteMsgId = id;
+ }
+
+ public String getServerMsgId() {
+ return this.serverMsgId;
+ }
+
+ public void setServerMsgId(String id) {
+ this.serverMsgId = id;
+ }
+
+ public boolean isRead() {
+ return this.read;
+ }
+
+ public void markRead() {
+ this.read = true;
+ }
+
+ public void markUnread() {
+ this.read = false;
+ }
+
+ public void setTime(long time) {
+ this.timeSent = time;
+ }
+
+ public String getEncryptedBody() {
+ return this.encryptedBody;
+ }
+
+ public void setEncryptedBody(String body) {
+ this.encryptedBody = body;
+ }
+
+ public int getType() {
+ return this.type;
+ }
+
+ public void setType(int type) {
+ this.type = type;
+ if (null == this.fileParams && (type == Message.TYPE_FILE || type == Message.TYPE_IMAGE)) {
+ this.setFileParams(new FileParams());
+ }
+ }
+
+ public boolean isCarbon() {
+ return carbon;
+ }
+
+ public void setCarbon(boolean carbon) {
+ this.carbon = carbon;
+ }
+
+ public boolean edited() {
+ return this.edited != null;
+ }
+
+ public void setTrueCounterpart(Jid trueCounterpart) {
+ this.trueCounterpart = trueCounterpart;
+ }
+
+ public Transferable getTransferable() {
+ return this.transferable;
+ }
+
+ public void setTransferable(Transferable transferable) {
+ this.transferable = transferable;
+ }
+
+ public boolean equals(Message message) {
+ if (this.getServerMsgId() != null && message.getServerMsgId() != null) {
+ return this.getServerMsgId().equals(message.getServerMsgId());
+ } else if (this.getBody() == null || this.getCounterpart() == null
+ || message.getBody() == null || message.getCounterpart() == null) {
+ return false;
+ } else {
+ String body = this.getBody();
+ String otherBody = message.getBody();
+
+ if (message.getRemoteMsgId() != null && this.getRemoteMsgId() != null) {
+ return (message.getRemoteMsgId().equals(this.getRemoteMsgId())
+ || message.getRemoteMsgId().equals(this.getUuid())
+ || message.getUuid().equals(this.getRemoteMsgId()))
+ && this.getCounterpart().equals(message.getCounterpart())
+ && (body.equals(otherBody)
+ ||(message.getEncryption() == Message.ENCRYPTION_PGP
+ && message.getRemoteMsgId().matches("[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}"))) ;
+ } else {
+ // existing (send) message with no remoteMsgId and MAM message with remoteMsgId
+ return ((this.getRemoteMsgId() == null && message.getRemoteMsgId() != null)
+ || (this.getRemoteMsgId() != null && message.getRemoteMsgId() == null)
+ // both null is also acceptable
+ || (this.getRemoteMsgId() == null && message.getRemoteMsgId() == null))
+ && this.getCounterpart().equals(message.getCounterpart())
+ && body.equals(otherBody);
+ }
+ }
+ }
+
+ public Message next() {
+ synchronized (this.conversation.messages) {
+ if (this.mNextMessage == null) {
+ int index = this.conversation.messages.indexOf(this);
+ if (index < 0 || index >= this.conversation.messages.size() - 1) {
+ this.mNextMessage = null;
+ } else {
+ this.mNextMessage = this.conversation.messages.get(index + 1);
+ }
+ }
+ return this.mNextMessage;
+ }
+ }
+
+ public Message prev() {
+ synchronized (this.conversation.messages) {
+ if (this.mPreviousMessage == null) {
+ int index = this.conversation.messages.indexOf(this);
+ if (index <= 0 || index > this.conversation.messages.size()) {
+ this.mPreviousMessage = null;
+ } else {
+ this.mPreviousMessage = this.conversation.messages.get(index - 1);
+ }
+ }
+ return this.mPreviousMessage;
+ }
+ }
+
+ public boolean hasMeCommand() {
+ return getBody().startsWith(ME_COMMAND);
+ }
+
+ public String getBodyReplacedMeCommand(String replaceString) {
+ try {
+ return getBody().replaceAll("^" + Message.ME_COMMAND, replaceString + " ");
+ } catch (ArrayIndexOutOfBoundsException e) {
+ return getBody();
+ }
+ }
+
+ public boolean trusted() {
+ Contact contact = this.getContact();
+ return (status > STATUS_RECEIVED || (contact != null && contact.trusted()));
+ }
+
+ public boolean fixCounterpart() {
+ Presences presences = conversation.getContact().getPresences();
+ if (counterpart != null && presences.has(counterpart.getResourcepart())) {
+ return true;
+ } else if (presences.size() >= 1) {
+ try {
+ counterpart = Jid.fromParts(conversation.getJid().getLocalpart(),
+ conversation.getJid().getDomainpart(),
+ presences.asStringArray()[0]);
+ return true;
+ } catch (InvalidJidException e) {
+ counterpart = null;
+ return false;
+ }
+ } else {
+ counterpart = null;
+ return false;
+ }
+ }
+
+ public void setUuid(String uuid) {
+ this.uuid = uuid;
+ }
+
+ public String getEditedId() {
+ return edited;
+ }
+
+ public void setOob(boolean isOob) {
+ this.oob = isOob;
+ }
+
+ public enum Decision {
+ MUST,
+ SHOULD,
+ NEVER,
+ NOT_DECIDED,
+ }
+
+ public String getMimeType() { // TODO: Move to fileparams
+ if (relativeFilePath != null) {
+ int start = relativeFilePath.lastIndexOf('.') + 1;
+ if (start < relativeFilePath.length()) {
+ return MimeUtils.guessMimeTypeFromExtension(relativeFilePath.substring(start));
+ } else {
+ return null;
+ }
+ } else {
+ try {
+ return MimeUtils.guessMimeTypeFromExtension(FileUtils.getRelevantExtension(new URL(this.getBody())));
+ } catch (MalformedURLException e) {
+ return null;
+ }
+ }
+ }
+
+ /**
+ * in case of later found error with decision, set it to a value which does not affect anything, hopefully
+ */
+ public void setNoDownloadable() {
+ mTreatAsDownloadAble = Decision.NEVER;
+ }
+
+ public void setTreatAsDownloadable(Decision downloadable) {
+ this.mTreatAsDownloadAble = downloadable;
+ }
+
+ public Decision treatAsDownloadable() {
+ // only test this ones, body will not change
+ if (mTreatAsDownloadAble != Decision.NOT_DECIDED) {
+ return mTreatAsDownloadAble;
+ }
+
+ MessageUtil.extractFileParamsFromBody(this);
+ return this.mTreatAsDownloadAble;
+ }
+
+ public void untie() {
+ this.mNextMessage = null;
+ this.mPreviousMessage = null;
+ }
+
+ public boolean isFileOrImage() {
+ return type == TYPE_FILE || type == TYPE_IMAGE;
+ }
+
+ public boolean hasFileAttached() {
+ return isFileOrImage() || isHttpUploaded() || (null != fileParams && null != fileParams.getPath());
+ }
+
+ /*
+ @TODO better
+ */
+ public boolean hasFileOnRemoteHost() {
+ return hasFileAttached() && null != getFileParams() && getFileParams().isRemoteAvailable();
+ }
+
+ /*
+ @TODO better
+ */
+ public boolean needsUploading() {
+ return hasFileAttached() && getFileParams().getFileStatus() == FileStatus.NEEDS_UPLOAD;
+ }
+
+ public void setFingerprint(String fingerprint) {
+ this.axolotlFingerprint = fingerprint;
+ }
+
+ public String getFingerprint() {
+ return axolotlFingerprint;
+ }
+
+ public boolean isTrusted() {
+ XmppAxolotlSession.Trust t = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint);
+ return t != null && t.trusted();
+ }
+
+ private int getPreviousEncryption() {
+ for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()){
+ if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) {
+ continue;
+ }
+ return iterator.getEncryption();
+ }
+ return ENCRYPTION_NONE;
+ }
+
+ private int getNextEncryption() {
+ for (Message iterator = this.next(); iterator != null; iterator = iterator.next()){
+ if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) {
+ continue;
+ }
+ return iterator.getEncryption();
+ }
+ return conversation.getNextEncryption();
+ }
+
+ public boolean isValidInSession() {
+ int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
+ int futureEncryption = getCleanedEncryption(this.getNextEncryption());
+
+ boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
+ || futureEncryption == ENCRYPTION_NONE
+ || pastEncryption != futureEncryption;
+
+ return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
+ }
+
+ public boolean isHttpUploaded() {
+ return httpUploaded;
+ }
+
+ public void setHttpUploaded(boolean httpUploaded) {
+ this.httpUploaded = httpUploaded;
+ }
+
+ public FileParams getFileParams() {
+ return this.fileParams;
+ }
+
+ public void setFileParams(FileParams params) {
+ this.fileParams = params;
+ }
+
+ private static int getCleanedEncryption(int encryption) {
+ if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
+ return ENCRYPTION_PGP;
+ }
+ return encryption;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java
new file mode 100644
index 00000000..7681a3d4
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/entities/MucOptions.java
@@ -0,0 +1,509 @@
+package eu.siacs.conversations.entities;
+
+import android.annotation.SuppressLint;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.xmpp.forms.Data;
+import eu.siacs.conversations.xmpp.forms.Field;
+import eu.siacs.conversations.xmpp.jid.InvalidJidException;
+import eu.siacs.conversations.xmpp.jid.Jid;
+import eu.siacs.conversations.xmpp.pep.Avatar;
+
+@SuppressLint("DefaultLocale")
+public class MucOptions {
+
+ public Account getAccount() {
+ return this.conversation.getAccount();
+ }
+
+ public void setSelf(User user) {
+ this.self = user;
+ }
+
+ public enum Affiliation {
+ OWNER("owner", 4, R.string.owner),
+ ADMIN("admin", 3, R.string.admin),
+ MEMBER("member", 2, R.string.member),
+ OUTCAST("outcast", 0, R.string.outcast),
+ NONE("none", 1, R.string.no_affiliation);
+
+ Affiliation(String string, int rank, int resId) {
+ this.string = string;
+ this.resId = resId;
+ this.rank = rank;
+ }
+
+ private String string;
+ private int resId;
+ private int rank;
+
+ public int getResId() {
+ return resId;
+ }
+
+ @Override
+ public String toString() {
+ return this.string;
+ }
+
+ public boolean outranks(Affiliation affiliation) {
+ return rank > affiliation.rank;
+ }
+
+ public boolean ranks(Affiliation affiliation) {
+ return rank >= affiliation.rank;
+ }
+ }
+
+ public enum Role {
+ MODERATOR("moderator", R.string.moderator,3),
+ VISITOR("visitor", R.string.visitor,1),
+ PARTICIPANT("participant", R.string.participant,2),
+ NONE("none", R.string.no_role,0);
+
+ Role(String string, int resId, int rank) {
+ this.string = string;
+ this.resId = resId;
+ this.rank = rank;
+ }
+
+ private String string;
+ private int resId;
+ private int rank;
+
+ public int getResId() {
+ return resId;
+ }
+
+ @Override
+ public String toString() {
+ return this.string;
+ }
+
+ public boolean ranks(Role role) {
+ return rank >= role.rank;
+ }
+ }
+
+ public enum Error {
+ NO_RESPONSE,
+ NONE,
+ NICK_IN_USE,
+ PASSWORD_REQUIRED,
+ BANNED,
+ MEMBERS_ONLY,
+ KICKED,
+ SHUTDOWN,
+ UNKNOWN
+ }
+
+ public static final String STATUS_CODE_ROOM_CONFIG_CHANGED = "104";
+ public static final String STATUS_CODE_SELF_PRESENCE = "110";
+ public static final String STATUS_CODE_BANNED = "301";
+ public static final String STATUS_CODE_CHANGED_NICK = "303";
+ public static final String STATUS_CODE_KICKED = "307";
+ public static final String STATUS_CODE_AFFILIATION_CHANGE = "321";
+ public static final String STATUS_CODE_LOST_MEMBERSHIP = "322";
+ public static final String STATUS_CODE_SHUTDOWN = "332";
+
+ private interface OnEventListener {
+ void onSuccess();
+
+ void onFailure();
+ }
+
+ public interface OnRenameListener extends OnEventListener {
+
+ }
+
+ public static class User {
+ private Role role = Role.NONE;
+ private Affiliation affiliation = Affiliation.NONE;
+ private Jid jid;
+ private Jid fullJid;
+ private long pgpKeyId = 0;
+ private Avatar avatar;
+ private MucOptions options;
+
+ public User(MucOptions options, Jid from) {
+ this.options = options;
+ this.fullJid = from;
+ }
+
+ public String getName() {
+ return this.fullJid.getResourcepart();
+ }
+
+ public void setJid(Jid jid) {
+ this.jid = jid;
+ }
+
+ public Jid getJid() {
+ return this.jid;
+ }
+
+ public Role getRole() {
+ return this.role;
+ }
+
+ public void setRole(String role) {
+ role = role.toLowerCase();
+ switch (role) {
+ case "moderator":
+ this.role = Role.MODERATOR;
+ break;
+ case "participant":
+ this.role = Role.PARTICIPANT;
+ break;
+ case "visitor":
+ this.role = Role.VISITOR;
+ break;
+ default:
+ this.role = Role.NONE;
+ break;
+ }
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ } else if (!(other instanceof User)) {
+ return false;
+ } else {
+ User o = (User) other;
+ return getName() != null && getName().equals(o.getName())
+ && jid != null && jid.equals(o.jid)
+ && affiliation == o.affiliation
+ && role == o.role;
+ }
+ }
+
+ public Affiliation getAffiliation() {
+ return this.affiliation;
+ }
+
+ public void setAffiliation(String affiliation) {
+ affiliation = affiliation.toLowerCase();
+ switch (affiliation) {
+ case "admin":
+ this.affiliation = Affiliation.ADMIN;
+ break;
+ case "owner":
+ this.affiliation = Affiliation.OWNER;
+ break;
+ case "member":
+ this.affiliation = Affiliation.MEMBER;
+ break;
+ case "outcast":
+ this.affiliation = Affiliation.OUTCAST;
+ break;
+ default:
+ this.affiliation = Affiliation.NONE;
+ }
+ }
+
+ public void setPgpKeyId(long id) {
+ this.pgpKeyId = id;
+ }
+
+ public long getPgpKeyId() {
+ return this.pgpKeyId;
+ }
+
+ public Contact getContact() {
+ return getAccount().getRoster().getContactFromRoster(getJid());
+ }
+
+ public boolean setAvatar(Avatar avatar) {
+ if (this.avatar != null && this.avatar.equals(avatar)) {
+ return false;
+ } else {
+ this.avatar = avatar;
+ return true;
+ }
+ }
+
+ public String getAvatar() {
+ return avatar == null ? null : avatar.getFilename();
+ }
+
+ public Account getAccount() {
+ return options.getAccount();
+ }
+
+ public Jid getFullJid() {
+ return fullJid;
+ }
+ }
+
+ private Account account;
+ private final Map<String, User> users = Collections.synchronizedMap(new LinkedHashMap<String, User>());
+ private final Set<Jid> members = Collections.synchronizedSet(new HashSet<Jid>());
+ private final List<String> features = new ArrayList<>();
+ private Data form = new Data();
+ private Conversation conversation;
+ private boolean isOnline = false;
+ private Error error = Error.NONE;
+ public OnRenameListener onRenameListener = null;
+ private User self;
+ private String subject = null;
+ private String password = null;
+ public boolean mNickChangingInProgress = false;
+
+ public MucOptions(Conversation conversation) {
+ this.account = conversation.getAccount();
+ this.conversation = conversation;
+ this.self = new User(this,createJoinJid(getProposedNick()));
+ }
+
+ public void updateFeatures(ArrayList<String> features) {
+ this.features.clear();
+ this.features.addAll(features);
+ }
+
+ public void updateFormData(Data form) {
+ this.form = form;
+ }
+
+ public boolean hasFeature(String feature) {
+ return this.features.contains(feature);
+ }
+
+ public boolean canInvite() {
+ Field field = this.form.getFieldByName("muc#roomconfig_allowinvites");
+ return !membersOnly() || self.getRole().ranks(Role.MODERATOR) || (field != null && "1".equals(field.getValue()));
+ }
+
+ public boolean canChangeSubject() {
+ Field field = this.form.getFieldByName("muc#roomconfig_changesubject");
+ return self.getRole().ranks(Role.MODERATOR) || (field != null && "1".equals(field.getValue()));
+ }
+
+ public boolean participating() {
+ return !online()
+ || self.getRole().ranks(Role.PARTICIPANT)
+ || hasFeature("muc_unmoderated");
+ }
+
+ public boolean membersOnly() {
+ return hasFeature("muc_membersonly");
+ }
+
+ public boolean mamSupport() {
+ // Update with "urn:xmpp:mam:1" once we support it
+ return hasFeature("urn:xmpp:mam:0");
+ }
+
+ public boolean nonanonymous() {
+ return hasFeature("muc_nonanonymous");
+ }
+
+ public boolean persistent() {
+ return hasFeature("muc_persistent");
+ }
+
+ public boolean moderated() {
+ return hasFeature("muc_moderated");
+ }
+
+ public User deleteUser(String name) {
+ return this.users.remove(name);
+ }
+
+ public void addUser(User user) {
+ this.users.put(user.getName(), user);
+ }
+
+ public User findUser(String name) {
+ return this.users.get(name);
+ }
+
+ public boolean isUserInRoom(String name) {
+ return findUser(name) != null;
+ }
+
+ public void setError(Error error) {
+ this.isOnline = isOnline && error == Error.NONE;
+ this.error = error;
+ }
+
+ public void setOnline() {
+ this.isOnline = true;
+ }
+
+ public ArrayList<User> getUsers() {
+ return new ArrayList<>(users.values());
+ }
+
+ public List<User> getUsers(int max) {
+ ArrayList<User> users = getUsers();
+ return users.subList(0, Math.min(max, users.size()));
+ }
+
+ public int getUserCount() {
+ return this.users.size();
+ }
+
+ public String getProposedNick() {
+ if (conversation.getBookmark() != null
+ && conversation.getBookmark().getNick() != null
+ && !conversation.getBookmark().getNick().isEmpty()) {
+ return conversation.getBookmark().getNick();
+ } else if (!conversation.getJid().isBareJid()) {
+ return conversation.getJid().getResourcepart();
+ } else {
+ return account.getUsername();
+ }
+ }
+
+ public String getActualNick() {
+ if (this.self.getName() != null) {
+ return this.self.getName();
+ } else {
+ return this.getProposedNick();
+ }
+ }
+
+ public boolean online() {
+ return this.isOnline;
+ }
+
+ public Error getError() {
+ return this.error;
+ }
+
+ public void setOnRenameListener(OnRenameListener listener) {
+ this.onRenameListener = listener;
+ }
+
+ public void setOffline() {
+ this.users.clear();
+ this.error = Error.NO_RESPONSE;
+ this.isOnline = false;
+ }
+
+ public User getSelf() {
+ return self;
+ }
+
+ public void setSubject(String content) {
+ this.subject = content;
+ }
+
+ public String getSubject() {
+ return this.subject;
+ }
+
+ public String createNameFromParticipants() {
+ if (users.size() >= 2) {
+ List<String> names = new ArrayList<>();
+ for (User user : getUsers(5)) {
+ Contact contact = user.getContact();
+ if (contact != null && !contact.getDisplayName().isEmpty()) {
+ names.add(contact.getDisplayName().split("\\s+")[0]);
+ } else {
+ names.add(user.getName());
+ }
+ }
+ StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < names.size(); ++i) {
+ builder.append(names.get(i));
+ if (i != names.size() - 1) {
+ builder.append(", ");
+ }
+ }
+ return builder.toString();
+ } else {
+ return null;
+ }
+ }
+
+ public long[] getPgpKeyIds() {
+ List<Long> ids = new ArrayList<>();
+ for (User user : this.users.values()) {
+ if (user.getPgpKeyId() != 0) {
+ ids.add(user.getPgpKeyId());
+ }
+ }
+ ids.add(account.getPgpId());
+ long[] primitiveLongArray = new long[ids.size()];
+ for (int i = 0; i < ids.size(); ++i) {
+ primitiveLongArray[i] = ids.get(i);
+ }
+ return primitiveLongArray;
+ }
+
+ public boolean pgpKeysInUse() {
+ for (User user : this.users.values()) {
+ if (user.getPgpKeyId() != 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public boolean everybodyHasKeys() {
+ for (User user : this.users.values()) {
+ if (user.getPgpKeyId() == 0) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public Jid createJoinJid(String nick) {
+ try {
+ return Jid.fromString(this.conversation.getJid().toBareJid().toString() + "/" + nick);
+ } catch (final InvalidJidException e) {
+ return null;
+ }
+ }
+
+ public Jid getTrueCounterpart(String name) {
+ if (name.equals(getSelf().getName())) {
+ return account.getJid().toBareJid();
+ }
+ User user = findUser(name);
+ return user == null ? null : user.getJid();
+ }
+
+ public String getPassword() {
+ this.password = conversation.getAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD);
+ if (this.password == null && conversation.getBookmark() != null
+ && conversation.getBookmark().getPassword() != null) {
+ return conversation.getBookmark().getPassword();
+ } else {
+ return this.password;
+ }
+ }
+
+ public void setPassword(String password) {
+ if (conversation.getBookmark() != null) {
+ conversation.getBookmark().setPassword(password);
+ } else {
+ this.password = password;
+ }
+ conversation.setAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD, password);
+ }
+
+ public Conversation getConversation() {
+ return this.conversation;
+ }
+
+ public void putMember(Jid jid) {
+ members.add(jid);
+ }
+
+ public List<Jid> getMembers() {
+ return new ArrayList<>(members);
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/entities/Presence.java b/src/main/java/eu/siacs/conversations/entities/Presence.java
new file mode 100644
index 00000000..442f1bca
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/entities/Presence.java
@@ -0,0 +1,80 @@
+package eu.siacs.conversations.entities;
+
+import java.lang.Comparable;
+import java.util.Locale;
+
+import eu.siacs.conversations.xml.Element;
+
+public class Presence implements Comparable {
+
+ public enum Status {
+ CHAT, ONLINE, AWAY, XA, DND, OFFLINE;
+
+ public String toShowString() {
+ switch(this) {
+ case CHAT: return "chat";
+ case AWAY: return "away";
+ case XA: return "xa";
+ case DND: return "dnd";
+ }
+
+ return null;
+ }
+ }
+
+ protected final Status status;
+ protected ServiceDiscoveryResult disco;
+ protected final String ver;
+ protected final String hash;
+
+ private Presence(Status status, String ver, String hash) {
+ this.status = status;
+ this.ver = ver;
+ this.hash = hash;
+ }
+
+ public static Presence parse(String show, Element caps) {
+ final String hash = caps == null ? null : caps.getAttribute("hash");
+ final String ver = caps == null ? null : caps.getAttribute("ver");
+ if (show == null) {
+ return new Presence(Status.ONLINE, ver, hash);
+ } else {
+ switch (show.toLowerCase(Locale.US)) {
+ case "away":
+ return new Presence(Status.AWAY, ver, hash);
+ case "xa":
+ return new Presence(Status.XA, ver, hash);
+ case "dnd":
+ return new Presence(Status.DND, ver, hash);
+ case "chat":
+ return new Presence(Status.CHAT, ver, hash);
+ default:
+ return new Presence(Status.ONLINE, ver, hash);
+ }
+ }
+ }
+
+ public int compareTo(Object other) {
+ return this.status.compareTo(((Presence)other).status);
+ }
+
+ public Status getStatus() {
+ return this.status;
+ }
+
+ public boolean hasCaps() {
+ return ver != null && hash != null;
+ }
+
+ public String getVer() {
+ return this.ver;
+ }
+
+ public String getHash() {
+ return this.hash;
+ }
+
+ public void setServiceDiscoveryResult(ServiceDiscoveryResult disco) {
+ this.disco = disco;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/entities/Presences.java b/src/main/java/eu/siacs/conversations/entities/Presences.java
new file mode 100644
index 00000000..813eda7a
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/entities/Presences.java
@@ -0,0 +1,60 @@
+package eu.siacs.conversations.entities;
+
+import java.util.Collections;
+import java.util.Hashtable;
+import java.util.Iterator;
+
+import eu.siacs.conversations.xml.Element;
+
+public class Presences {
+ private final Hashtable<String, Presence> presences = new Hashtable<>();
+
+ public Hashtable<String, Presence> getPresences() {
+ return this.presences;
+ }
+
+ public void updatePresence(String resource, Presence presence) {
+ synchronized (this.presences) {
+ this.presences.put(resource, presence);
+ }
+ }
+
+ public void removePresence(String resource) {
+ synchronized (this.presences) {
+ this.presences.remove(resource);
+ }
+ }
+
+ public void clearPresences() {
+ synchronized (this.presences) {
+ this.presences.clear();
+ }
+ }
+
+ public Presence getMostAvailablePresence() {
+ synchronized (this.presences) {
+ if (presences.size() < 1) { return null; }
+ return Collections.min(presences.values());
+ }
+ }
+
+ public int size() {
+ synchronized (this.presences) {
+ return presences.size();
+ }
+ }
+
+ public String[] asStringArray() {
+ synchronized (this.presences) {
+ final String[] presencesArray = new String[presences.size()];
+ presences.keySet().toArray(presencesArray);
+ return presencesArray;
+ }
+ }
+
+ public boolean has(String presence) {
+ synchronized (this.presences) {
+ return presences.containsKey(presence);
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/entities/Roster.java b/src/main/java/eu/siacs/conversations/entities/Roster.java
new file mode 100644
index 00000000..d3ad9181
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/entities/Roster.java
@@ -0,0 +1,96 @@
+package eu.siacs.conversations.entities;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+public class Roster {
+ final Account account;
+ final HashMap<Jid, Contact> contacts = new HashMap<>();
+ private String version = null;
+
+ public Roster(Account account) {
+ this.account = account;
+ }
+
+ public Contact getContactFromRoster(Jid jid) {
+ if (jid == null) {
+ return null;
+ }
+ synchronized (this.contacts) {
+ Contact contact = contacts.get(jid.toBareJid());
+ if (contact != null && contact.showInRoster()) {
+ return contact;
+ } else {
+ return null;
+ }
+ }
+ }
+
+ public Contact getContact(final Jid jid) {
+ synchronized (this.contacts) {
+ if (!contacts.containsKey(jid.toBareJid())) {
+ Contact contact = new Contact(jid.toBareJid());
+ contact.setAccount(account);
+ contacts.put(contact.getJid().toBareJid(), contact);
+ return contact;
+ }
+ return contacts.get(jid.toBareJid());
+ }
+ }
+
+ public void clearPresences() {
+ for (Contact contact : getContacts()) {
+ contact.clearPresences();
+ }
+ }
+
+ public void markAllAsNotInRoster() {
+ for (Contact contact : getContacts()) {
+ contact.resetOption(Contact.Options.IN_ROSTER);
+ }
+ }
+
+ public List<Contact> getWithSystemAccounts() {
+ List<Contact> with = getContacts();
+ for(Iterator<Contact> iterator = with.iterator(); iterator.hasNext();) {
+ Contact contact = iterator.next();
+ if (contact.getSystemAccount() == null) {
+ iterator.remove();
+ }
+ }
+ return with;
+ }
+
+ public List<Contact> getContacts() {
+ synchronized (this.contacts) {
+ return new ArrayList<>(this.contacts.values());
+ }
+ }
+
+ public void initContact(final Contact contact) {
+ if (contact == null) {
+ return;
+ }
+ contact.setAccount(account);
+ contact.setOption(Contact.Options.IN_ROSTER);
+ synchronized (this.contacts) {
+ contacts.put(contact.getJid().toBareJid(), contact);
+ }
+ }
+
+ public void setVersion(String version) {
+ this.version = version;
+ }
+
+ public String getVersion() {
+ return this.version;
+ }
+
+ public Account getAccount() {
+ return this.account;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java b/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java
new file mode 100644
index 00000000..a8f60e39
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java
@@ -0,0 +1,293 @@
+package eu.siacs.conversations.entities;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.util.Base64;
+import java.io.UnsupportedEncodingException;
+import java.lang.Comparable;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.forms.Data;
+import eu.siacs.conversations.xmpp.forms.Field;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+public class ServiceDiscoveryResult {
+ public static final String TABLENAME = "discovery_results";
+ public static final String HASH = "hash";
+ public static final String VER = "ver";
+ public static final String RESULT = "result";
+
+ protected static String blankNull(String s) {
+ return s == null ? "" : s;
+ }
+
+ public static class Identity implements Comparable {
+ protected final String category;
+ protected final String type;
+ protected final String lang;
+ protected final String name;
+
+ public Identity(final String category, final String type, final String lang, final String name) {
+ this.category = category;
+ this.type = type;
+ this.lang = lang;
+ this.name = name;
+ }
+
+ public Identity(final Element el) {
+ this(
+ el.getAttribute("category"),
+ el.getAttribute("type"),
+ el.getAttribute("xml:lang"),
+ el.getAttribute("name")
+ );
+ }
+
+ public Identity(final JSONObject o) {
+ this(
+ o.optString("category", null),
+ o.optString("type", null),
+ o.optString("lang", null),
+ o.optString("name", null)
+ );
+ }
+
+ public String getCategory() {
+ return this.category;
+ }
+
+ public String getType() {
+ return this.type;
+ }
+
+ public String getLang() {
+ return this.lang;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public int compareTo(Object other) {
+ Identity o = (Identity)other;
+ int r = blankNull(this.getCategory()).compareTo(blankNull(o.getCategory()));
+ if(r == 0) {
+ r = blankNull(this.getType()).compareTo(blankNull(o.getType()));
+ }
+ if(r == 0) {
+ r = blankNull(this.getLang()).compareTo(blankNull(o.getLang()));
+ }
+ if(r == 0) {
+ r = blankNull(this.getName()).compareTo(blankNull(o.getName()));
+ }
+
+ return r;
+ }
+
+ public JSONObject toJSON() {
+ try {
+ JSONObject o = new JSONObject();
+ o.put("category", this.getCategory());
+ o.put("type", this.getType());
+ o.put("lang", this.getLang());
+ o.put("name", this.getName());
+ return o;
+ } catch(JSONException e) {
+ return null;
+ }
+ }
+ }
+
+ protected final String hash;
+ protected final byte[] ver;
+ protected final List<Identity> identities;
+ protected final List<String> features;
+ protected final List<Data> forms;
+
+ public ServiceDiscoveryResult(final IqPacket packet) {
+ this.identities = new ArrayList<>();
+ this.features = new ArrayList<>();
+ this.forms = new ArrayList<>();
+ this.hash = "sha-1"; // We only support sha-1 for now
+
+ final List<Element> elements = packet.query().getChildren();
+
+ for (final Element element : elements) {
+ if (element.getName().equals("identity")) {
+ Identity id = new Identity(element);
+ if (id.getType() != null && id.getCategory() != null) {
+ identities.add(id);
+ }
+ } else if (element.getName().equals("feature")) {
+ if (element.getAttribute("var") != null) {
+ features.add(element.getAttribute("var"));
+ }
+ } else if (element.getName().equals("x") && "jabber:x:data".equals(element.getAttribute("xmlns"))) {
+ forms.add(Data.parse(element));
+ }
+ }
+ this.ver = this.mkCapHash();
+ }
+
+ public ServiceDiscoveryResult(String hash, byte[] ver, JSONObject o) throws JSONException {
+ this.identities = new ArrayList<>();
+ this.features = new ArrayList<>();
+ this.forms = new ArrayList<>();
+ this.hash = hash;
+ this.ver = ver;
+
+ JSONArray identities = o.optJSONArray("identities");
+ if (identities != null) {
+ for (int i = 0; i < identities.length(); i++) {
+ this.identities.add(new Identity(identities.getJSONObject(i)));
+ }
+ }
+ JSONArray features = o.optJSONArray("features");
+ if (features != null) {
+ for (int i = 0; i < features.length(); i++) {
+ this.features.add(features.getString(i));
+ }
+ }
+ }
+
+ public String getVer() {
+ return new String(Base64.encode(this.ver, Base64.DEFAULT));
+ }
+
+ public ServiceDiscoveryResult(Cursor cursor) throws JSONException {
+ this(
+ cursor.getString(cursor.getColumnIndex(HASH)),
+ Base64.decode(cursor.getString(cursor.getColumnIndex(VER)), Base64.DEFAULT),
+ new JSONObject(cursor.getString(cursor.getColumnIndex(RESULT)))
+ );
+ }
+
+ public List<Identity> getIdentities() {
+ return this.identities;
+ }
+
+ public List<String> getFeatures() {
+ return this.features;
+ }
+
+ public boolean hasIdentity(String category, String type) {
+ for(Identity id : this.getIdentities()) {
+ if((category == null || id.getCategory().equals(category)) &&
+ (type == null || id.getType().equals(type))) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public String getExtendedDiscoInformation(String formType, String name) {
+ for(Data form : this.forms) {
+ if (formType.equals(form.getFormType())) {
+ for(Field field: form.getFields()) {
+ if (name.equals(field.getFieldName())) {
+ return field.getValue();
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ protected byte[] mkCapHash() {
+ StringBuilder s = new StringBuilder();
+
+ List<Identity> identities = this.getIdentities();
+ Collections.sort(identities);
+
+ for(Identity id : identities) {
+ s.append(
+ blankNull(id.getCategory()) + "/" +
+ blankNull(id.getType()) + "/" +
+ blankNull(id.getLang()) + "/" +
+ blankNull(id.getName()) + "<"
+ );
+ }
+
+ List<String> features = this.getFeatures();
+ Collections.sort(features);
+
+ for (String feature : features) {
+ s.append(feature + "<");
+ }
+
+ Collections.sort(forms, new Comparator<Data>() {
+ @Override
+ public int compare(Data lhs, Data rhs) {
+ return lhs.getFormType().compareTo(rhs.getFormType());
+ }
+ });
+
+ for(Data form : forms) {
+ s.append(form.getFormType() + "<");
+ List<Field> fields = form.getFields();
+ Collections.sort(fields, new Comparator<Field>() {
+ @Override
+ public int compare(Field lhs, Field rhs) {
+ return lhs.getFieldName().compareTo(rhs.getFieldName());
+ }
+ });
+ for(Field field : fields) {
+ s.append(field.getFieldName()+"<");
+ List<String> values = field.getValues();
+ Collections.sort(values);
+ for(String value : values) {
+ s.append(value+"<");
+ }
+ }
+ }
+
+ MessageDigest md;
+ try {
+ md = MessageDigest.getInstance("SHA-1");
+ } catch (NoSuchAlgorithmException e) {
+ return null;
+ }
+
+ try {
+ return md.digest(s.toString().getBytes("UTF-8"));
+ } catch(UnsupportedEncodingException e) {
+ return null;
+ }
+ }
+
+ public JSONObject toJSON() {
+ try {
+ JSONObject o = new JSONObject();
+
+ JSONArray ids = new JSONArray();
+ for(Identity id : this.getIdentities()) {
+ ids.put(id.toJSON());
+ }
+ o.put("identites", ids);
+
+ o.put("features", new JSONArray(this.getFeatures()));
+
+ return o;
+ } catch(JSONException e) {
+ return null;
+ }
+ }
+
+ public ContentValues getContentValues() {
+ final ContentValues values = new ContentValues();
+ values.put(HASH, this.hash);
+ values.put(VER, getVer());
+ values.put(RESULT, this.toJSON().toString());
+ return values;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/entities/Transferable.java b/src/main/java/eu/siacs/conversations/entities/Transferable.java
new file mode 100644
index 00000000..5a47c451
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/entities/Transferable.java
@@ -0,0 +1,31 @@
+package eu.siacs.conversations.entities;
+
+import java.util.Arrays;
+import java.util.List;
+
+public interface Transferable {
+
+ List<String> VALID_IMAGE_EXTENSIONS = Arrays.asList("webp", "jpeg", "jpg", "png", "jpe");
+ List<String> VALID_CRYPTO_EXTENSIONS = Arrays.asList("pgp", "gpg", "otr");
+ List<String> WELL_KNOWN_EXTENSIONS = Arrays.asList("pdf","m4a","mp4","3gp","aac","amr","mp3");
+
+ int STATUS_UNKNOWN = 0x200;
+ int STATUS_CHECKING = 0x201;
+ int STATUS_FAILED = 0x202;
+ int STATUS_OFFER = 0x203;
+ int STATUS_DOWNLOADING = 0x204;
+ int STATUS_DELETED = 0x205;
+ int STATUS_OFFER_CHECK_FILESIZE = 0x206;
+ int STATUS_UPLOADING = 0x207;
+
+
+ boolean start();
+
+ int getStatus();
+
+ long getFileSize();
+
+ int getProgress();
+
+ void cancel();
+}
diff --git a/src/main/java/eu/siacs/conversations/entities/TransferablePlaceholder.java b/src/main/java/eu/siacs/conversations/entities/TransferablePlaceholder.java
new file mode 100644
index 00000000..e065953e
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/entities/TransferablePlaceholder.java
@@ -0,0 +1,34 @@
+package eu.siacs.conversations.entities;
+
+public class TransferablePlaceholder implements Transferable {
+
+ private int status;
+
+ public TransferablePlaceholder(int status) {
+ this.status = status;
+ }
+ @Override
+ public boolean start() {
+ return false;
+ }
+
+ @Override
+ public int getStatus() {
+ return status;
+ }
+
+ @Override
+ public long getFileSize() {
+ return 0;
+ }
+
+ @Override
+ public int getProgress() {
+ return 0;
+ }
+
+ @Override
+ public void cancel() {
+
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java
new file mode 100644
index 00000000..649f767d
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java
@@ -0,0 +1,74 @@
+package eu.siacs.conversations.generator;
+
+import android.util.Base64;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.TimeZone;
+
+import de.thedevstack.conversationsplus.ConversationsPlusApplication;
+import de.tzur.conversations.Settings;
+import eu.siacs.conversations.crypto.axolotl.AxolotlService;
+
+public abstract class AbstractGenerator {
+ private final String[] FEATURES = {
+ "urn:xmpp:jingle:1",
+ "urn:xmpp:jingle:apps:file-transfer:3",
+ "urn:xmpp:jingle:transports:s5b:1",
+ "urn:xmpp:jingle:transports:ibb:1",
+ "http://jabber.org/protocol/muc",
+ "jabber:x:conference",
+ "http://jabber.org/protocol/caps",
+ "http://jabber.org/protocol/disco#info",
+ "urn:xmpp:avatar:metadata+notify",
+ "http://jabber.org/protocol/nick+notify",
+ "urn:xmpp:ping",
+ "jabber:iq:version",
+ "http://jabber.org/protocol/chatstates",
+ AxolotlService.PEP_DEVICE_LIST+"+notify"};
+ private final String[] MESSAGE_CONFIRMATION_FEATURES = {
+ "urn:xmpp:chat-markers:0",
+ "urn:xmpp:receipts"
+ };
+ protected final String IDENTITY_TYPE = "phone";
+
+ private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
+
+ public String getCapHash() {
+ StringBuilder s = new StringBuilder();
+ s.append("client/" + IDENTITY_TYPE + "//" + ConversationsPlusApplication.getNameAndVersion() + "<");
+ MessageDigest md;
+ try {
+ md = MessageDigest.getInstance("SHA-1");
+ } catch (NoSuchAlgorithmException e) {
+ return null;
+ }
+
+ for (String feature : getFeatures()) {
+ s.append(feature + "<");
+ }
+ byte[] sha1 = md.digest(s.toString().getBytes());
+ return new String(Base64.encode(sha1, Base64.DEFAULT));
+ }
+
+ public static String getTimestamp(long time) {
+ DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
+ return DATE_FORMAT.format(time);
+ }
+
+ public List<String> getFeatures() {
+ ArrayList<String> features = new ArrayList<>();
+ features.addAll(Arrays.asList(FEATURES));
+ if (Settings.CONFIRM_MESSAGE_RECEIVED) {
+ features.addAll(Arrays.asList(MESSAGE_CONFIRMATION_FEATURES));
+ }
+ Collections.sort(features);
+ return features;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java
new file mode 100644
index 00000000..012a5943
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java
@@ -0,0 +1,290 @@
+package eu.siacs.conversations.generator;
+
+
+import android.util.Base64;
+import android.util.Log;
+
+import org.whispersystems.libaxolotl.IdentityKey;
+import org.whispersystems.libaxolotl.ecc.ECPublicKey;
+import org.whispersystems.libaxolotl.state.PreKeyRecord;
+import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
+
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import de.thedevstack.conversationsplus.ConversationsPlusApplication;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.crypto.axolotl.AxolotlService;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.services.MessageArchiveService;
+import eu.siacs.conversations.utils.Xmlns;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.forms.Data;
+import eu.siacs.conversations.xmpp.jid.Jid;
+import eu.siacs.conversations.xmpp.pep.Avatar;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+public class IqGenerator extends AbstractGenerator {
+
+ public IqPacket discoResponse(final IqPacket request) {
+ final IqPacket packet = new IqPacket(IqPacket.TYPE.RESULT);
+ packet.setId(request.getId());
+ packet.setTo(request.getFrom());
+ final Element query = packet.addChild("query",
+ "http://jabber.org/protocol/disco#info");
+ query.setAttribute("node", request.query().getAttribute("node"));
+ final Element identity = query.addChild("identity");
+ identity.setAttribute("category", "client");
+ identity.setAttribute("type", IDENTITY_TYPE);
+ identity.setAttribute("name", ConversationsPlusApplication.getNameAndVersion());
+ for (final String feature : getFeatures()) {
+ query.addChild("feature").setAttribute("var", feature);
+ }
+ return packet;
+ }
+
+ public IqPacket versionResponse(final IqPacket request) {
+ final IqPacket packet = request.generateResponse(IqPacket.TYPE.RESULT);
+ Element query = packet.query("jabber:iq:version");
+ query.addChild("name").setContent(ConversationsPlusApplication.getName());
+ query.addChild("version").setContent(ConversationsPlusApplication.getVersion());
+ return packet;
+ }
+
+ protected IqPacket publish(final String node, final Element item) {
+ final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
+ final Element pubsub = packet.addChild("pubsub",
+ "http://jabber.org/protocol/pubsub");
+ final Element publish = pubsub.addChild("publish");
+ publish.setAttribute("node", node);
+ publish.addChild(item);
+ return packet;
+ }
+
+ protected IqPacket retrieve(String node, Element item) {
+ final IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
+ final Element pubsub = packet.addChild("pubsub",
+ "http://jabber.org/protocol/pubsub");
+ final Element items = pubsub.addChild("items");
+ items.setAttribute("node", node);
+ if (item != null) {
+ items.addChild(item);
+ }
+ return packet;
+ }
+
+ public IqPacket publishNick(String nick) {
+ final Element item = new Element("item");
+ item.addChild("nick","http://jabber.org/protocol/nick").setContent(nick);
+ return publish("http://jabber.org/protocol/nick", item);
+ }
+
+ public static IqPacket retrieveVcardAvatar(final Avatar avatar) {
+ final IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
+ packet.setTo(avatar.owner);
+ packet.addChild("vCard", "vcard-temp");
+ return packet;
+ }
+
+ public IqPacket retrieveDeviceIds(final Jid to) {
+ final IqPacket packet = retrieve(AxolotlService.PEP_DEVICE_LIST, null);
+ if(to != null) {
+ packet.setTo(to);
+ }
+ return packet;
+ }
+
+ public IqPacket retrieveBundlesForDevice(final Jid to, final int deviceid) {
+ final IqPacket packet = retrieve(AxolotlService.PEP_BUNDLES+":"+deviceid, null);
+ packet.setTo(to);
+ return packet;
+ }
+
+ public IqPacket retrieveVerificationForDevice(final Jid to, final int deviceid) {
+ final IqPacket packet = retrieve(AxolotlService.PEP_VERIFICATION+":"+deviceid, null);
+ packet.setTo(to);
+ return packet;
+ }
+
+ public IqPacket publishDeviceIds(final Set<Integer> ids) {
+ final Element item = new Element("item");
+ final Element list = item.addChild("list", AxolotlService.PEP_PREFIX);
+ for(Integer id:ids) {
+ final Element device = new Element("device");
+ device.setAttribute("id", id);
+ list.addChild(device);
+ }
+ return publish(AxolotlService.PEP_DEVICE_LIST, item);
+ }
+
+ public IqPacket publishBundles(final SignedPreKeyRecord signedPreKeyRecord, final IdentityKey identityKey,
+ final Set<PreKeyRecord> preKeyRecords, final int deviceId) {
+ final Element item = new Element("item");
+ final Element bundle = item.addChild("bundle", AxolotlService.PEP_PREFIX);
+ final Element signedPreKeyPublic = bundle.addChild("signedPreKeyPublic");
+ signedPreKeyPublic.setAttribute("signedPreKeyId", signedPreKeyRecord.getId());
+ ECPublicKey publicKey = signedPreKeyRecord.getKeyPair().getPublicKey();
+ signedPreKeyPublic.setContent(Base64.encodeToString(publicKey.serialize(),Base64.DEFAULT));
+ final Element signedPreKeySignature = bundle.addChild("signedPreKeySignature");
+ signedPreKeySignature.setContent(Base64.encodeToString(signedPreKeyRecord.getSignature(),Base64.DEFAULT));
+ final Element identityKeyElement = bundle.addChild("identityKey");
+ identityKeyElement.setContent(Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT));
+
+ final Element prekeys = bundle.addChild("prekeys", AxolotlService.PEP_PREFIX);
+ for(PreKeyRecord preKeyRecord:preKeyRecords) {
+ final Element prekey = prekeys.addChild("preKeyPublic");
+ prekey.setAttribute("preKeyId", preKeyRecord.getId());
+ prekey.setContent(Base64.encodeToString(preKeyRecord.getKeyPair().getPublicKey().serialize(), Base64.DEFAULT));
+ }
+
+ return publish(AxolotlService.PEP_BUNDLES+":"+deviceId, item);
+ }
+
+ public IqPacket publishVerification(byte[] signature, X509Certificate[] certificates, final int deviceId) {
+ final Element item = new Element("item");
+ final Element verification = item.addChild("verification", AxolotlService.PEP_PREFIX);
+ final Element chain = verification.addChild("chain");
+ for(int i = 0; i < certificates.length; ++i) {
+ try {
+ Element certificate = chain.addChild("certificate");
+ certificate.setContent(Base64.encodeToString(certificates[i].getEncoded(), Base64.DEFAULT));
+ certificate.setAttribute("index",i);
+ } catch (CertificateEncodingException e) {
+ Log.d(Config.LOGTAG, "could not encode certificate");
+ }
+ }
+ verification.addChild("signature").setContent(Base64.encodeToString(signature, Base64.DEFAULT));
+ return publish(AxolotlService.PEP_VERIFICATION+":"+deviceId, item);
+ }
+
+ public IqPacket queryMessageArchiveManagement(final MessageArchiveService.Query mam) {
+ final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
+ final Element query = packet.query("urn:xmpp:mam:0");
+ query.setAttribute("queryid", mam.getQueryId());
+ final Data data = new Data();
+ data.setFormType("urn:xmpp:mam:0");
+ if (mam.muc()) {
+ packet.setTo(mam.getWith());
+ } else if (mam.getWith()!=null) {
+ data.put("with", mam.getWith().toString());
+ }
+ data.put("start", getTimestamp(mam.getStart()));
+ data.put("end", getTimestamp(mam.getEnd()));
+ data.submit();
+ query.addChild(data);
+ if (mam.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) {
+ query.addChild("set", "http://jabber.org/protocol/rsm").addChild("before").setContent(mam.getReference());
+ } else if (mam.getReference() != null) {
+ query.addChild("set", "http://jabber.org/protocol/rsm").addChild("after").setContent(mam.getReference());
+ }
+ return packet;
+ }
+ public IqPacket generateGetBlockList() {
+ final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
+ iq.addChild("blocklist", Xmlns.BLOCKING);
+
+ return iq;
+ }
+
+ public IqPacket generateSetBlockRequest(final Jid jid) {
+ final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
+ final Element block = iq.addChild("block", Xmlns.BLOCKING);
+ block.addChild("item").setAttribute("jid", jid.toBareJid().toString());
+ return iq;
+ }
+
+ public IqPacket generateSetUnblockRequest(final Jid jid) {
+ final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
+ final Element block = iq.addChild("unblock", Xmlns.BLOCKING);
+ block.addChild("item").setAttribute("jid", jid.toBareJid().toString());
+ return iq;
+ }
+
+ public IqPacket generateSetPassword(final Account account, final String newPassword) {
+ final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
+ packet.setTo(account.getServer());
+ final Element query = packet.addChild("query", Xmlns.REGISTER);
+ final Jid jid = account.getJid();
+ query.addChild("username").setContent(jid.getLocalpart());
+ query.addChild("password").setContent(newPassword);
+ return packet;
+ }
+
+ public IqPacket changeAffiliation(Conversation conference, Jid jid, String affiliation) {
+ List<Jid> jids = new ArrayList<>();
+ jids.add(jid);
+ return changeAffiliation(conference,jids,affiliation);
+ }
+
+ public IqPacket changeAffiliation(Conversation conference, List<Jid> jids, String affiliation) {
+ IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
+ packet.setTo(conference.getJid().toBareJid());
+ packet.setFrom(conference.getAccount().getJid());
+ Element query = packet.query("http://jabber.org/protocol/muc#admin");
+ for(Jid jid : jids) {
+ Element item = query.addChild("item");
+ item.setAttribute("jid", jid.toString());
+ item.setAttribute("affiliation", affiliation);
+ }
+ return packet;
+ }
+
+ public IqPacket changeRole(Conversation conference, String nick, String role) {
+ IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
+ packet.setTo(conference.getJid().toBareJid());
+ packet.setFrom(conference.getAccount().getJid());
+ Element item = packet.query("http://jabber.org/protocol/muc#admin").addChild("item");
+ item.setAttribute("nick", nick);
+ item.setAttribute("role", role);
+ return packet;
+ }
+
+ public IqPacket generateCreateAccountWithCaptcha(Account account, String id, Data data) {
+ final IqPacket register = new IqPacket(IqPacket.TYPE.SET);
+
+ register.setTo(account.getServer());
+ register.setId(id);
+ register.query("jabber:iq:register").addChild(data);
+
+ return register;
+ }
+
+ public IqPacket pushTokenToAppServer(Jid appServer, String token, String deviceId) {
+ IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
+ packet.setTo(appServer);
+ Element command = packet.addChild("command", "http://jabber.org/protocol/commands");
+ command.setAttribute("node","register-push-gcm");
+ command.setAttribute("action","execute");
+ Data data = new Data();
+ data.put("token", token);
+ data.put("device-id", deviceId);
+ data.submit();
+ command.addChild(data);
+ return packet;
+ }
+
+ public IqPacket enablePush(Jid jid, String node, String secret) {
+ IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
+ Element enable = packet.addChild("enable","urn:xmpp:push:0");
+ enable.setAttribute("jid",jid.toString());
+ enable.setAttribute("node", node);
+ Data data = new Data();
+ data.setFormType("http://jabber.org/protocol/pubsub#publish-options");
+ data.put("secret",secret);
+ data.submit();
+ enable.addChild(data);
+ return packet;
+ }
+
+ public IqPacket queryAffiliation(Conversation conversation, String affiliation) {
+ IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
+ packet.setTo(conversation.getJid().toBareJid());
+ packet.query("http://jabber.org/protocol/muc#admin").addChild("item").setAttribute("affiliation",affiliation);
+ return packet;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java
new file mode 100644
index 00000000..582d64e6
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java
@@ -0,0 +1,217 @@
+package eu.siacs.conversations.generator;
+
+import net.java.otr4j.OtrException;
+import net.java.otr4j.session.Session;
+
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
+import de.thedevstack.conversationsplus.ConversationsPlusPreferences;
+import de.thedevstack.conversationsplus.entities.FileParams;
+import de.thedevstack.conversationsplus.xmpp.httpuploadim.HttpUploadHint;
+
+import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.chatstate.ChatState;
+import eu.siacs.conversations.xmpp.jid.Jid;
+import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
+
+public class MessageGenerator extends AbstractGenerator {
+
+ private MessagePacket preparePacket(Message message) {
+ Conversation conversation = message.getConversation();
+ Account account = conversation.getAccount();
+ MessagePacket packet = new MessagePacket();
+ if (conversation.getMode() == Conversation.MODE_SINGLE) {
+ packet.setTo(message.getCounterpart());
+ packet.setType(MessagePacket.TYPE_CHAT);
+ packet.addChild("markable", "urn:xmpp:chat-markers:0");
+ if (ConversationsPlusPreferences.indicateReceived()) {
+ packet.addChild("request", "urn:xmpp:receipts");
+ }
+ } else if (message.getType() == Message.TYPE_PRIVATE) {
+ packet.setTo(message.getCounterpart());
+ packet.setType(MessagePacket.TYPE_CHAT);
+ if (ConversationsPlusPreferences.indicateReceived()) {
+ packet.addChild("request", "urn:xmpp:receipts");
+ }
+ } else {
+ packet.setTo(message.getCounterpart().toBareJid());
+ packet.setType(MessagePacket.TYPE_GROUPCHAT);
+ }
+ packet.setFrom(account.getJid());
+ packet.setId(message.getUuid());
+ if (message.edited()) {
+ packet.addChild("replace","urn:xmpp:message-correct:0").setAttribute("id",message.getEditedId());
+ }
+ return packet;
+ }
+
+ public void addDelay(MessagePacket packet, long timestamp) {
+ final SimpleDateFormat mDateFormat = new SimpleDateFormat(
+ "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
+ mDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+ Element delay = packet.addChild("delay", "urn:xmpp:delay");
+ Date date = new Date(timestamp);
+ delay.setAttribute("stamp", mDateFormat.format(date));
+ }
+
+ public MessagePacket generateAxolotlChat(Message message, XmppAxolotlMessage axolotlMessage) {
+ MessagePacket packet = preparePacket(message);
+ if (axolotlMessage == null) {
+ return null;
+ }
+ packet.setAxolotlMessage(axolotlMessage.toElement());
+ packet.addChild("store", "urn:xmpp:hints");
+ return packet;
+ }
+
+ public static void addXhtmlImImage(MessagePacket packet, FileParams params) {
+ Element html = packet.addChild("html", "http://jabber.org/protocol/xhtml-im");
+ Element body = html.addChild("body", "http://www.w3.org/1999/xhtml");
+ Element img = body.addChild("img");
+ img.setAttribute("src", params.getUrl());
+ img.setAttribute("height", params.getHeight());
+ img.setAttribute("width", params.getWidth());
+ }
+
+ public static void addMessageHints(MessagePacket packet) {
+ packet.addChild("private", "urn:xmpp:carbons:2");
+ packet.addChild("no-copy", "urn:xmpp:hints");
+ packet.addChild("no-permanent-store", "urn:xmpp:hints");
+ packet.addChild("no-permanent-storage", "urn:xmpp:hints"); //do not copy this. this is wrong. it is *store*
+ }
+
+ public MessagePacket generateOtrChat(Message message) {
+ Session otrSession = message.getConversation().getOtrSession();
+ if (otrSession == null) {
+ return null;
+ }
+ MessagePacket packet = preparePacket(message);
+ addMessageHints(packet);
+ try {
+ String content = message.getBody();
+ packet.setBody(otrSession.transformSending(content)[0]);
+ return packet;
+ } catch (OtrException e) {
+ return null;
+ }
+ }
+
+ public MessagePacket generateChat(Message message) {
+ MessagePacket packet = preparePacket(message);
+ String content;
+ if (message.hasFileOnRemoteHost()) {
+ FileParams fileParams = message.getFileParams();
+ content = message.getBody();
+ if (message.isHttpUploaded()) {
+ packet.addChild(new HttpUploadHint());
+ }
+ packet.addChild("x","jabber:x:oob").addChild("url").setContent(content);
+ if (fileParams.getWidth() > 0 && fileParams.getHeight() > 0) {
+ addXhtmlImImage(packet, fileParams);
+ }
+ } else {
+ content = message.getBody();
+ }
+ packet.setBody(content);
+ return packet;
+ }
+
+ public MessagePacket generatePgpChat(Message message) {
+ MessagePacket packet = preparePacket(message);
+ packet.setBody("This is an XEP-0027 encrypted message");
+ if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
+ packet.addChild("x", "jabber:x:encrypted").setContent(message.getEncryptedBody());
+ } else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
+ packet.addChild("x", "jabber:x:encrypted").setContent(message.getBody());
+ }
+ return packet;
+ }
+
+ public MessagePacket generateChatState(Conversation conversation) {
+ final Account account = conversation.getAccount();
+ MessagePacket packet = new MessagePacket();
+ packet.setType(MessagePacket.TYPE_CHAT);
+ packet.setTo(conversation.getJid().toBareJid());
+ packet.setFrom(account.getJid());
+ packet.addChild(ChatState.toElement(conversation.getOutgoingChatState()));
+ packet.addChild("no-store", "urn:xmpp:hints");
+ packet.addChild("no-storage", "urn:xmpp:hints"); //wrong! don't copy this. Its *store*
+ return packet;
+ }
+
+ public MessagePacket confirm(final Account account, final Jid to, final String id) {
+ MessagePacket packet = new MessagePacket();
+ packet.setType(MessagePacket.TYPE_CHAT);
+ packet.setTo(to);
+ packet.setFrom(account.getJid());
+ Element received = packet.addChild("displayed", "urn:xmpp:chat-markers:0");
+ received.setAttribute("id", id);
+ return packet;
+ }
+
+ public MessagePacket conferenceSubject(Conversation conversation,String subject) {
+ MessagePacket packet = new MessagePacket();
+ packet.setType(MessagePacket.TYPE_GROUPCHAT);
+ packet.setTo(conversation.getJid().toBareJid());
+ Element subjectChild = new Element("subject");
+ subjectChild.setContent(subject);
+ packet.addChild(subjectChild);
+ packet.setFrom(conversation.getAccount().getJid().toBareJid());
+ return packet;
+ }
+
+ public MessagePacket directInvite(final Conversation conversation, final Jid contact) {
+ MessagePacket packet = new MessagePacket();
+ packet.setType(MessagePacket.TYPE_NORMAL);
+ packet.setTo(contact);
+ packet.setFrom(conversation.getAccount().getJid());
+ Element x = packet.addChild("x", "jabber:x:conference");
+ x.setAttribute("jid", conversation.getJid().toBareJid().toString());
+ return packet;
+ }
+
+ public MessagePacket invite(Conversation conversation, Jid contact) {
+ MessagePacket packet = new MessagePacket();
+ packet.setTo(conversation.getJid().toBareJid());
+ packet.setFrom(conversation.getAccount().getJid());
+ Element x = new Element("x");
+ x.setAttribute("xmlns", "http://jabber.org/protocol/muc#user");
+ Element invite = new Element("invite");
+ invite.setAttribute("to", contact.toBareJid().toString());
+ x.addChild(invite);
+ packet.addChild(x);
+ return packet;
+ }
+
+ public MessagePacket received(Account account, MessagePacket originalMessage, ArrayList<String> namespaces, int type) {
+ MessagePacket receivedPacket = new MessagePacket();
+ receivedPacket.setType(type);
+ receivedPacket.setTo(originalMessage.getFrom());
+ receivedPacket.setFrom(account.getJid());
+ for(String namespace : namespaces) {
+ receivedPacket.addChild("received", namespace).setAttribute("id", originalMessage.getId());
+ }
+ return receivedPacket;
+ }
+
+ public MessagePacket generateOtrError(Jid to, String id, String errorText) {
+ MessagePacket packet = new MessagePacket();
+ packet.setType(MessagePacket.TYPE_ERROR);
+ packet.setAttribute("id",id);
+ packet.setTo(to);
+ Element error = packet.addChild("error");
+ error.setAttribute("code","406");
+ error.setAttribute("type","modify");
+ error.addChild("not-acceptable","urn:ietf:params:xml:ns:xmpp-stanzas");
+ error.addChild("text").setContent("?OTR Error:" + errorText);
+ return packet;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java b/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java
new file mode 100644
index 00000000..9ac7d318
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java
@@ -0,0 +1,62 @@
+package eu.siacs.conversations.generator;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Presence;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
+
+public class PresenceGenerator extends AbstractGenerator {
+
+ private PresencePacket subscription(String type, Contact contact) {
+ PresencePacket packet = new PresencePacket();
+ packet.setAttribute("type", type);
+ packet.setTo(contact.getJid());
+ packet.setFrom(contact.getAccount().getJid().toBareJid());
+ return packet;
+ }
+
+ public PresencePacket requestPresenceUpdatesFrom(Contact contact) {
+ return subscription("subscribe", contact);
+ }
+
+ public PresencePacket stopPresenceUpdatesFrom(Contact contact) {
+ return subscription("unsubscribe", contact);
+ }
+
+ public PresencePacket stopPresenceUpdatesTo(Contact contact) {
+ return subscription("unsubscribed", contact);
+ }
+
+ public PresencePacket sendPresenceUpdatesTo(Contact contact) {
+ return subscription("subscribed", contact);
+ }
+
+ public PresencePacket selfPresence(Account account, Presence.Status status) {
+ PresencePacket packet = new PresencePacket();
+ if(status.toShowString() != null) {
+ packet.addChild("show").setContent(status.toShowString());
+ }
+ packet.setFrom(account.getJid());
+ String sig = account.getPgpSignature();
+ if (sig != null) {
+ packet.addChild("x", "jabber:x:signed").setContent(sig);
+ }
+ String capHash = getCapHash();
+ if (capHash != null) {
+ Element cap = packet.addChild("c",
+ "http://jabber.org/protocol/caps");
+ cap.setAttribute("hash", "sha-1");
+ cap.setAttribute("node", "http://conversations.im");
+ cap.setAttribute("ver", capHash);
+ }
+ return packet;
+ }
+
+ public PresencePacket sendOfflinePresence(Account account) {
+ PresencePacket packet = new PresencePacket();
+ packet.setFrom(account.getJid());
+ packet.setAttribute("type","unavailable");
+ return packet;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java
new file mode 100644
index 00000000..e3398f93
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java
@@ -0,0 +1,92 @@
+package eu.siacs.conversations.http;
+
+import org.apache.http.conn.ssl.StrictHostnameVerifier;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.X509TrustManager;
+
+import de.thedevstack.conversationsplus.ConversationsPlusApplication;
+
+import de.thedevstack.conversationsplus.utils.MessageUtil;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.services.AbstractConnectionManager;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.utils.SSLSocketHelper;
+
+public class HttpConnectionManager extends AbstractConnectionManager {
+ private static HttpConnectionManager INSTANCE;
+
+ public static void init() {
+ INSTANCE = new HttpConnectionManager();
+ }
+
+ private List<HttpDownloadConnection> downloadConnections = new CopyOnWriteArrayList<>();
+
+ public static HttpDownloadConnection createNewDownloadConnection(Message message) {
+ return createNewDownloadConnection(message, false);
+ }
+
+ public static HttpDownloadConnection createNewDownloadConnection(Message message, boolean interactive) {
+ if (MessageUtil.needsDownload(message)) {
+ HttpDownloadConnection connection = new HttpDownloadConnection(INSTANCE);
+ connection.init(message, interactive);
+ INSTANCE.downloadConnections.add(connection);
+ return connection;
+ }
+ return null;
+ }
+
+ public void finishConnection(HttpDownloadConnection connection) {
+ this.downloadConnections.remove(connection);
+ }
+
+ public static void setupTrustManager(final HttpsURLConnection connection, final boolean interactive) {
+ final X509TrustManager trustManager;
+ final HostnameVerifier hostnameVerifier;
+ if (interactive) {
+ trustManager = ConversationsPlusApplication.getMemorizingTrustManager();
+ hostnameVerifier = ConversationsPlusApplication.getMemorizingTrustManager().wrapHostnameVerifier(
+ new StrictHostnameVerifier());
+ } else {
+ trustManager = ConversationsPlusApplication.getMemorizingTrustManager()
+ .getNonInteractive();
+ hostnameVerifier = ConversationsPlusApplication.getMemorizingTrustManager()
+ .wrapHostnameVerifierNonInteractive(
+ new StrictHostnameVerifier());
+ }
+ try {
+ final SSLContext sc = SSLSocketHelper.getSSLContext();
+ sc.init(null, new X509TrustManager[]{trustManager},
+ ConversationsPlusApplication.getSecureRandom());
+
+ final SSLSocketFactory sf = sc.getSocketFactory();
+ final String[] cipherSuites = CryptoHelper.getOrderedCipherSuites(
+ sf.getSupportedCipherSuites());
+ if (cipherSuites.length > 0) {
+ sc.getDefaultSSLParameters().setCipherSuites(cipherSuites);
+
+ }
+
+ connection.setSSLSocketFactory(sf);
+ connection.setHostnameVerifier(hostnameVerifier);
+ } catch (final KeyManagementException | NoSuchAlgorithmException ignored) {
+ }
+ }
+
+ public Proxy getProxy() throws IOException {
+ return new Proxy(Proxy.Type.HTTP, new InetSocketAddress(InetAddress.getLocalHost(), 8118));
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java
new file mode 100644
index 00000000..77b8e333
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java
@@ -0,0 +1,376 @@
+package eu.siacs.conversations.http;
+
+import android.os.PowerManager;
+import android.util.Log;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.concurrent.CancellationException;
+
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLHandshakeException;
+
+import de.thedevstack.android.logcat.Logging;
+import de.thedevstack.conversationsplus.ConversationsPlusApplication;
+import de.thedevstack.conversationsplus.ConversationsPlusPreferences;
+import de.thedevstack.conversationsplus.entities.FileParams;
+import de.thedevstack.conversationsplus.enums.FileStatus;
+import de.thedevstack.conversationsplus.exceptions.RemoteFileNotFoundException;
+import de.thedevstack.conversationsplus.utils.MessageUtil;
+import de.thedevstack.conversationsplus.utils.StreamUtil;
+import de.thedevstack.conversationsplus.utils.XmppConnectionServiceAccessor;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.Transferable;
+import eu.siacs.conversations.entities.TransferablePlaceholder;
+import eu.siacs.conversations.persistance.FileBackend;
+import eu.siacs.conversations.services.AbstractConnectionManager;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.utils.FileUtils;
+
+public class HttpDownloadConnection implements Transferable {
+
+ private HttpConnectionManager mHttpConnectionManager;
+ private XmppConnectionService mXmppConnectionService;
+
+ private URL mUrl;
+ private Message message;
+ private DownloadableFile file;
+ private int mStatus = Transferable.STATUS_UNKNOWN;
+ private boolean acceptedAutomatically = false;
+ private int mProgress = 0;
+ private boolean canceled = false;
+
+ public HttpDownloadConnection(HttpConnectionManager manager) {
+ this.mHttpConnectionManager = manager;
+ this.mXmppConnectionService = XmppConnectionServiceAccessor.xmppConnectionService;
+ }
+
+ @Override
+ public boolean start() {
+ if (mXmppConnectionService.hasInternetConnection()) {
+ if (this.mStatus == STATUS_OFFER_CHECK_FILESIZE) {
+ checkFileSize(true);
+ } else {
+ new Thread(new FileDownloader(true)).start();
+ }
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public void init(Message message) {
+ init(message, false);
+ }
+
+ public void init(Message message, boolean interactive) {
+ this.message = message;
+ this.message.setTransferable(this);
+ try {
+ String url = (null != message && null != message.getFileParams()) ? message.getFileParams().getUrl() : null;
+ if (null == url) {
+ /*
+ * If this code is reached and the URL is null something went wrong.
+ * Try again to extract the file parameters from the message.
+ */
+ MessageUtil.extractFileParamsFromBody(message);
+ url = (null != message.getFileParams()) ? message.getFileParams().getUrl() : null;
+ if (null == url) {
+ message.setTreatAsDownloadable(Message.Decision.NEVER); // TODO find sth better
+ this.cancel();
+ return;
+ }
+ }
+ mUrl = new URL(url);
+ final String sUrlFilename = mUrl.getPath().substring(mUrl.getPath().lastIndexOf('/') + 1).toLowerCase();
+ final String lastPart = FileUtils.getLastExtension(sUrlFilename);
+
+ if (!lastPart.isEmpty() && ("pgp".equals(lastPart) || "gpg".equals(lastPart))) {
+ this.message.setEncryption(Message.ENCRYPTION_PGP);
+ } else if (message.getEncryption() != Message.ENCRYPTION_OTR
+ && message.getEncryption() != Message.ENCRYPTION_AXOLOTL) {
+ this.message.setEncryption(Message.ENCRYPTION_NONE);
+ }
+
+ String extension;
+ String originalFilename;
+ if (!lastPart.isEmpty() && VALID_CRYPTO_EXTENSIONS.contains(lastPart)) {
+ extension = FileUtils.getSecondToLastExtension(sUrlFilename);
+ originalFilename = sUrlFilename.replace("." + lastPart, "");
+ } else {
+ extension = lastPart;
+ originalFilename = sUrlFilename;
+ }
+ message.setRelativeFilePath(message.getUuid() + "." + extension);
+ this.file = FileBackend.getFile(message, false);
+
+ FileParams fileParams = message.getFileParams();
+ if (null == fileParams) {
+ fileParams = new FileParams();
+ message.setFileParams(fileParams);
+ }
+ fileParams.setOriginalFilename(originalFilename);
+
+ if ((this.message.getEncryption() == Message.ENCRYPTION_OTR
+ || this.message.getEncryption() == Message.ENCRYPTION_AXOLOTL)
+ && this.file.getKey() == null) {
+ this.message.setEncryption(Message.ENCRYPTION_NONE);
+ }
+ checkFileSize(interactive);
+ } catch (MalformedURLException e) {
+ this.cancel();
+ }
+ }
+
+ private void checkFileSize(boolean interactive) {
+ new Thread(new FileSizeChecker(interactive)).start();
+ }
+
+ @Override
+ public void cancel() {
+ this.canceled = true;
+ mHttpConnectionManager.finishConnection(this);
+ if (message.isFileOrImage()) {
+ message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED));
+ } else {
+ message.setTransferable(null);
+ }
+ mXmppConnectionService.updateConversationUi();
+ }
+
+ private void finish() {
+ FileBackend.updateMediaScanner(file, mXmppConnectionService);
+ message.setTransferable(null);
+ MessageUtil.setAndSaveFileStatus(this.message, FileStatus.DOWNLOADED);
+ mHttpConnectionManager.finishConnection(this);
+ if (message.getEncryption() == Message.ENCRYPTION_PGP) {
+ message.getConversation().getAccount().getPgpDecryptionService().add(message);
+ }
+ mXmppConnectionService.updateConversationUi();
+ if (acceptedAutomatically) {
+ mXmppConnectionService.getNotificationService().push(message);
+ }
+ }
+
+ private void changeStatus(int status) {
+ this.mStatus = status;
+ mXmppConnectionService.updateConversationUi();
+ }
+
+ private void showToastForException(Exception e) {
+ e.printStackTrace();
+ if (e instanceof java.net.UnknownHostException) {
+ mXmppConnectionService.showErrorToastInUi(R.string.download_failed_server_not_found);
+ } else if (e instanceof java.net.ConnectException) {
+ mXmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_connect);
+ } else if (!(e instanceof CancellationException)) {
+ mXmppConnectionService.showErrorToastInUi(R.string.download_failed_file_not_found);
+ }
+ }
+
+ private class FileSizeChecker implements Runnable {
+
+ private boolean interactive = false;
+
+ public FileSizeChecker(boolean interactive) {
+ this.interactive = interactive;
+ }
+
+ @Override
+ public void run() {
+ long size;
+ try {
+ size = retrieveFileSize();
+ } catch (SSLHandshakeException e) {
+ changeStatus(STATUS_OFFER_CHECK_FILESIZE);
+ HttpDownloadConnection.this.acceptedAutomatically = false;
+ HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message);
+ return;
+ } catch (RemoteFileNotFoundException e) {
+ message.setNoDownloadable(); // TODO Set remote file status to not-available
+ cancel();
+ return;
+ } catch (IOException e) {
+ Log.d(Config.LOGTAG, "io exception in http file size checker: " + e.getMessage());
+ if (interactive) {
+ showToastForException(e);
+ }
+ cancel();
+ return;
+ }
+ file.setExpectedSize(size);
+ if (mHttpConnectionManager.hasStoragePermission()
+ && size != -1
+ && size <= ConversationsPlusPreferences.autoAcceptFileSize()
+ && mXmppConnectionService.isDownloadAllowedInConnection()) {
+ HttpDownloadConnection.this.acceptedAutomatically = true;
+ new Thread(new FileDownloader(interactive)).start();
+ } else {
+ changeStatus(STATUS_OFFER);
+ HttpDownloadConnection.this.acceptedAutomatically = false;
+ HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message);
+ }
+ }
+
+ private long retrieveFileSize() throws IOException {
+ try {
+ Logging.d(Config.LOGTAG, "retrieve file size. interactive:" + String.valueOf(interactive));
+ changeStatus(STATUS_CHECKING);
+ HttpURLConnection connection = (HttpURLConnection) mUrl.openConnection();
+ connection.setRequestMethod("HEAD");
+ Logging.d(Config.LOGTAG, "url: " + connection.getURL().toString());
+ Logging.d(Config.LOGTAG, "connection: " + connection.toString());
+ connection.setRequestProperty("User-Agent", ConversationsPlusApplication.getNameAndVersion());
+ // https://code.google.com/p/android/issues/detail?id=24672
+ connection.setRequestProperty("Accept-Encoding", "");
+ if (connection instanceof HttpsURLConnection) {
+ mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive);
+ }
+ connection.connect();
+ if (connection.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) {
+ Logging.d(Config.LOGTAG, "remote file not found");
+ throw new RemoteFileNotFoundException();
+ }
+ String contentLength = connection.getHeaderField("Content-Length");
+ connection.disconnect();
+ if (contentLength == null) {
+ return -1;
+ }
+ return Long.parseLong(contentLength, 10);
+ } catch (RemoteFileNotFoundException e) {
+ throw e;
+ } catch (IOException e) {
+ return -1;
+ } catch (NumberFormatException e) {
+ return -1;
+ }
+ }
+
+ }
+
+ private class FileDownloader implements Runnable {
+
+ private boolean interactive = false;
+
+ private OutputStream os;
+
+ public FileDownloader(boolean interactive) {
+ this.interactive = interactive;
+ }
+
+ @Override
+ public void run() {
+ try {
+ changeStatus(STATUS_DOWNLOADING);
+ download();
+ updateImageBounds();
+ finish();
+ } catch (SSLHandshakeException e) {
+ changeStatus(STATUS_OFFER);
+ } catch (Exception e) {
+ if (interactive) {
+ showToastForException(e);
+ }
+ cancel();
+ }
+ }
+
+ private void download() throws SSLHandshakeException, IOException {
+ InputStream is = null;
+ PowerManager.WakeLock wakeLock = ConversationsPlusApplication.createPartialWakeLock("http_download_"+message.getUuid());
+ try {
+ wakeLock.acquire();
+ HttpURLConnection connection = (HttpURLConnection) mUrl.openConnection();
+ if (connection instanceof HttpsURLConnection) {
+ mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive);
+ }
+ connection.setRequestProperty("User-Agent", ConversationsPlusApplication.getNameAndVersion());
+ final boolean tryResume = file.exists() && file.getKey() == null;
+ if (tryResume) {
+ Logging.d(Config.LOGTAG, "http download trying resume");
+ long size = file.getSize();
+ connection.setRequestProperty("Range", "bytes="+size+"-");
+ }
+ connection.connect();
+ is = new BufferedInputStream(connection.getInputStream());
+ boolean serverResumed = "bytes".equals(connection.getHeaderField("Accept-Ranges"));
+ long transmitted = 0;
+ long expected = file.getExpectedSize();
+ if (tryResume && serverResumed) {
+ Logging.d(Config.LOGTAG, "server resumed");
+ transmitted = file.getSize();
+ updateProgress((int) ((((double) transmitted) / expected) * 100));
+ os = AbstractConnectionManager.createAppendedOutputStream(file);
+ } else {
+ file.getParentFile().mkdirs();
+ file.createNewFile();
+ os = AbstractConnectionManager.createOutputStream(file, true);
+ }
+ int count = -1;
+ byte[] buffer = new byte[1024];
+ while ((count = is.read(buffer)) != -1) {
+ transmitted += count;
+ os.write(buffer, 0, count);
+ updateProgress((int) ((((double) transmitted) / expected) * 100));
+ if (canceled) {
+ throw new CancellationException();
+ }
+ }
+ } catch (CancellationException | IOException e) {
+ throw e;
+ } finally {
+ if (os != null) {
+ try {
+ os.flush();
+ } catch (final IOException ignored) {
+
+ }
+ }
+ StreamUtil.close(os);
+ StreamUtil.close(is);
+ wakeLock.release();
+ }
+ }
+
+ private void updateImageBounds() {
+ message.setType(Message.TYPE_FILE);
+ MessageUtil.updateFileParams(message, mUrl);
+ mXmppConnectionService.updateMessage(message);
+ }
+
+ }
+
+ public void updateProgress(int i) {
+ this.mProgress = i;
+ mXmppConnectionService.updateConversationUi();
+ }
+
+ @Override
+ public int getStatus() {
+ return this.mStatus;
+ }
+
+ @Override
+ public long getFileSize() {
+ if (this.file != null) {
+ return this.file.getExpectedSize();
+ } else {
+ return 0;
+ }
+ }
+
+ @Override
+ public int getProgress() {
+ return this.mProgress;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java
new file mode 100644
index 00000000..ad368f11
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java
@@ -0,0 +1,96 @@
+package eu.siacs.conversations.parser;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.jid.Jid;
+import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
+
+public abstract class AbstractParser {
+
+ protected XmppConnectionService mXmppConnectionService;
+
+ protected AbstractParser(XmppConnectionService service) {
+ this.mXmppConnectionService = service;
+ }
+
+ /**
+ * Gets the timestamp from the 'delay' element.
+ * Refer to XEP-0203: Delayed Delivery for details. @link{http://xmpp.org/extensions/xep-0203.html}
+ * @param element the element to find the child element 'delay' in.
+ * @return the time in milli seconds of the attribute 'stamp' of the
+ * element 'delay'. In case there is no 'delay' element or no 'stamp'
+ * attribute or the current time is less than the value of the 'stamp'
+ * attribute the current time is returned.
+ */
+ public static Long getTimestamp(Element element, Long defaultValue) {
+ Element delay = element.findChild("delay","urn:xmpp:delay");
+ if (delay != null) {
+ String stamp = delay.getAttribute("stamp");
+ if (stamp != null) {
+ try {
+ return AbstractParser.parseTimestamp(delay.getAttribute("stamp")).getTime();
+ } catch (ParseException e) {
+ return defaultValue;
+ }
+ }
+ }
+ return defaultValue;
+ }
+
+ protected long getTimestamp(Element packet) {
+ return getTimestamp(packet,System.currentTimeMillis());
+ }
+
+ /**
+ * Parses the timestamp according to XEP-0082: XMPP Date and Time Profiles.
+ * @link{http://xmpp.org/extensions/xep-0082.html}
+ *
+ * @param timestamp the timestamp to parse
+ * @return Date
+ * @throws ParseException
+ */
+ public static Date parseTimestamp(String timestamp) throws ParseException {
+ /*try {
+ Logging.d("TIMESTAMP", timestamp);
+ return DatatypeFactory.newInstance().newXMLGregorianCalendar(timestamp).toGregorianCalendar().getTime();
+ } catch (DatatypeConfigurationException e) {
+ Logging.d("TIMESTAMP", e.getMessage());
+ return new Date();
+ }*/
+ timestamp = timestamp.replace("Z", "+0000");
+ SimpleDateFormat dateFormat;
+ timestamp = timestamp.substring(0,19)+timestamp.substring(timestamp.length() -5,timestamp.length());
+ dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ",Locale.US);
+ return dateFormat.parse(timestamp);
+ }
+
+ protected void updateLastseen(final AbstractStanza packet, final Account account, final boolean presenceOverwrite) {
+ updateLastseen(getTimestamp(packet), account, packet.getFrom(), presenceOverwrite);
+ }
+
+ protected void updateLastseen(long timestamp, final Account account, final Jid from, final boolean presenceOverwrite) {
+ final String presence = from == null || from.isBareJid() ? "" : from.getResourcepart();
+ final Contact contact = account.getRoster().getContact(from);
+ if (timestamp >= contact.lastseen.time) {
+ contact.lastseen.time = timestamp;
+ if (!presence.isEmpty() && presenceOverwrite) {
+ contact.lastseen.presence = presence;
+ }
+ }
+ }
+
+ protected String avatarData(Element items) {
+ Element item = items.findChild("item");
+ if (item == null) {
+ return null;
+ }
+ return item.findChildContent("data", "urn:xmpp:avatar:data");
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/parser/IqParser.java b/src/main/java/eu/siacs/conversations/parser/IqParser.java
new file mode 100644
index 00000000..c03ed42f
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/parser/IqParser.java
@@ -0,0 +1,355 @@
+package eu.siacs.conversations.parser;
+
+import android.support.annotation.NonNull;
+import android.util.Base64;
+import android.util.Log;
+import android.util.Pair;
+
+import org.whispersystems.libaxolotl.IdentityKey;
+import org.whispersystems.libaxolotl.ecc.Curve;
+import org.whispersystems.libaxolotl.ecc.ECPublicKey;
+import org.whispersystems.libaxolotl.state.PreKeyBundle;
+
+import java.io.ByteArrayInputStream;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import de.thedevstack.android.logcat.Logging;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.crypto.axolotl.AxolotlService;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.services.AvatarService;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.utils.Xmlns;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.OnIqPacketReceived;
+import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
+import eu.siacs.conversations.xmpp.jid.Jid;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+public class IqParser extends AbstractParser implements OnIqPacketReceived {
+
+ public IqParser(final XmppConnectionService service) {
+ super(service);
+ }
+
+ private void rosterItems(final Account account, final Element query) {
+ final String version = query.getAttribute("ver");
+ if (version != null) {
+ account.getRoster().setVersion(version);
+ }
+ for (final Element item : query.getChildren()) {
+ if (item.getName().equals("item")) {
+ final Jid jid = item.getAttributeAsJid("jid");
+ if (jid == null) {
+ continue;
+ }
+ final String name = item.getAttribute("name");
+ final String subscription = item.getAttribute("subscription");
+ final Contact contact = account.getRoster().getContact(jid);
+ if (!contact.getOption(Contact.Options.DIRTY_PUSH)) {
+ contact.setServerName(name);
+ contact.parseGroupsFromElement(item);
+ }
+ if (subscription != null) {
+ if (subscription.equals("remove")) {
+ contact.resetOption(Contact.Options.IN_ROSTER);
+ contact.resetOption(Contact.Options.DIRTY_DELETE);
+ contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
+ } else {
+ contact.setOption(Contact.Options.IN_ROSTER);
+ contact.resetOption(Contact.Options.DIRTY_PUSH);
+ contact.parseSubscriptionFromElement(item);
+ }
+ }
+ AvatarService.getInstance().clear(contact);
+ }
+ }
+ mXmppConnectionService.updateConversationUi();
+ mXmppConnectionService.updateRosterUi();
+ }
+
+ public String avatarData(final IqPacket packet) {
+ final Element pubsub = packet.findChild("pubsub",
+ "http://jabber.org/protocol/pubsub");
+ if (pubsub == null) {
+ return null;
+ }
+ final Element items = pubsub.findChild("items");
+ if (items == null) {
+ return null;
+ }
+ return super.avatarData(items);
+ }
+
+ public Element getItem(final IqPacket packet) {
+ final Element pubsub = packet.findChild("pubsub",
+ "http://jabber.org/protocol/pubsub");
+ if (pubsub == null) {
+ return null;
+ }
+ final Element items = pubsub.findChild("items");
+ if (items == null) {
+ return null;
+ }
+ return items.findChild("item");
+ }
+
+ @NonNull
+ public Set<Integer> deviceIds(final Element item) {
+ Set<Integer> deviceIds = new HashSet<>();
+ if (item != null) {
+ final Element list = item.findChild("list");
+ if (list != null) {
+ for (Element device : list.getChildren()) {
+ if (!device.getName().equals("device")) {
+ continue;
+ }
+ try {
+ Integer id = Integer.valueOf(device.getAttribute("id"));
+ deviceIds.add(id);
+ } catch (NumberFormatException e) {
+ Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Encountered invalid <device> node in PEP ("+e.getMessage()+"):" + device.toString()+ ", skipping...");
+ continue;
+ }
+ }
+ }
+ }
+ return deviceIds;
+ }
+
+ public Integer signedPreKeyId(final Element bundle) {
+ final Element signedPreKeyPublic = bundle.findChild("signedPreKeyPublic");
+ if(signedPreKeyPublic == null) {
+ return null;
+ }
+ return Integer.valueOf(signedPreKeyPublic.getAttribute("signedPreKeyId"));
+ }
+
+ public ECPublicKey signedPreKeyPublic(final Element bundle) {
+ ECPublicKey publicKey = null;
+ final Element signedPreKeyPublic = bundle.findChild("signedPreKeyPublic");
+ if(signedPreKeyPublic == null) {
+ return null;
+ }
+ try {
+ publicKey = Curve.decodePoint(Base64.decode(signedPreKeyPublic.getContent(),Base64.DEFAULT), 0);
+ } catch (Throwable e) {
+ Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Invalid signedPreKeyPublic in PEP: " + e.getMessage());
+ }
+ return publicKey;
+ }
+
+ public byte[] signedPreKeySignature(final Element bundle) {
+ final Element signedPreKeySignature = bundle.findChild("signedPreKeySignature");
+ if(signedPreKeySignature == null) {
+ return null;
+ }
+ try {
+ return Base64.decode(signedPreKeySignature.getContent(), Base64.DEFAULT);
+ } catch (Throwable e) {
+ Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : Invalid base64 in signedPreKeySignature");
+ return null;
+ }
+ }
+
+ public IdentityKey identityKey(final Element bundle) {
+ IdentityKey identityKey = null;
+ final Element identityKeyElement = bundle.findChild("identityKey");
+ if(identityKeyElement == null) {
+ return null;
+ }
+ try {
+ identityKey = new IdentityKey(Base64.decode(identityKeyElement.getContent(), Base64.DEFAULT), 0);
+ } catch (Throwable e) {
+ Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Invalid identityKey in PEP: "+e.getMessage());
+ }
+ return identityKey;
+ }
+
+ public Map<Integer, ECPublicKey> preKeyPublics(final IqPacket packet) {
+ Map<Integer, ECPublicKey> preKeyRecords = new HashMap<>();
+ Element item = getItem(packet);
+ if (item == null) {
+ Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Couldn't find <item> in bundle IQ packet: " + packet);
+ return null;
+ }
+ final Element bundleElement = item.findChild("bundle");
+ if(bundleElement == null) {
+ return null;
+ }
+ final Element prekeysElement = bundleElement.findChild("prekeys");
+ if(prekeysElement == null) {
+ Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Couldn't find <prekeys> in bundle IQ packet: " + packet);
+ return null;
+ }
+ for(Element preKeyPublicElement : prekeysElement.getChildren()) {
+ if(!preKeyPublicElement.getName().equals("preKeyPublic")){
+ Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Encountered unexpected tag in prekeys list: " + preKeyPublicElement);
+ continue;
+ }
+ Integer preKeyId = Integer.valueOf(preKeyPublicElement.getAttribute("preKeyId"));
+ try {
+ ECPublicKey preKeyPublic = Curve.decodePoint(Base64.decode(preKeyPublicElement.getContent(), Base64.DEFAULT), 0);
+ preKeyRecords.put(preKeyId, preKeyPublic);
+ } catch (Throwable e) {
+ Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Invalid preKeyPublic (ID="+preKeyId+") in PEP: "+ e.getMessage()+", skipping...");
+ continue;
+ }
+ }
+ return preKeyRecords;
+ }
+
+ public Pair<X509Certificate[],byte[]> verification(final IqPacket packet) {
+ Element item = getItem(packet);
+ Element verification = item != null ? item.findChild("verification", AxolotlService.PEP_PREFIX) : null;
+ Element chain = verification != null ? verification.findChild("chain") : null;
+ Element signature = verification != null ? verification.findChild("signature") : null;
+ if (chain != null && signature != null) {
+ List<Element> certElements = chain.getChildren();
+ X509Certificate[] certificates = new X509Certificate[certElements.size()];
+ try {
+ CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
+ int i = 0;
+ for(Element cert : certElements) {
+ certificates[i] = (X509Certificate) certificateFactory.generateCertificate(new ByteArrayInputStream(Base64.decode(cert.getContent(),Base64.DEFAULT)));
+ ++i;
+ }
+ return new Pair<>(certificates,Base64.decode(signature.getContent(),Base64.DEFAULT));
+ } catch (CertificateException e) {
+ return null;
+ }
+ } else {
+ return null;
+ }
+ }
+
+ public PreKeyBundle bundle(final IqPacket bundle) {
+ Element bundleItem = getItem(bundle);
+ if(bundleItem == null) {
+ return null;
+ }
+ final Element bundleElement = bundleItem.findChild("bundle");
+ if(bundleElement == null) {
+ return null;
+ }
+ ECPublicKey signedPreKeyPublic = signedPreKeyPublic(bundleElement);
+ Integer signedPreKeyId = signedPreKeyId(bundleElement);
+ byte[] signedPreKeySignature = signedPreKeySignature(bundleElement);
+ IdentityKey identityKey = identityKey(bundleElement);
+ if(signedPreKeyPublic == null || identityKey == null) {
+ return null;
+ }
+
+ return new PreKeyBundle(0, 0, 0, null,
+ signedPreKeyId, signedPreKeyPublic, signedPreKeySignature, identityKey);
+ }
+
+ public List<PreKeyBundle> preKeys(final IqPacket preKeys) {
+ List<PreKeyBundle> bundles = new ArrayList<>();
+ Map<Integer, ECPublicKey> preKeyPublics = preKeyPublics(preKeys);
+ if ( preKeyPublics != null) {
+ for (Integer preKeyId : preKeyPublics.keySet()) {
+ ECPublicKey preKeyPublic = preKeyPublics.get(preKeyId);
+ bundles.add(new PreKeyBundle(0, 0, preKeyId, preKeyPublic,
+ 0, null, null, null));
+ }
+ }
+
+ return bundles;
+ }
+
+ @Override
+ public void onIqPacketReceived(final Account account, final IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE.ERROR || packet.getType() == IqPacket.TYPE.TIMEOUT) {
+ return;
+ } else if (packet.hasChild("query", Xmlns.ROSTER) && packet.fromServer(account)) {
+ final Element query = packet.findChild("query");
+ // If this is in response to a query for the whole roster:
+ if (packet.getType() == IqPacket.TYPE.RESULT) {
+ account.getRoster().markAllAsNotInRoster();
+ }
+ this.rosterItems(account, query);
+ } else if ((packet.hasChild("block", Xmlns.BLOCKING) || packet.hasChild("blocklist", Xmlns.BLOCKING)) &&
+ packet.fromServer(account)) {
+ // Block list or block push.
+ Logging.d(Config.LOGTAG, "Received blocklist update from server");
+ final Element blocklist = packet.findChild("blocklist", Xmlns.BLOCKING);
+ final Element block = packet.findChild("block", Xmlns.BLOCKING);
+ final Collection<Element> items = blocklist != null ? blocklist.getChildren() :
+ (block != null ? block.getChildren() : null);
+ // If this is a response to a blocklist query, clear the block list and replace with the new one.
+ // Otherwise, just update the existing blocklist.
+ if (packet.getType() == IqPacket.TYPE.RESULT) {
+ account.clearBlocklist();
+ account.getXmppConnection().getFeatures().setBlockListRequested(true);
+ }
+ if (items != null) {
+ final Collection<Jid> jids = new ArrayList<>(items.size());
+ // Create a collection of Jids from the packet
+ for (final Element item : items) {
+ if (item.getName().equals("item")) {
+ final Jid jid = item.getAttributeAsJid("jid");
+ if (jid != null) {
+ jids.add(jid);
+ }
+ }
+ }
+ account.getBlocklist().addAll(jids);
+ }
+ // Update the UI
+ mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
+ } else if (packet.hasChild("unblock", Xmlns.BLOCKING) &&
+ packet.fromServer(account) && packet.getType() == IqPacket.TYPE.SET) {
+ Logging.d(Config.LOGTAG, "Received unblock update from server");
+ final Collection<Element> items = packet.findChild("unblock", Xmlns.BLOCKING).getChildren();
+ if (items.size() == 0) {
+ // No children to unblock == unblock all
+ account.getBlocklist().clear();
+ } else {
+ final Collection<Jid> jids = new ArrayList<>(items.size());
+ for (final Element item : items) {
+ if (item.getName().equals("item")) {
+ final Jid jid = item.getAttributeAsJid("jid");
+ if (jid != null) {
+ jids.add(jid);
+ }
+ }
+ }
+ account.getBlocklist().removeAll(jids);
+ }
+ mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED);
+ } else if (packet.hasChild("open", "http://jabber.org/protocol/ibb")
+ || packet.hasChild("data", "http://jabber.org/protocol/ibb")) {
+ mXmppConnectionService.getJingleConnectionManager()
+ .deliverIbbPacket(account, packet);
+ } else if (packet.hasChild("query", "http://jabber.org/protocol/disco#info")) {
+ final IqPacket response = mXmppConnectionService.getIqGenerator().discoResponse(packet);
+ mXmppConnectionService.sendIqPacket(account, response, null);
+ } else if (packet.hasChild("query","jabber:iq:version")) {
+ final IqPacket response = mXmppConnectionService.getIqGenerator().versionResponse(packet);
+ mXmppConnectionService.sendIqPacket(account,response,null);
+ } else if (packet.hasChild("ping", "urn:xmpp:ping")) {
+ final IqPacket response = packet.generateResponse(IqPacket.TYPE.RESULT);
+ mXmppConnectionService.sendIqPacket(account, response, null);
+ } else {
+ if (packet.getType() == IqPacket.TYPE.GET || packet.getType() == IqPacket.TYPE.SET) {
+ final IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR);
+ final Element error = response.addChild("error");
+ error.setAttribute("type", "cancel");
+ error.addChild("feature-not-implemented","urn:ietf:params:xml:ns:xmpp-stanzas");
+ account.getXmppConnection().sendIqPacket(response, null);
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java
new file mode 100644
index 00000000..b68e7014
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java
@@ -0,0 +1,581 @@
+package eu.siacs.conversations.parser;
+
+import android.util.Log;
+import android.util.Pair;
+
+import de.thedevstack.conversationsplus.entities.FileParams;
+import de.thedevstack.conversationsplus.enums.FileStatus;
+import de.thedevstack.conversationsplus.utils.MessageUtil;
+import de.thedevstack.conversationsplus.xmpp.httpuploadim.HttpUploadHint;
+import de.tzur.conversations.Settings;
+
+import net.java.otr4j.session.Session;
+import net.java.otr4j.session.SessionStatus;
+
+import java.util.ArrayList;
+import java.util.Set;
+
+import de.thedevstack.android.logcat.Logging;
+import de.thedevstack.conversationsplus.ConversationsPlusPreferences;
+import de.thedevstack.conversationsplus.utils.AvatarUtil;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.crypto.OtrService;
+import eu.siacs.conversations.crypto.axolotl.AxolotlService;
+import eu.siacs.conversations.crypto.axolotl.AxolotlServiceImpl;
+import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Bookmark;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.MucOptions;
+import eu.siacs.conversations.http.HttpConnectionManager;
+import eu.siacs.conversations.services.AvatarService;
+import eu.siacs.conversations.services.MessageArchiveService;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.OnMessagePacketReceived;
+import eu.siacs.conversations.xmpp.chatstate.ChatState;
+import eu.siacs.conversations.xmpp.jid.Jid;
+import eu.siacs.conversations.xmpp.pep.Avatar;
+import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
+
+public class MessageParser extends AbstractParser implements
+ OnMessagePacketReceived {
+ public MessageParser(XmppConnectionService service) {
+ super(service);
+ }
+
+ private boolean extractChatState(Conversation conversation, final MessagePacket packet) {
+ ChatState state = ChatState.parse(packet);
+ if (state != null && conversation != null) {
+ final Account account = conversation.getAccount();
+ Jid from = packet.getFrom();
+ if (from.toBareJid().equals(account.getJid().toBareJid())) {
+ conversation.setOutgoingChatState(state);
+ if (state == ChatState.ACTIVE || state == ChatState.COMPOSING) {
+ Logging.d("markRead", "MessageParser.extractChatState (" + conversation.getName() + ")");
+ mXmppConnectionService.markRead(conversation);
+ account.activateGracePeriod();
+ }
+ return false;
+ } else {
+ return conversation.setIncomingChatState(state);
+ }
+ }
+ return false;
+ }
+
+ private Message parseOtrChat(String body, Jid from, String id, Conversation conversation) {
+ String presence;
+ if (from.isBareJid()) {
+ presence = "";
+ } else {
+ presence = from.getResourcepart();
+ }
+ if (body.matches("^\\?OTRv\\d{1,2}\\?.*")) {
+ conversation.endOtrIfNeeded();
+ }
+ if (!conversation.hasValidOtrSession()) {
+ conversation.startOtrSession(presence,false);
+ } else {
+ String foreignPresence = conversation.getOtrSession().getSessionID().getUserID();
+ if (!foreignPresence.equals(presence)) {
+ conversation.endOtrIfNeeded();
+ conversation.startOtrSession(presence, false);
+ }
+ }
+ try {
+ conversation.setLastReceivedOtrMessageId(id);
+ Session otrSession = conversation.getOtrSession();
+ body = otrSession.transformReceiving(body);
+ SessionStatus status = otrSession.getSessionStatus();
+ if (body == null && status == SessionStatus.ENCRYPTED) {
+ mXmppConnectionService.onOtrSessionEstablished(conversation);
+ return null;
+ } else if (body == null && status == SessionStatus.FINISHED) {
+ conversation.resetOtrSession();
+ mXmppConnectionService.updateConversationUi();
+ return null;
+ } else if (body == null || (body.isEmpty())) {
+ return null;
+ }
+ if (body.startsWith(CryptoHelper.FILETRANSFER)) {
+ String key = body.substring(CryptoHelper.FILETRANSFER.length());
+ conversation.setSymmetricKey(CryptoHelper.hexToBytes(key));
+ return null;
+ }
+ final OtrService otrService = conversation.getAccount().getOtrService();
+ Message finishedMessage = new Message(conversation, body, Message.ENCRYPTION_OTR, Message.STATUS_RECEIVED);
+ finishedMessage.setFingerprint(otrService.getFingerprint(otrSession.getRemotePublicKey()));
+ conversation.setLastReceivedOtrMessageId(null);
+
+ return finishedMessage;
+ } catch (Exception e) {
+ conversation.resetOtrSession();
+ return null;
+ }
+ }
+
+ private Message parseAxolotlChat(Element axolotlMessage, Jid from, Conversation conversation, int status) {
+ Message finishedMessage = null;
+ AxolotlService service = conversation.getAccount().getAxolotlService();
+ XmppAxolotlMessage xmppAxolotlMessage = XmppAxolotlMessage.fromElement(axolotlMessage, from.toBareJid());
+ XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = service.processReceivingPayloadMessage(xmppAxolotlMessage);
+ if(plaintextMessage != null) {
+ finishedMessage = new Message(conversation, plaintextMessage.getPlaintext(), Message.ENCRYPTION_AXOLOTL, status);
+ finishedMessage.setFingerprint(plaintextMessage.getFingerprint());
+ Logging.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(finishedMessage.getConversation().getAccount())+" Received Message with session fingerprint: "+plaintextMessage.getFingerprint());
+ }
+
+ return finishedMessage;
+ }
+
+ private class Invite {
+ Jid jid;
+ String password;
+ Invite(Jid jid, String password) {
+ this.jid = jid;
+ this.password = password;
+ }
+
+ public boolean execute(Account account) {
+ if (jid != null) {
+ Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, jid, true);
+ if (!conversation.getMucOptions().online()) {
+ conversation.getMucOptions().setPassword(password);
+ mXmppConnectionService.databaseBackend.updateConversation(conversation);
+ mXmppConnectionService.joinMuc(conversation);
+ mXmppConnectionService.updateConversationUi();
+ }
+ return true;
+ }
+ return false;
+ }
+ }
+
+ private Invite extractInvite(Element message) {
+ Element x = message.findChild("x", "http://jabber.org/protocol/muc#user");
+ if (x != null) {
+ Element invite = x.findChild("invite");
+ if (invite != null) {
+ Element pw = x.findChild("password");
+ return new Invite(message.getAttributeAsJid("from"), pw != null ? pw.getContent(): null);
+ }
+ } else {
+ x = message.findChild("x","jabber:x:conference");
+ if (x != null) {
+ return new Invite(x.getAttributeAsJid("jid"),x.getAttribute("password"));
+ }
+ }
+ return null;
+ }
+
+ private static String extractStanzaId(Element packet, Jid by) {
+ for(Element child : packet.getChildren()) {
+ if (child.getName().equals("stanza-id")
+ && "urn:xmpp:sid:0".equals(child.getNamespace())
+ && by.equals(child.getAttributeAsJid("by"))) {
+ return child.getAttribute("id");
+ }
+ }
+ return null;
+ }
+
+ private void parseEvent(final Element event, final Jid from, final Account account) {
+ Element items = event.findChild("items");
+ String node = items == null ? null : items.getAttribute("node");
+ if ("urn:xmpp:avatar:metadata".equals(node)) {
+ Avatar avatar = Avatar.parseMetadata(items);
+ if (avatar != null) {
+ avatar.owner = from.toBareJid();
+ if (AvatarUtil.isAvatarCached(avatar)) {
+ if (account.getJid().toBareJid().equals(from)) {
+ if (account.setAvatar(avatar.getFilename())) {
+ mXmppConnectionService.databaseBackend.updateAccount(account);
+ }
+ AvatarService.getInstance().clear(account);
+ mXmppConnectionService.updateConversationUi();
+ mXmppConnectionService.updateAccountUi();
+ } else {
+ Contact contact = account.getRoster().getContact(from);
+ contact.setAvatar(avatar);
+ AvatarService.getInstance().clear(contact);
+ mXmppConnectionService.updateConversationUi();
+ mXmppConnectionService.updateRosterUi();
+ }
+ } else {
+ AvatarService.getInstance().fetchAvatar(account, avatar);
+ }
+ }
+ } else if ("http://jabber.org/protocol/nick".equals(node)) {
+ Element i = items.findChild("item");
+ Element nick = i == null ? null : i.findChild("nick", "http://jabber.org/protocol/nick");
+ if (nick != null && nick.getContent() != null) {
+ Contact contact = account.getRoster().getContact(from);
+ contact.setPresenceName(nick.getContent());
+ AvatarService.getInstance().clear(account);
+ mXmppConnectionService.updateConversationUi();
+ mXmppConnectionService.updateAccountUi();
+ }
+ } else if (ConversationsPlusPreferences.omemoEnabled() && AxolotlService.PEP_DEVICE_LIST.equals(node)) {
+ Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account)+"Received PEP device list update from "+ from + ", processing...");
+ Element item = items.findChild("item");
+ Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item);
+ AxolotlService axolotlService = account.getAxolotlService();
+ axolotlService.registerDevices(from, deviceIds);
+ mXmppConnectionService.updateAccountUi();
+ }
+ }
+
+ private boolean handleErrorMessage(Account account, MessagePacket packet) {
+ if (packet.getType() == MessagePacket.TYPE_ERROR) {
+ Jid from = packet.getFrom();
+ if (from != null) {
+ Element error = packet.findChild("error");
+ String text = error == null ? null : error.findChildContent("text");
+ if (text != null) {
+ Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": sending message to "+ from+ " failed - " + text);
+ } else if (error != null) {
+ Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": sending message to "+ from+ " failed - " + error);
+ }
+ Message message = mXmppConnectionService.markMessage(account,
+ from.toBareJid(),
+ packet.getId(),
+ Message.STATUS_SEND_FAILED);
+ if (message != null && message.getEncryption() == Message.ENCRYPTION_OTR) {
+ message.getConversation().endOtrIfNeeded();
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void onMessagePacketReceived(Account account, MessagePacket original) {
+ if (handleErrorMessage(account, original)) {
+ return;
+ }
+ final MessagePacket packet;
+ Long timestamp = null;
+ final boolean isForwarded;
+ boolean isCarbon = false;
+ String serverMsgId = null;
+ final Element fin = original.findChild("fin", "urn:xmpp:mam:0");
+ if (fin != null) {
+ mXmppConnectionService.getMessageArchiveService().processFin(fin,original.getFrom());
+ return;
+ }
+ final Element result = original.findChild("result","urn:xmpp:mam:0");
+ final MessageArchiveService.Query query = result == null ? null : mXmppConnectionService.getMessageArchiveService().findQuery(result.getAttribute("queryid"));
+ if (query != null && query.validFrom(original.getFrom())) {
+ Pair<MessagePacket, Long> f = original.getForwardedMessagePacket("result", "urn:xmpp:mam:0");
+ if (f == null) {
+ return;
+ }
+ timestamp = f.second;
+ packet = f.first;
+ isForwarded = true;
+ serverMsgId = result.getAttribute("id");
+ query.incrementMessageCount();
+ } else if (query != null) {
+ Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": received mam result from invalid sender");
+ return;
+ } else if (original.fromServer(account)) {
+ Pair<MessagePacket, Long> f;
+ f = original.getForwardedMessagePacket("received", "urn:xmpp:carbons:2");
+ f = f == null ? original.getForwardedMessagePacket("sent", "urn:xmpp:carbons:2") : f;
+ packet = f != null ? f.first : original;
+ if (handleErrorMessage(account, packet)) {
+ return;
+ }
+ timestamp = f != null ? f.second : null;
+ isCarbon = f != null;
+ isForwarded = isCarbon;
+ } else {
+ packet = original;
+ isForwarded = false;
+ }
+
+ if (timestamp == null) {
+ timestamp = AbstractParser.getTimestamp(packet, System.currentTimeMillis());
+ }
+ final String body = packet.getBody();
+ final Element mucUserElement = packet.findChild("x", "http://jabber.org/protocol/muc#user");
+ final String pgpEncrypted = packet.findChildContent("x", "jabber:x:encrypted");
+ final Element oob = packet.findChild("x", "jabber:x:oob");
+ final boolean isOob = oob!= null && body != null && body.equals(oob.findChildContent("url"));
+ final Element axolotlEncrypted = packet.findChild(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX);
+ int status;
+ final Jid counterpart;
+ final Jid to = packet.getTo();
+ final Jid from = packet.getFrom();
+ final String remoteMsgId = packet.getId();
+
+ if (from == null) {
+ Log.d(Config.LOGTAG,"no from in: "+packet.toString());
+ return;
+ }
+
+ boolean isTypeGroupChat = packet.getType() == MessagePacket.TYPE_GROUPCHAT;
+ boolean isProperlyAddressed = (to != null ) && (!to.isBareJid() || account.countPresences() <= 1);
+ boolean isMucStatusMessage = from.isBareJid() && mucUserElement != null && mucUserElement.hasChild("status");
+ if (packet.fromAccount(account)) {
+ status = Message.STATUS_SEND;
+ counterpart = to != null ? to : account.getJid();
+ } else {
+ status = Message.STATUS_RECEIVED;
+ counterpart = from;
+ }
+
+ Invite invite = extractInvite(packet);
+ if (invite != null && invite.execute(account)) {
+ return;
+ }
+
+ if (extractChatState(mXmppConnectionService.find(account, counterpart.toBareJid()), packet)) {
+ mXmppConnectionService.updateConversationUi();
+ }
+
+ if ((body != null || pgpEncrypted != null || axolotlEncrypted != null) && !isMucStatusMessage) {
+ Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, counterpart.toBareJid(), isTypeGroupChat, query);
+ if (isTypeGroupChat) {
+ if (counterpart.getResourcepart().equals(conversation.getMucOptions().getActualNick())) {
+ status = Message.STATUS_SEND_RECEIVED;
+ isCarbon = true; //not really carbon but received from another resource
+ if (MessageUtil.markMessage(conversation, remoteMsgId, status)) {
+ return;
+ } else if (remoteMsgId == null || Config.IGNORE_ID_REWRITE_IN_MUC) {
+ Message message = conversation.findSentMessageWithBody(packet.getBody());
+ if (message != null) {
+ MessageUtil.markMessage(message, status);
+ return;
+ }
+ }
+ } else {
+ status = Message.STATUS_RECEIVED;
+ }
+ }
+ Message message;
+ if (body != null && body.startsWith("?OTR") && Config.supportOtr()) {
+ if (!isForwarded && !isTypeGroupChat && isProperlyAddressed) {
+ message = parseOtrChat(body, from, remoteMsgId, conversation);
+ if (message == null) {
+ return;
+ }
+ } else {
+ Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": ignoring OTR message from "+from+" isForwarded="+Boolean.toString(isForwarded)+", isProperlyAddressed="+Boolean.valueOf(isProperlyAddressed));
+ message = new Message(conversation, body, Message.ENCRYPTION_NONE, status);
+ }
+ } else if (pgpEncrypted != null && Config.supportOpenPgp()) {
+ message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status);
+ } else if (axolotlEncrypted != null && Config.supportOmemo()) {
+ Jid origin;
+ if (conversation.getMode() == Conversation.MODE_MULTI) {
+ origin = conversation.getMucOptions().getTrueCounterpart(counterpart.getResourcepart());
+ if (origin == null) {
+ Log.d(Config.LOGTAG,"axolotl message in non anonymous conference received");
+ return;
+ }
+ } else {
+ origin = from;
+ }
+ message = parseAxolotlChat(axolotlEncrypted, origin, conversation, status);
+ if (message == null) {
+ return;
+ }
+ } else {
+ message = new Message(conversation, body, Message.ENCRYPTION_NONE, status);
+ }
+
+ if (serverMsgId == null) {
+ serverMsgId = extractStanzaId(packet, isTypeGroupChat ? conversation.getJid().toBareJid() : account.getServer());
+ }
+ message.setHttpUploaded(packet.hasChild(HttpUploadHint.ELEMENT_NAME, HttpUploadHint.NAMESPACE));
+ if (message.isHttpUploaded()) {
+ message.setTreatAsDownloadable(Message.Decision.MUST);
+ }
+
+ message.setCounterpart(counterpart);
+ message.setRemoteMsgId(remoteMsgId);
+ message.setServerMsgId(serverMsgId);
+ message.setCarbon(isCarbon);
+ message.setTime(timestamp);
+ message.setOob(isOob);
+ message.markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0");
+ if (conversation.getMode() == Conversation.MODE_MULTI) {
+ Jid trueCounterpart = conversation.getMucOptions().getTrueCounterpart(counterpart.getResourcepart());
+ message.setTrueCounterpart(trueCounterpart);
+ if (trueCounterpart != null) {
+ updateLastseen(timestamp, account, trueCounterpart, false);
+ }
+ if (!isTypeGroupChat) {
+ message.setType(Message.TYPE_PRIVATE);
+ }
+ } else {
+ updateLastseen(timestamp, account, packet.getFrom(), true);
+ }
+
+ boolean checkForDuplicates = query != null
+ || (isTypeGroupChat && packet.hasChild("delay","urn:xmpp:delay"))
+ || message.getType() == Message.TYPE_PRIVATE;
+ if (checkForDuplicates && conversation.hasDuplicateMessage(message)) {
+ Logging.d(Config.LOGTAG, "skipping duplicate message from '" + message.getCounterpart().toString() + "' with remote id " + message.getRemoteMsgId());
+ return;
+ }
+
+ if (query != null && query.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) {
+ conversation.prepend(message);
+ } else {
+ conversation.add(message);
+ }
+
+ if (message.getEncryption() == Message.ENCRYPTION_PGP) {
+ conversation.getAccount().getPgpDecryptionService().add(message);
+ }
+
+ if (query == null || query.getWith() == null) { //either no mam or catchup
+ if (status == Message.STATUS_SEND || status == Message.STATUS_SEND_RECEIVED) {
+ Logging.d("markRead", "MessageParser.onMessagePacketReceived1 (" + conversation.getName() + ")");
+ mXmppConnectionService.markRead(conversation);
+ if (query == null) {
+ account.activateGracePeriod();
+ }
+ } else {
+ // only not mam messages should be marked as unread
+ if (query == null) {
+ message.markUnread();
+ }
+ }
+ }
+
+ if (query == null) {
+ mXmppConnectionService.updateConversationUi();
+ }
+
+ if (Settings.CONFIRM_MESSAGE_READ && remoteMsgId != null && !isForwarded && !isTypeGroupChat) {
+ sendMessageReceipts(account, packet);
+ }
+
+ if (message.getStatus() == Message.STATUS_RECEIVED
+ && conversation.getOtrSession() != null
+ && !conversation.getOtrSession().getSessionID().getUserID()
+ .equals(message.getCounterpart().getResourcepart())) {
+ conversation.endOtrIfNeeded();
+ }
+
+ if (message.getEncryption() == Message.ENCRYPTION_NONE || !ConversationsPlusPreferences.dontSaveEncrypted()) {
+ mXmppConnectionService.databaseBackend.createMessage(message);
+ }
+ MessageUtil.extractFileParamsFromBody(message);
+ FileParams fileParams = message.getFileParams();
+ if (message.treatAsDownloadable() != Message.Decision.NEVER && message.treatAsDownloadable() != Message.Decision.NOT_DECIDED) {
+ if (null != fileParams) {
+ fileParams.setFileStatus(FileStatus.NEEDS_DOWNLOAD);
+ }
+ }
+ if (message.trusted()
+ && message.treatAsDownloadable() != Message.Decision.NEVER
+ && ConversationsPlusPreferences.autoAcceptFileSize() > 0
+ && (message.isHttpUploaded() || ConversationsPlusPreferences.autoDownloadFileLink())) {
+ HttpConnectionManager.createNewDownloadConnection(message);
+ } else {
+ if (query == null) {
+ mXmppConnectionService.getNotificationService().push(message);
+ } else if (query.getWith() == null) { // mam catchup
+ /*
+ Like suggested in https://bugs.thedevstack.de/task/156 user should be notified
+ in some other way of loaded messages.
+ */
+ // mXmppConnectionService.getNotificationService().pushFromBacklog(message);
+ }
+ }
+ } else if (!packet.hasChild("body")){ //no body
+ if (isTypeGroupChat) {
+ Conversation conversation = mXmppConnectionService.find(account, from.toBareJid());
+ if (packet.hasChild("subject")) {
+ if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) {
+ conversation.setHasMessagesLeftOnServer(conversation.countMessages() > 0);
+ String subject = packet.findChildContent("subject");
+ conversation.getMucOptions().setSubject(subject);
+ final Bookmark bookmark = conversation.getBookmark();
+ if (bookmark != null && bookmark.getBookmarkName() == null) {
+ if (bookmark.setBookmarkName(subject)) {
+ mXmppConnectionService.pushBookmarks(account);
+ }
+ }
+ mXmppConnectionService.updateConversationUi();
+ return;
+ }
+ }
+
+ if (conversation != null && isMucStatusMessage) {
+ for (Element child : mucUserElement.getChildren()) {
+ if (child.getName().equals("status")
+ && MucOptions.STATUS_CODE_ROOM_CONFIG_CHANGED.equals(child.getAttribute("code"))) {
+ mXmppConnectionService.fetchConferenceConfiguration(conversation);
+ }
+ }
+ }
+ }
+ }
+
+ Element received = packet.findChild("received", "urn:xmpp:chat-markers:0");
+ if (received == null) {
+ received = packet.findChild("received", "urn:xmpp:receipts");
+ }
+ if (received != null && !packet.fromAccount(account)) {
+ mXmppConnectionService.markMessage(account, from.toBareJid(), received.getAttribute("id"), Message.STATUS_SEND_RECEIVED);
+ }
+ Element displayed = packet.findChild("displayed", "urn:xmpp:chat-markers:0");
+ if (displayed != null) {
+ if (packet.fromAccount(account)) {
+ Conversation conversation = mXmppConnectionService.find(account,counterpart.toBareJid());
+ if (conversation != null) {
+ Logging.d("markRead", "MessageParser.onMessagePacketReceived2 (" + conversation.getName() + ")");
+ mXmppConnectionService.markRead(conversation);
+ }
+ } else {
+ updateLastseen(timestamp, account, packet.getFrom(), true);
+ final Message displayedMessage = mXmppConnectionService.markMessage(account, from.toBareJid(), displayed.getAttribute("id"), Message.STATUS_SEND_DISPLAYED);
+ Message message = displayedMessage == null ? null : displayedMessage.prev();
+ while (message != null
+ && message.getStatus() == Message.STATUS_SEND_RECEIVED
+ && message.getTimeSent() < displayedMessage.getTimeSent()) {
+ MessageUtil.markMessage(message, Message.STATUS_SEND_DISPLAYED);
+ message = message.prev();
+ }
+ }
+ }
+
+ Element event = packet.findChild("event", "http://jabber.org/protocol/pubsub#event");
+ if (event != null) {
+ parseEvent(event, from, account);
+ }
+
+ String nick = packet.findChildContent("nick", "http://jabber.org/protocol/nick");
+ if (nick != null) {
+ Contact contact = account.getRoster().getContact(from);
+ contact.setPresenceName(nick);
+ }
+ }
+
+ private void sendMessageReceipts(Account account, MessagePacket packet) {
+ ArrayList<String> receiptsNamespaces = new ArrayList<>();
+ if (packet.hasChild("markable", "urn:xmpp:chat-markers:0")) {
+ receiptsNamespaces.add("urn:xmpp:chat-markers:0");
+ }
+ if (packet.hasChild("request", "urn:xmpp:receipts")) {
+ receiptsNamespaces.add("urn:xmpp:receipts");
+ }
+ if (receiptsNamespaces.size() > 0) {
+ MessagePacket receipt = mXmppConnectionService.getMessageGenerator().received(account,
+ packet,
+ receiptsNamespaces,
+ packet.getType());
+ mXmppConnectionService.sendMessagePacket(account, receipt);
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java
new file mode 100644
index 00000000..3d933364
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java
@@ -0,0 +1,265 @@
+package eu.siacs.conversations.parser;
+
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import de.thedevstack.android.logcat.Logging;
+import de.thedevstack.conversationsplus.utils.AvatarUtil;
+import de.thedevstack.conversationsplus.utils.UiUpdateHelper;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.crypto.PgpEngine;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.MucOptions;
+import eu.siacs.conversations.entities.Presence;
+import eu.siacs.conversations.generator.PresenceGenerator;
+import eu.siacs.conversations.persistance.DatabaseBackend;
+import eu.siacs.conversations.services.AvatarService;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.OnPresencePacketReceived;
+import eu.siacs.conversations.xmpp.jid.Jid;
+import eu.siacs.conversations.xmpp.pep.Avatar;
+import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
+
+public class PresenceParser extends AbstractParser implements
+ OnPresencePacketReceived {
+
+ public PresenceParser(XmppConnectionService service) {
+ super(service);
+ }
+
+ public void parseConferencePresence(PresencePacket packet, Account account) {
+ final Conversation conversation = packet.getFrom() == null ? null : mXmppConnectionService.find(account, packet.getFrom().toBareJid());
+ if (conversation != null) {
+ final MucOptions mucOptions = conversation.getMucOptions();
+ boolean before = mucOptions.online();
+ int count = mucOptions.getUserCount();
+ final List<MucOptions.User> tileUserBefore = mucOptions.getUsers(5);
+ processConferencePresence(packet, mucOptions);
+ final List<MucOptions.User> tileUserAfter = mucOptions.getUsers(5);
+ if (!tileUserAfter.equals(tileUserBefore)) {
+ Logging.d(Config.LOGTAG, account.getJid().toBareJid() + ": update tiles for " + conversation.getName());
+ AvatarService.getInstance().clear(conversation);
+ }
+ if (before != mucOptions.online() || (mucOptions.online() && count != mucOptions.getUserCount())) {
+ UiUpdateHelper.updateConversationUi();
+ } else if (mucOptions.online()) {
+ UiUpdateHelper.updateMucRosterUi();
+ }
+ }
+ }
+
+ private void processConferencePresence(PresencePacket packet, MucOptions mucOptions) {
+ final Jid from = packet.getFrom();
+ if (!from.isBareJid()) {
+ final String type = packet.getAttribute("type");
+ final Element x = packet.findChild("x", "http://jabber.org/protocol/muc#user");
+ Avatar avatar = Avatar.parsePresence(packet.findChild("x", "vcard-temp:x:update"));
+ final List<String> codes = getStatusCodes(x);
+ if (type == null) {
+ if (x != null) {
+ Element item = x.findChild("item");
+ if (item != null && !from.isBareJid()) {
+ mucOptions.setError(MucOptions.Error.NONE);
+ MucOptions.User user = new MucOptions.User(mucOptions, from);
+ user.setAffiliation(item.getAttribute("affiliation"));
+ user.setRole(item.getAttribute("role"));
+ Jid real = item.getAttributeAsJid("jid");
+ if (real != null) {
+ user.setJid(real);
+ mucOptions.putMember(real.toBareJid());
+ }
+ if (codes.contains(MucOptions.STATUS_CODE_SELF_PRESENCE) || packet.getFrom().equals(mucOptions.getConversation().getJid())) {
+ mucOptions.setOnline();
+ mucOptions.setSelf(user);
+ if (mucOptions.mNickChangingInProgress) {
+ if (mucOptions.onRenameListener != null) {
+ mucOptions.onRenameListener.onSuccess();
+ }
+ mucOptions.mNickChangingInProgress = false;
+ }
+ } else {
+ mucOptions.addUser(user);
+ }
+ if (mXmppConnectionService.getPgpEngine() != null) {
+ Element signed = packet.findChild("x", "jabber:x:signed");
+ if (signed != null) {
+ Element status = packet.findChild("status");
+ String msg = status == null ? "" : status.getContent();
+ long keyId = mXmppConnectionService.getPgpEngine().fetchKeyId(mucOptions.getAccount(), msg, signed.getContent());
+ if (keyId != 0) {
+ user.setPgpKeyId(keyId);
+ }
+ }
+ }
+ if (avatar != null) {
+ avatar.owner = from;
+ if (AvatarUtil.isAvatarCached(avatar)) {
+ if (user.setAvatar(avatar)) {
+ AvatarService.getInstance().clear(user);
+ }
+ } else {
+ AvatarService.getInstance().fetchAvatar(mucOptions.getAccount(), avatar);
+ }
+ }
+ }
+ }
+ } else if (type.equals("unavailable")) {
+ if (codes.contains(MucOptions.STATUS_CODE_SELF_PRESENCE) ||
+ packet.getFrom().equals(mucOptions.getConversation().getJid())) {
+ if (codes.contains(MucOptions.STATUS_CODE_CHANGED_NICK)) {
+ mucOptions.mNickChangingInProgress = true;
+ } else if (codes.contains(MucOptions.STATUS_CODE_KICKED)) {
+ mucOptions.setError(MucOptions.Error.KICKED);
+ } else if (codes.contains(MucOptions.STATUS_CODE_BANNED)) {
+ mucOptions.setError(MucOptions.Error.BANNED);
+ } else if (codes.contains(MucOptions.STATUS_CODE_LOST_MEMBERSHIP)) {
+ mucOptions.setError(MucOptions.Error.MEMBERS_ONLY);
+ } else if (codes.contains(MucOptions.STATUS_CODE_AFFILIATION_CHANGE)) {
+ mucOptions.setError(MucOptions.Error.MEMBERS_ONLY);
+ } else if (codes.contains(MucOptions.STATUS_CODE_SHUTDOWN)) {
+ mucOptions.setError(MucOptions.Error.SHUTDOWN);
+ } else {
+ mucOptions.setError(MucOptions.Error.UNKNOWN);
+ Log.d(Config.LOGTAG, "unknown error in conference: " + packet);
+ }
+ } else if (!from.isBareJid()){
+ MucOptions.User user = mucOptions.deleteUser(from.getResourcepart());
+ if (user != null) {
+ AvatarService.getInstance().clear(user);
+ }
+ }
+ } else if (type.equals("error")) {
+ Element error = packet.findChild("error");
+ if (error != null && error.hasChild("conflict")) {
+ if (mucOptions.online()) {
+ if (mucOptions.onRenameListener != null) {
+ mucOptions.onRenameListener.onFailure();
+ }
+ } else {
+ mucOptions.setError(MucOptions.Error.NICK_IN_USE);
+ }
+ } else if (error != null && error.hasChild("not-authorized")) {
+ mucOptions.setError(MucOptions.Error.PASSWORD_REQUIRED);
+ } else if (error != null && error.hasChild("forbidden")) {
+ mucOptions.setError(MucOptions.Error.BANNED);
+ } else if (error != null && error.hasChild("registration-required")) {
+ mucOptions.setError(MucOptions.Error.BANNED);
+ }
+ }
+ }
+ }
+
+ private static List<String> getStatusCodes(Element x) {
+ List<String> codes = new ArrayList<>();
+ if (x != null) {
+ for (Element child : x.getChildren()) {
+ if (child.getName().equals("status")) {
+ String code = child.getAttribute("code");
+ if (code != null) {
+ codes.add(code);
+ }
+ }
+ }
+ }
+ return codes;
+ }
+
+ public void parseContactPresence(final PresencePacket packet, final Account account) {
+ final PresenceGenerator mPresenceGenerator = mXmppConnectionService.getPresenceGenerator();
+ final Jid from = packet.getFrom();
+ if (from == null) {
+ return;
+ }
+ final String type = packet.getAttribute("type");
+ final Contact contact = account.getRoster().getContact(from);
+ if (type == null) {
+ final String resource = from.isBareJid() ? "" : from.getResourcepart();
+ contact.setPresenceName(packet.findChildContent("nick", "http://jabber.org/protocol/nick"));
+ Avatar avatar = Avatar.parsePresence(packet.findChild("x", "vcard-temp:x:update"));
+ if (avatar != null && (!contact.isSelf() || null == account.getAvatar())) {
+ avatar.owner = from.toBareJid();
+ if (AvatarUtil.isAvatarCached(avatar)) {
+ if (avatar.owner.equals(account.getJid().toBareJid())) {
+ account.setAvatar(avatar.getFilename());
+ DatabaseBackend.getInstance().updateAccount(account);
+ AvatarService.getInstance().clear(account);
+ UiUpdateHelper.updateAccountUi();
+ } else if (contact.setAvatar(avatar)) {
+ AvatarService.getInstance().clear(contact);
+ UiUpdateHelper.updateRosterUi();
+ }
+ UiUpdateHelper.updateConversationUi();
+ } else {
+ AvatarService.getInstance().fetchAvatar(account, avatar);
+ }
+ }
+ int sizeBefore = contact.getPresences().size();
+
+ final String show = packet.findChildContent("show");
+ final Element caps = packet.findChild("c", "http://jabber.org/protocol/caps");
+ final Presence presence = Presence.parse(show, caps);
+ contact.updatePresence(resource, presence);
+ if (presence.hasCaps() && Config.REQUEST_DISCO) {
+ mXmppConnectionService.fetchCaps(account, from, presence);
+ }
+
+ PgpEngine pgp = mXmppConnectionService.getPgpEngine();
+ Element x = packet.findChild("x", "jabber:x:signed");
+ if (pgp != null && x != null) {
+ Element status = packet.findChild("status");
+ String msg = status != null ? status.getContent() : "";
+ contact.setPgpKeyId(pgp.fetchKeyId(account, msg, x.getContent()));
+ }
+ boolean online = sizeBefore < contact.getPresences().size();
+ updateLastseen(packet, account, false);
+ mXmppConnectionService.onContactStatusChanged.onContactStatusChanged(contact, online);
+ } else if (type.equals("unavailable")) {
+ if (from.isBareJid()) {
+ contact.clearPresences();
+ } else {
+ contact.removePresence(from.getResourcepart());
+ }
+ mXmppConnectionService.onContactStatusChanged.onContactStatusChanged(contact, false);
+ } else if (type.equals("subscribe")) {
+ if (contact.getOption(Contact.Options.PREEMPTIVE_GRANT)) {
+ mXmppConnectionService.sendPresencePacket(account,
+ mPresenceGenerator.sendPresenceUpdatesTo(contact));
+ } else {
+ contact.setOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST);
+ final Conversation conversation = mXmppConnectionService.findOrCreateConversation(
+ account, contact.getJid().toBareJid(), false);
+ final String statusMessage = packet.findChildContent("status");
+ if (statusMessage != null
+ && !statusMessage.isEmpty()
+ && conversation.countMessages() == 0) {
+ conversation.add(new Message(
+ conversation,
+ statusMessage,
+ Message.ENCRYPTION_NONE,
+ Message.STATUS_RECEIVED
+ ));
+ }
+ }
+ }
+ UiUpdateHelper.updateRosterUi();
+ }
+
+ @Override
+ public void onPresencePacketReceived(Account account, PresencePacket packet) {
+ if (packet.hasChild("x", "http://jabber.org/protocol/muc#user")) {
+ this.parseConferencePresence(packet, account);
+ } else if (packet.hasChild("x", "http://jabber.org/protocol/muc")) {
+ this.parseConferencePresence(packet, account);
+ } else {
+ this.parseContactPresence(packet, account);
+ }
+ }
+
+}
diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java
new file mode 100644
index 00000000..793b050f
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java
@@ -0,0 +1,1219 @@
+package eu.siacs.conversations.persistance;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteCantOpenDatabaseException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.util.Base64;
+import android.util.Log;
+import android.util.Pair;
+
+import org.whispersystems.libaxolotl.AxolotlAddress;
+import org.whispersystems.libaxolotl.IdentityKey;
+import org.whispersystems.libaxolotl.IdentityKeyPair;
+import org.whispersystems.libaxolotl.InvalidKeyException;
+import org.whispersystems.libaxolotl.state.PreKeyRecord;
+import org.whispersystems.libaxolotl.state.SessionRecord;
+import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArrayList;
+import org.json.JSONException;
+
+import de.thedevstack.android.logcat.Logging;
+
+import de.thedevstack.conversationsplus.ConversationsPlusApplication;
+import de.thedevstack.conversationsplus.persistance.db.access.CursorHelper;
+import de.thedevstack.conversationsplus.persistance.db.access.MessageDatabaseAccess;
+import de.thedevstack.conversationsplus.utils.SimpleCryptoUtil;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.crypto.axolotl.AxolotlServiceImpl;
+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.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.Roster;
+import eu.siacs.conversations.entities.ServiceDiscoveryResult;
+import eu.siacs.conversations.xmpp.jid.InvalidJidException;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+public class DatabaseBackend extends SQLiteOpenHelper {
+
+ private static DatabaseBackend instance = null;
+
+ private static final String DATABASE_NAME = "history";
+ private static final int DATABASE_VERSION = 25;
+ private static final int C_TO_CPLUS_VERSION_OFFSET = 1000;
+ private static final int CPLUS_DATABASE_VERSION = 3;
+ private static final int CPLUS_DATABASE_VERSION_MULTIPLIER = 100;
+ private static final int PHYSICAL_DATABASE_VERSION = DATABASE_VERSION + C_TO_CPLUS_VERSION_OFFSET + (CPLUS_DATABASE_VERSION * CPLUS_DATABASE_VERSION_MULTIPLIER);
+
+ private static String CREATE_CONTATCS_STATEMENT = "create table "
+ + Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, "
+ + Contact.SERVERNAME + " TEXT, " + Contact.SYSTEMNAME + " TEXT,"
+ + Contact.JID + " TEXT," + Contact.KEYS + " TEXT,"
+ + Contact.PHOTOURI + " TEXT," + Contact.OPTIONS + " NUMBER,"
+ + Contact.SYSTEMACCOUNT + " NUMBER, " + Contact.AVATAR + " TEXT, "
+ + Contact.LAST_PRESENCE + " TEXT, " + Contact.LAST_TIME + " NUMBER, "
+ + Contact.GROUPS + " TEXT, FOREIGN KEY(" + Contact.ACCOUNT + ") REFERENCES "
+ + Account.TABLENAME + "(" + Account.UUID
+ + ") ON DELETE CASCADE, UNIQUE(" + Contact.ACCOUNT + ", "
+ + Contact.JID + ") ON CONFLICT REPLACE);";
+
+ private static String CREATE_DISCOVERY_RESULTS_STATEMENT = "create table "
+ + ServiceDiscoveryResult.TABLENAME + "("
+ + ServiceDiscoveryResult.HASH + " TEXT, "
+ + ServiceDiscoveryResult.VER + " TEXT, "
+ + ServiceDiscoveryResult.RESULT + " TEXT, "
+ + "UNIQUE(" + ServiceDiscoveryResult.HASH + ", "
+ + ServiceDiscoveryResult.VER + ") ON CONFLICT REPLACE);";
+
+ private static String CREATE_PREKEYS_STATEMENT = "CREATE TABLE "
+ + SQLiteAxolotlStore.PREKEY_TABLENAME + "("
+ + SQLiteAxolotlStore.ACCOUNT + " TEXT, "
+ + SQLiteAxolotlStore.ID + " INTEGER, "
+ + SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY("
+ + SQLiteAxolotlStore.ACCOUNT
+ + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, "
+ + "UNIQUE( " + SQLiteAxolotlStore.ACCOUNT + ", "
+ + SQLiteAxolotlStore.ID
+ + ") ON CONFLICT REPLACE"
+ + ");";
+
+ private static String CREATE_SIGNED_PREKEYS_STATEMENT = "CREATE TABLE "
+ + SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME + "("
+ + SQLiteAxolotlStore.ACCOUNT + " TEXT, "
+ + SQLiteAxolotlStore.ID + " INTEGER, "
+ + SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY("
+ + SQLiteAxolotlStore.ACCOUNT
+ + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, "
+ + "UNIQUE( " + SQLiteAxolotlStore.ACCOUNT + ", "
+ + SQLiteAxolotlStore.ID
+ + ") ON CONFLICT REPLACE" +
+ ");";
+
+ private static String CREATE_SESSIONS_STATEMENT = "CREATE TABLE "
+ + SQLiteAxolotlStore.SESSION_TABLENAME + "("
+ + SQLiteAxolotlStore.ACCOUNT + " TEXT, "
+ + SQLiteAxolotlStore.NAME + " TEXT, "
+ + SQLiteAxolotlStore.DEVICE_ID + " INTEGER, "
+ + SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY("
+ + SQLiteAxolotlStore.ACCOUNT
+ + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, "
+ + "UNIQUE( " + SQLiteAxolotlStore.ACCOUNT + ", "
+ + SQLiteAxolotlStore.NAME + ", "
+ + SQLiteAxolotlStore.DEVICE_ID
+ + ") ON CONFLICT REPLACE"
+ + ");";
+
+ private static String CREATE_IDENTITIES_STATEMENT = "CREATE TABLE "
+ + SQLiteAxolotlStore.IDENTITIES_TABLENAME + "("
+ + SQLiteAxolotlStore.ACCOUNT + " TEXT, "
+ + SQLiteAxolotlStore.NAME + " TEXT, "
+ + SQLiteAxolotlStore.OWN + " INTEGER, "
+ + SQLiteAxolotlStore.FINGERPRINT + " TEXT, "
+ + SQLiteAxolotlStore.CERTIFICATE + " BLOB, "
+ + SQLiteAxolotlStore.TRUSTED + " INTEGER, "
+ + SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY("
+ + SQLiteAxolotlStore.ACCOUNT
+ + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, "
+ + "UNIQUE( " + SQLiteAxolotlStore.ACCOUNT + ", "
+ + SQLiteAxolotlStore.NAME + ", "
+ + SQLiteAxolotlStore.FINGERPRINT
+ + ") ON CONFLICT IGNORE"
+ + ");";
+
+ private static int calculateCDatabaseVersion(int physicalDatabaseVersion) {
+ return physicalDatabaseVersion % CPLUS_DATABASE_VERSION_MULTIPLIER;
+ }
+
+ private static int calculateCPLusDatabaseVersion(int physicalDatabaseVersion) {
+ int cPlusDatabaseVersion = (physicalDatabaseVersion - C_TO_CPLUS_VERSION_OFFSET) / CPLUS_DATABASE_VERSION_MULTIPLIER;
+ return cPlusDatabaseVersion < 0 ? 0 : cPlusDatabaseVersion;
+ }
+
+ private DatabaseBackend(Context context) {
+ super(context, DATABASE_NAME, null, PHYSICAL_DATABASE_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ db.execSQL("PRAGMA foreign_keys=ON;");
+ db.execSQL("create table " + Account.TABLENAME + "(" + Account.UUID
+ + " TEXT PRIMARY KEY," + Account.USERNAME + " TEXT,"
+ + Account.SERVER + " TEXT," + Account.PASSWORD + " TEXT,"
+ + Account.DISPLAY_NAME + " TEXT, "
+ + Account.ROSTERVERSION + " TEXT," + Account.OPTIONS
+ + " NUMBER, " + Account.AVATAR + " TEXT, " + Account.KEYS
+ + " TEXT, " + Account.HOSTNAME + " TEXT, " + Account.PORT + " NUMBER DEFAULT 5222)");
+ db.execSQL("create table " + Conversation.TABLENAME + " ("
+ + Conversation.UUID + " TEXT PRIMARY KEY, " + Conversation.NAME
+ + " TEXT, " + Conversation.CONTACT + " TEXT, "
+ + Conversation.ACCOUNT + " TEXT, " + Conversation.CONTACTJID
+ + " TEXT, " + Conversation.CREATED + " NUMBER, "
+ + Conversation.STATUS + " NUMBER, " + Conversation.MODE
+ + " NUMBER, " + Conversation.ATTRIBUTES + " TEXT, FOREIGN KEY("
+ + Conversation.ACCOUNT + ") REFERENCES " + Account.TABLENAME
+ + "(" + Account.UUID + ") ON DELETE CASCADE);");
+ db.execSQL("create table " + Message.TABLENAME + "( " + Message.UUID
+ + " TEXT PRIMARY KEY, " + Message.CONVERSATION + " TEXT, "
+ + Message.TIME_SENT + " NUMBER, " + Message.COUNTERPART
+ + " TEXT, " + Message.TRUE_COUNTERPART + " TEXT,"
+ + Message.BODY + " TEXT, " + Message.ENCRYPTION + " NUMBER, "
+ + Message.STATUS + " NUMBER," + Message.TYPE + " NUMBER, "
+ + Message.RELATIVE_FILE_PATH + " TEXT, "
+ + Message.SERVER_MSG_ID + " TEXT, "
+ + Message.FINGERPRINT + " TEXT, "
+ + Message.CARBON + " INTEGER, "
+ + Message.EDITED + " TEXT, "
+ + Message.READ + " NUMBER DEFAULT 1, "
+ + Message.OOB + " INTEGER, "
+ + Message.REMOTE_MSG_ID + " TEXT, FOREIGN KEY("
+ + Message.CONVERSATION + ") REFERENCES "
+ + Conversation.TABLENAME + "(" + Conversation.UUID
+ + ") ON DELETE CASCADE);");
+
+ db.execSQL(CREATE_CONTATCS_STATEMENT);
+ db.execSQL(CREATE_DISCOVERY_RESULTS_STATEMENT);
+ db.execSQL(CREATE_SESSIONS_STATEMENT);
+ db.execSQL(CREATE_PREKEYS_STATEMENT);
+ db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT);
+ db.execSQL(CREATE_IDENTITIES_STATEMENT);
+
+ MessageDatabaseAccess.create(db);
+ }
+
+ protected void onUpgradeCPlusDatabase(SQLiteDatabase db, int oldVersion, int newVersion) {
+ Logging.d("db.upgrade.cplus", "Updating Conversations+ database from version '" + oldVersion + "' to '" + newVersion + "'");
+ if (oldVersion < newVersion) {
+ if (oldVersion < 1 && newVersion >= 1) {
+ MessageDatabaseAccess.upgrade(db, oldVersion, newVersion);
+ }
+ if (oldVersion < 2 && newVersion >= 2) {
+ Logging.d("db.upgrade.cplus", "Encrypt all passwords for the first time");
+ Cursor cursor = db.rawQuery("SELECT " + Account.UUID + ", " + Account.PASSWORD + " FROM " + Account.TABLENAME, new String[0]);
+ while (cursor.moveToNext()) {
+ String uuid = CursorHelper.getString(cursor, Account.UUID);
+ String password = CursorHelper.getString(cursor, Account.PASSWORD);
+ String encryptedPassword = SimpleCryptoUtil.encrypt(Account.PW_SEED, password);
+ ContentValues values = new ContentValues();
+ values.put(Account.PASSWORD, encryptedPassword);
+ db.update(Account.TABLENAME, values, Account.UUID + "=?", new String[] {uuid});
+ }
+ cursor.close();
+ }
+
+ if (oldVersion < 3 && newVersion >= 3) {
+ MessageDatabaseAccess.upgrade(db, oldVersion, newVersion);
+ }
+ }
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ onUpgradeConversationsDatabase(db, calculateCDatabaseVersion(oldVersion), calculateCDatabaseVersion(newVersion));
+ onUpgradeCPlusDatabase(db, calculateCPLusDatabaseVersion(oldVersion), calculateCPLusDatabaseVersion(newVersion));
+ }
+
+ protected void onUpgradeConversationsDatabase(SQLiteDatabase db, int oldVersion, int newVersion) {
+ Logging.d("db.upgrade.conversations", "Updating Conversations database from version '" + oldVersion + "' to '" + newVersion + "'");
+ if (oldVersion == newVersion) {
+ return;
+ }
+ if (oldVersion < 2 && newVersion >= 2) {
+ db.execSQL("update " + Account.TABLENAME + " set "
+ + Account.OPTIONS + " = " + Account.OPTIONS + " | 8");
+ }
+ if (oldVersion < 3 && newVersion >= 3) {
+ db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "
+ + Message.TYPE + " NUMBER");
+ }
+ if (oldVersion < 5 && newVersion >= 5) {
+ db.execSQL("DROP TABLE " + Contact.TABLENAME);
+ db.execSQL(CREATE_CONTATCS_STATEMENT);
+ db.execSQL("UPDATE " + Account.TABLENAME + " SET "
+ + Account.ROSTERVERSION + " = NULL");
+ }
+ if (oldVersion < 6 && newVersion >= 6) {
+ db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "
+ + Message.TRUE_COUNTERPART + " TEXT");
+ }
+ if (oldVersion < 7 && newVersion >= 7) {
+ db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "
+ + Message.REMOTE_MSG_ID + " TEXT");
+ db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN "
+ + Contact.AVATAR + " TEXT");
+ db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN "
+ + Account.AVATAR + " TEXT");
+ }
+ if (oldVersion < 8 && newVersion >= 8) {
+ db.execSQL("ALTER TABLE " + Conversation.TABLENAME + " ADD COLUMN "
+ + Conversation.ATTRIBUTES + " TEXT");
+ }
+ if (oldVersion < 9 && newVersion >= 9) {
+ db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN "
+ + Contact.LAST_TIME + " NUMBER");
+ db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN "
+ + Contact.LAST_PRESENCE + " TEXT");
+ }
+ if (oldVersion < 10 && newVersion >= 10) {
+ db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "
+ + Message.RELATIVE_FILE_PATH + " TEXT");
+ }
+ if (oldVersion < 11 && newVersion >= 11) {
+ db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN "
+ + Contact.GROUPS + " TEXT");
+ db.execSQL("delete from " + Contact.TABLENAME);
+ db.execSQL("update " + Account.TABLENAME + " set " + Account.ROSTERVERSION + " = NULL");
+ }
+ if (oldVersion < 12 && newVersion >= 12) {
+ db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "
+ + Message.SERVER_MSG_ID + " TEXT");
+ }
+ if (oldVersion < 13 && newVersion >= 13) {
+ db.execSQL("delete from " + Contact.TABLENAME);
+ db.execSQL("update " + Account.TABLENAME + " set " + Account.ROSTERVERSION + " = NULL");
+ }
+ if (oldVersion < 14 && newVersion >= 14) {
+ // migrate db to new, canonicalized JID domainpart representation
+
+ // Conversation table
+ Cursor cursor = db.rawQuery("select * from " + Conversation.TABLENAME, new String[0]);
+ while (cursor.moveToNext()) {
+ String newJid;
+ try {
+ newJid = Jid.fromString(
+ cursor.getString(cursor.getColumnIndex(Conversation.CONTACTJID))
+ ).toString();
+ } catch (InvalidJidException ignored) {
+ Logging.e(Config.LOGTAG, "Failed to migrate Conversation CONTACTJID "
+ + cursor.getString(cursor.getColumnIndex(Conversation.CONTACTJID))
+ + ": " + ignored + ". Skipping...");
+ continue;
+ }
+
+ String updateArgs[] = {
+ newJid,
+ cursor.getString(cursor.getColumnIndex(Conversation.UUID)),
+ };
+ db.execSQL("update " + Conversation.TABLENAME
+ + " set " + Conversation.CONTACTJID + " = ? "
+ + " where " + Conversation.UUID + " = ?", updateArgs);
+ }
+ cursor.close();
+
+ // Contact table
+ cursor = db.rawQuery("select * from " + Contact.TABLENAME, new String[0]);
+ while (cursor.moveToNext()) {
+ String newJid;
+ try {
+ newJid = Jid.fromString(
+ cursor.getString(cursor.getColumnIndex(Contact.JID))
+ ).toString();
+ } catch (InvalidJidException ignored) {
+ Logging.e(Config.LOGTAG, "Failed to migrate Contact JID "
+ + cursor.getString(cursor.getColumnIndex(Contact.JID))
+ + ": " + ignored + ". Skipping...");
+ continue;
+ }
+
+ String updateArgs[] = {
+ newJid,
+ cursor.getString(cursor.getColumnIndex(Contact.ACCOUNT)),
+ cursor.getString(cursor.getColumnIndex(Contact.JID)),
+ };
+ db.execSQL("update " + Contact.TABLENAME
+ + " set " + Contact.JID + " = ? "
+ + " where " + Contact.ACCOUNT + " = ? "
+ + " AND " + Contact.JID + " = ?", updateArgs);
+ }
+ cursor.close();
+
+ // Account table
+ cursor = db.rawQuery("select * from " + Account.TABLENAME, new String[0]);
+ while (cursor.moveToNext()) {
+ String newServer;
+ try {
+ newServer = Jid.fromParts(
+ cursor.getString(cursor.getColumnIndex(Account.USERNAME)),
+ cursor.getString(cursor.getColumnIndex(Account.SERVER)),
+ "mobile"
+ ).getDomainpart();
+ } catch (InvalidJidException ignored) {
+ Logging.e(Config.LOGTAG, "Failed to migrate Account SERVER "
+ + cursor.getString(cursor.getColumnIndex(Account.SERVER))
+ + ": " + ignored + ". Skipping...");
+ continue;
+ }
+
+ String updateArgs[] = {
+ newServer,
+ cursor.getString(cursor.getColumnIndex(Account.UUID)),
+ };
+ db.execSQL("update " + Account.TABLENAME
+ + " set " + Account.SERVER + " = ? "
+ + " where " + Account.UUID + " = ?", updateArgs);
+ }
+ cursor.close();
+ }
+ if (oldVersion < 15 && newVersion >= 15) {
+ recreateAxolotlDb(db);
+ db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "
+ + Message.FINGERPRINT + " TEXT");
+ }
+ if (oldVersion < 16 && newVersion >= 16) {
+ db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "
+ + Message.CARBON + " INTEGER");
+ }
+ if (oldVersion < 19 && newVersion >= 19) {
+ db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.DISPLAY_NAME + " TEXT");
+ }
+ if (oldVersion < 20 && newVersion >= 20) {
+ db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.HOSTNAME + " TEXT");
+ db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.PORT + " NUMBER DEFAULT 5222");
+ }
+ /* Any migrations that alter the Account table need to happen BEFORE this migration, as it
+ * depends on account de-serialization.
+ */
+ if (oldVersion < 17 && newVersion >= 17) {
+ List<Account> accounts = getAccounts(db);
+ for (Account account : accounts) {
+ String ownDeviceIdString = account.getKey(SQLiteAxolotlStore.JSONKEY_REGISTRATION_ID);
+ if (ownDeviceIdString == null) {
+ continue;
+ }
+ int ownDeviceId = Integer.valueOf(ownDeviceIdString);
+ AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toString(), ownDeviceId);
+ deleteSession(db, account, ownAddress);
+ IdentityKeyPair identityKeyPair = loadOwnIdentityKeyPair(db, account);
+ if (identityKeyPair != null) {
+ setIdentityKeyTrust(db, account, identityKeyPair.getPublicKey().getFingerprint().replaceAll("\\s", ""), XmppAxolotlSession.Trust.TRUSTED);
+ } else {
+ Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not load own identity key pair");
+ }
+ }
+ }
+ if (oldVersion < 18 && newVersion >= 18) {
+ db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.READ + " NUMBER DEFAULT 1");
+ }
+
+ if (oldVersion < 21 && newVersion >= 21) {
+ List<Account> accounts = getAccounts(db);
+ for (Account account : accounts) {
+ account.unsetPgpSignature();
+ db.update(Account.TABLENAME, account.getContentValues(), Account.UUID
+ + "=?", new String[]{account.getUuid()});
+ }
+ }
+
+ if (oldVersion < 22 && oldVersion >= 15 && newVersion >= 22) {
+ db.execSQL("ALTER TABLE " + SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN " + SQLiteAxolotlStore.CERTIFICATE);
+ }
+
+ if (oldVersion < 23 && newVersion >= 23) {
+ db.execSQL(CREATE_DISCOVERY_RESULTS_STATEMENT);
+ }
+
+ if (oldVersion < 24 && newVersion >= 24) {
+ db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.EDITED + " TEXT");
+ }
+
+ if (oldVersion < 25 && newVersion >= 25) {
+ db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.OOB + " INTEGER");
+ }
+ }
+
+ public static synchronized DatabaseBackend getInstance() {
+ return getInstance(ConversationsPlusApplication.getAppContext());
+ }
+
+ public static synchronized DatabaseBackend getInstance(Context context) {
+ if (instance == null) {
+ instance = new DatabaseBackend(context);
+ }
+ return instance;
+ }
+
+ public void createConversation(Conversation conversation) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ db.insert(Conversation.TABLENAME, null, conversation.getContentValues());
+ }
+
+ public void createMessage(Message message) {
+ Logging.d("db.msg.insert", "Inserting new message with uuid '" + message.getUuid() + "', isRead: " + message.isRead());
+
+ SQLiteDatabase db = this.getWritableDatabase();
+ db.beginTransaction();
+ db.insert(Message.TABLENAME, null, message.getContentValues());
+ db.insert(MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS, null, MessageDatabaseAccess.getAdditionalParametersContentValues(message));
+ db.setTransactionSuccessful();
+ db.endTransaction();
+ }
+
+ public void createAccount(Account account) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ db.insert(Account.TABLENAME, null, account.getContentValues());
+ }
+
+ public void insertDiscoveryResult(ServiceDiscoveryResult result) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ db.insert(ServiceDiscoveryResult.TABLENAME, null, result.getContentValues());
+ }
+
+ public ServiceDiscoveryResult findDiscoveryResult(final String hash, final String ver) {
+ SQLiteDatabase db = this.getReadableDatabase();
+ String[] selectionArgs = {hash, ver};
+ Cursor cursor = db.query(ServiceDiscoveryResult.TABLENAME, null,
+ ServiceDiscoveryResult.HASH + "=? AND " + ServiceDiscoveryResult.VER + "=?",
+ selectionArgs, null, null, null);
+ if (cursor.getCount() == 0) {
+ cursor.close();
+ return null;
+ }
+ cursor.moveToFirst();
+
+ ServiceDiscoveryResult result = null;
+ try {
+ result = new ServiceDiscoveryResult(cursor);
+ } catch (JSONException e) { /* result is still null */ }
+
+ cursor.close();
+ return result;
+ }
+
+ public CopyOnWriteArrayList<Conversation> getConversations(int status) {
+ CopyOnWriteArrayList<Conversation> list = new CopyOnWriteArrayList<>();
+ SQLiteDatabase db = this.getReadableDatabase();
+ String[] selectionArgs = {Integer.toString(status)};
+ Cursor cursor = db.rawQuery("select * from " + Conversation.TABLENAME
+ + " where " + Conversation.STATUS + " = ? order by "
+ + Conversation.CREATED + " desc", selectionArgs);
+ while (cursor.moveToNext()) {
+ list.add(Conversation.fromCursor(cursor));
+ }
+ cursor.close();
+ return list;
+ }
+
+ public ArrayList<Message> getMessages(Conversation conversations, int limit) {
+ return getMessages(conversations, limit, -1);
+ }
+
+ public ArrayList<Message> getMessages(Conversation conversation, int limit,
+ long timestamp) {
+ ArrayList<Message> list = new ArrayList<>();
+ SQLiteDatabase db = this.getReadableDatabase();
+ Cursor cursor;
+ if (timestamp == -1) {
+ String[] selectionArgs = {conversation.getUuid()};
+ cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION
+ + "=?", selectionArgs, null, null, Message.TIME_SENT
+ + " DESC", String.valueOf(limit));
+ } else {
+ String[] selectionArgs = {conversation.getUuid(),
+ Long.toString(timestamp)};
+ cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION
+ + "=? and " + Message.TIME_SENT + "<?", selectionArgs,
+ null, null, Message.TIME_SENT + " DESC",
+ String.valueOf(limit));
+ }
+ if (cursor.getCount() > 0) {
+ cursor.moveToLast();
+ do {
+ Message message = Message.fromCursor(cursor);
+ MessageDatabaseAccess.populateMessageParameters(db, message);
+ message.setConversation(conversation);
+ list.add(message);
+ } while (cursor.moveToPrevious());
+ }
+ cursor.close();
+ return list;
+ }
+
+ public Iterable<Message> getMessagesIterable(final Conversation conversation) {
+ return new Iterable<Message>() {
+ @Override
+ public Iterator<Message> iterator() {
+ class MessageIterator implements Iterator<Message> {
+ SQLiteDatabase db = getReadableDatabase();
+ String[] selectionArgs = {conversation.getUuid()};
+ Cursor cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION
+ + "=?", selectionArgs, null, null, Message.TIME_SENT
+ + " ASC", null);
+
+ public MessageIterator() {
+ cursor.moveToFirst();
+ }
+
+ @Override
+ public boolean hasNext() {
+ return !cursor.isAfterLast();
+ }
+
+ @Override
+ public Message next() {
+ Message message = Message.fromCursor(cursor);
+ MessageDatabaseAccess.populateMessageParameters(db, message);
+ cursor.moveToNext();
+ return message;
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ }
+ return new MessageIterator();
+ }
+ };
+ }
+
+ public Conversation findConversation(final Account account, final Jid contactJid) {
+ SQLiteDatabase db = this.getReadableDatabase();
+ String[] selectionArgs = {account.getUuid(),
+ contactJid.toBareJid().toString() + "/%",
+ contactJid.toBareJid().toString()
+ };
+ Cursor cursor = db.query(Conversation.TABLENAME, null,
+ Conversation.ACCOUNT + "=? AND (" + Conversation.CONTACTJID
+ + " like ? OR " + Conversation.CONTACTJID + "=?)", selectionArgs, null, null, null);
+ if (cursor.getCount() == 0) {
+ cursor.close();
+ return null;
+ }
+ cursor.moveToFirst();
+ Conversation conversation = Conversation.fromCursor(cursor);
+ cursor.close();
+ return conversation;
+ }
+
+ public void updateConversation(final Conversation conversation) {
+ final SQLiteDatabase db = this.getWritableDatabase();
+ final String[] args = {conversation.getUuid()};
+ db.update(Conversation.TABLENAME, conversation.getContentValues(),
+ Conversation.UUID + "=?", args);
+ }
+
+ public List<Account> getAccounts() {
+ SQLiteDatabase db = this.getReadableDatabase();
+ return getAccounts(db);
+ }
+
+ private List<Account> getAccounts(SQLiteDatabase db) {
+ List<Account> list = new ArrayList<>();
+ Cursor cursor = db.query(Account.TABLENAME, null, null, null, null,
+ null, null);
+ while (cursor.moveToNext()) {
+ list.add(Account.fromCursor(cursor));
+ }
+ cursor.close();
+ return list;
+ }
+
+ public void updateAccount(Account account) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ String[] args = {account.getUuid()};
+ db.update(Account.TABLENAME, account.getContentValues(), Account.UUID
+ + "=?", args);
+ }
+
+ public void deleteAccount(Account account) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ String[] args = {account.getUuid()};
+ db.delete(Account.TABLENAME, Account.UUID + "=?", args);
+ }
+
+ public boolean hasEnabledAccounts() {
+ SQLiteDatabase db = this.getReadableDatabase();
+ Cursor cursor = db.rawQuery("select count(" + Account.UUID + ") from "
+ + Account.TABLENAME + " where not options & (1 <<1)", null);
+ try {
+ cursor.moveToFirst();
+ int count = cursor.getInt(0);
+ return (count > 0);
+ } catch (SQLiteCantOpenDatabaseException e) {
+ return true; // better safe than sorry
+ } catch (RuntimeException e) {
+ return true; // better safe than sorry
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+
+ @Override
+ public SQLiteDatabase getWritableDatabase() {
+ SQLiteDatabase db = super.getWritableDatabase();
+ db.execSQL("PRAGMA foreign_keys=ON;");
+ return db;
+ }
+
+ public void updateMessage(Message message) {
+ this.updateMessage(message, message.getUuid());
+ }
+
+ public void updateMessage(Message message, String uuid) {
+ Logging.d("db.msg.update", "Updating message with uuid '" + uuid + "', isRead: " + message.isRead());
+
+ SQLiteDatabase db = this.getWritableDatabase();
+ String[] args = {uuid};
+ db.beginTransaction();
+ db.update(Message.TABLENAME, message.getContentValues(), Message.UUID
+ + "=?", args);
+ MessageDatabaseAccess.updateMessageParameters(db, message, uuid);
+ db.setTransactionSuccessful();
+ db.endTransaction();
+ }
+
+ public void readRoster(Roster roster) {
+ SQLiteDatabase db = this.getReadableDatabase();
+ Cursor cursor;
+ String args[] = {roster.getAccount().getUuid()};
+ cursor = db.query(Contact.TABLENAME, null, Contact.ACCOUNT + "=?", args, null, null, null);
+ while (cursor.moveToNext()) {
+ roster.initContact(Contact.fromCursor(cursor));
+ }
+ cursor.close();
+ }
+
+ public void writeRoster(final Roster roster) {
+ final Account account = roster.getAccount();
+ final SQLiteDatabase db = this.getWritableDatabase();
+ db.beginTransaction();
+ for (Contact contact : roster.getContacts()) {
+ if (contact.getOption(Contact.Options.IN_ROSTER)) {
+ db.insert(Contact.TABLENAME, null, contact.getContentValues());
+ } else {
+ String where = Contact.ACCOUNT + "=? AND " + Contact.JID + "=?";
+ String[] whereArgs = {account.getUuid(), contact.getJid().toString()};
+ db.delete(Contact.TABLENAME, where, whereArgs);
+ }
+ }
+ db.setTransactionSuccessful();
+ db.endTransaction();
+ account.setRosterVersion(roster.getVersion());
+ updateAccount(account);
+ }
+
+ public void deleteMessagesInConversation(Conversation conversation) {
+ Logging.d("db.msg.delete", "Deleting messages in conversation with uuid '" + conversation.getUuid());
+
+ SQLiteDatabase db = this.getWritableDatabase();
+ String[] args = {conversation.getUuid()};
+ db.delete(Message.TABLENAME, Message.CONVERSATION + "=?", args);
+ }
+
+ public Pair<Long, String> getLastMessageReceived(Account account) {
+ Cursor cursor = null;
+ try {
+ SQLiteDatabase db = this.getReadableDatabase();
+ String sql = "select messages.timeSent,messages.serverMsgId from accounts join conversations on accounts.uuid=conversations.accountUuid join messages on conversations.uuid=messages.conversationUuid where accounts.uuid=? and (messages.status=0 or messages.carbon=1 or messages.serverMsgId not null) order by messages.timesent desc limit 1";
+ String[] args = {account.getUuid()};
+ cursor = db.rawQuery(sql, args);
+ if (cursor.getCount() == 0) {
+ return null;
+ } else {
+ cursor.moveToFirst();
+ return new Pair<>(cursor.getLong(0), cursor.getString(1));
+ }
+ } catch (Exception e) {
+ return null;
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+
+ private Cursor getCursorForSession(Account account, AxolotlAddress contact) {
+ final SQLiteDatabase db = this.getReadableDatabase();
+ String[] columns = null;
+ String[] selectionArgs = {account.getUuid(),
+ contact.getName(),
+ Integer.toString(contact.getDeviceId())};
+ Cursor cursor = db.query(SQLiteAxolotlStore.SESSION_TABLENAME,
+ columns,
+ SQLiteAxolotlStore.ACCOUNT + " = ? AND "
+ + SQLiteAxolotlStore.NAME + " = ? AND "
+ + SQLiteAxolotlStore.DEVICE_ID + " = ? ",
+ selectionArgs,
+ null, null, null);
+
+ return cursor;
+ }
+
+ public SessionRecord loadSession(Account account, AxolotlAddress contact) {
+ SessionRecord session = null;
+ Cursor cursor = getCursorForSession(account, contact);
+ if (cursor.getCount() != 0) {
+ cursor.moveToFirst();
+ try {
+ session = new SessionRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), Base64.DEFAULT));
+ } catch (IOException e) {
+ cursor.close();
+ throw new AssertionError(e);
+ }
+ }
+ cursor.close();
+ return session;
+ }
+
+ public List<Integer> getSubDeviceSessions(Account account, AxolotlAddress contact) {
+ final SQLiteDatabase db = this.getReadableDatabase();
+ return getSubDeviceSessions(db, account, contact);
+ }
+
+ private List<Integer> getSubDeviceSessions(SQLiteDatabase db, Account account, AxolotlAddress contact) {
+ List<Integer> devices = new ArrayList<>();
+ String[] columns = {SQLiteAxolotlStore.DEVICE_ID};
+ String[] selectionArgs = {account.getUuid(),
+ contact.getName()};
+ Cursor cursor = db.query(SQLiteAxolotlStore.SESSION_TABLENAME,
+ columns,
+ SQLiteAxolotlStore.ACCOUNT + " = ? AND "
+ + SQLiteAxolotlStore.NAME + " = ?",
+ selectionArgs,
+ null, null, null);
+
+ while (cursor.moveToNext()) {
+ devices.add(cursor.getInt(
+ cursor.getColumnIndex(SQLiteAxolotlStore.DEVICE_ID)));
+ }
+
+ cursor.close();
+ return devices;
+ }
+
+ public boolean containsSession(Account account, AxolotlAddress contact) {
+ Cursor cursor = getCursorForSession(account, contact);
+ int count = cursor.getCount();
+ cursor.close();
+ return count != 0;
+ }
+
+ public void storeSession(Account account, AxolotlAddress contact, SessionRecord session) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ ContentValues values = new ContentValues();
+ values.put(SQLiteAxolotlStore.NAME, contact.getName());
+ values.put(SQLiteAxolotlStore.DEVICE_ID, contact.getDeviceId());
+ values.put(SQLiteAxolotlStore.KEY, Base64.encodeToString(session.serialize(), Base64.DEFAULT));
+ values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid());
+ db.insert(SQLiteAxolotlStore.SESSION_TABLENAME, null, values);
+ }
+
+ public void deleteSession(Account account, AxolotlAddress contact) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ deleteSession(db, account, contact);
+ }
+
+ private void deleteSession(SQLiteDatabase db, Account account, AxolotlAddress contact) {
+ String[] args = {account.getUuid(),
+ contact.getName(),
+ Integer.toString(contact.getDeviceId())};
+ db.delete(SQLiteAxolotlStore.SESSION_TABLENAME,
+ SQLiteAxolotlStore.ACCOUNT + " = ? AND "
+ + SQLiteAxolotlStore.NAME + " = ? AND "
+ + SQLiteAxolotlStore.DEVICE_ID + " = ? ",
+ args);
+ }
+
+ public void deleteAllSessions(Account account, AxolotlAddress contact) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ String[] args = {account.getUuid(), contact.getName()};
+ db.delete(SQLiteAxolotlStore.SESSION_TABLENAME,
+ SQLiteAxolotlStore.ACCOUNT + "=? AND "
+ + SQLiteAxolotlStore.NAME + " = ?",
+ args);
+ }
+
+ private Cursor getCursorForPreKey(Account account, int preKeyId) {
+ SQLiteDatabase db = this.getReadableDatabase();
+ String[] columns = {SQLiteAxolotlStore.KEY};
+ String[] selectionArgs = {account.getUuid(), Integer.toString(preKeyId)};
+ Cursor cursor = db.query(SQLiteAxolotlStore.PREKEY_TABLENAME,
+ columns,
+ SQLiteAxolotlStore.ACCOUNT + "=? AND "
+ + SQLiteAxolotlStore.ID + "=?",
+ selectionArgs,
+ null, null, null);
+
+ return cursor;
+ }
+
+ public PreKeyRecord loadPreKey(Account account, int preKeyId) {
+ PreKeyRecord record = null;
+ Cursor cursor = getCursorForPreKey(account, preKeyId);
+ if (cursor.getCount() != 0) {
+ cursor.moveToFirst();
+ try {
+ record = new PreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), Base64.DEFAULT));
+ } catch (IOException e) {
+ throw new AssertionError(e);
+ }
+ }
+ cursor.close();
+ return record;
+ }
+
+ public boolean containsPreKey(Account account, int preKeyId) {
+ Cursor cursor = getCursorForPreKey(account, preKeyId);
+ int count = cursor.getCount();
+ cursor.close();
+ return count != 0;
+ }
+
+ public void storePreKey(Account account, PreKeyRecord record) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ ContentValues values = new ContentValues();
+ values.put(SQLiteAxolotlStore.ID, record.getId());
+ values.put(SQLiteAxolotlStore.KEY, Base64.encodeToString(record.serialize(), Base64.DEFAULT));
+ values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid());
+ db.insert(SQLiteAxolotlStore.PREKEY_TABLENAME, null, values);
+ }
+
+ public void deletePreKey(Account account, int preKeyId) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ String[] args = {account.getUuid(), Integer.toString(preKeyId)};
+ db.delete(SQLiteAxolotlStore.PREKEY_TABLENAME,
+ SQLiteAxolotlStore.ACCOUNT + "=? AND "
+ + SQLiteAxolotlStore.ID + "=?",
+ args);
+ }
+
+ private Cursor getCursorForSignedPreKey(Account account, int signedPreKeyId) {
+ SQLiteDatabase db = this.getReadableDatabase();
+ String[] columns = {SQLiteAxolotlStore.KEY};
+ String[] selectionArgs = {account.getUuid(), Integer.toString(signedPreKeyId)};
+ Cursor cursor = db.query(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
+ columns,
+ SQLiteAxolotlStore.ACCOUNT + "=? AND " + SQLiteAxolotlStore.ID + "=?",
+ selectionArgs,
+ null, null, null);
+
+ return cursor;
+ }
+
+ public SignedPreKeyRecord loadSignedPreKey(Account account, int signedPreKeyId) {
+ SignedPreKeyRecord record = null;
+ Cursor cursor = getCursorForSignedPreKey(account, signedPreKeyId);
+ if (cursor.getCount() != 0) {
+ cursor.moveToFirst();
+ try {
+ record = new SignedPreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), Base64.DEFAULT));
+ } catch (IOException e) {
+ throw new AssertionError(e);
+ }
+ }
+ cursor.close();
+ return record;
+ }
+
+ public List<SignedPreKeyRecord> loadSignedPreKeys(Account account) {
+ List<SignedPreKeyRecord> prekeys = new ArrayList<>();
+ SQLiteDatabase db = this.getReadableDatabase();
+ String[] columns = {SQLiteAxolotlStore.KEY};
+ String[] selectionArgs = {account.getUuid()};
+ Cursor cursor = db.query(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
+ columns,
+ SQLiteAxolotlStore.ACCOUNT + "=?",
+ selectionArgs,
+ null, null, null);
+
+ while (cursor.moveToNext()) {
+ try {
+ prekeys.add(new SignedPreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), Base64.DEFAULT)));
+ } catch (IOException ignored) {
+ }
+ }
+ cursor.close();
+ return prekeys;
+ }
+
+ public boolean containsSignedPreKey(Account account, int signedPreKeyId) {
+ Cursor cursor = getCursorForPreKey(account, signedPreKeyId);
+ int count = cursor.getCount();
+ cursor.close();
+ return count != 0;
+ }
+
+ public void storeSignedPreKey(Account account, SignedPreKeyRecord record) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ ContentValues values = new ContentValues();
+ values.put(SQLiteAxolotlStore.ID, record.getId());
+ values.put(SQLiteAxolotlStore.KEY, Base64.encodeToString(record.serialize(), Base64.DEFAULT));
+ values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid());
+ db.insert(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, null, values);
+ }
+
+ public void deleteSignedPreKey(Account account, int signedPreKeyId) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ String[] args = {account.getUuid(), Integer.toString(signedPreKeyId)};
+ db.delete(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
+ SQLiteAxolotlStore.ACCOUNT + "=? AND "
+ + SQLiteAxolotlStore.ID + "=?",
+ args);
+ }
+
+ private Cursor getIdentityKeyCursor(Account account, String name, boolean own) {
+ final SQLiteDatabase db = this.getReadableDatabase();
+ return getIdentityKeyCursor(db, account, name, own);
+ }
+
+ private Cursor getIdentityKeyCursor(SQLiteDatabase db, Account account, String name, boolean own) {
+ return getIdentityKeyCursor(db, account, name, own, null);
+ }
+
+ private Cursor getIdentityKeyCursor(Account account, String fingerprint) {
+ final SQLiteDatabase db = this.getReadableDatabase();
+ return getIdentityKeyCursor(db, account, fingerprint);
+ }
+
+ private Cursor getIdentityKeyCursor(SQLiteDatabase db, Account account, String fingerprint) {
+ return getIdentityKeyCursor(db, account, null, null, fingerprint);
+ }
+
+ private Cursor getIdentityKeyCursor(SQLiteDatabase db, Account account, String name, Boolean own, String fingerprint) {
+ String[] columns = {SQLiteAxolotlStore.TRUSTED,
+ SQLiteAxolotlStore.KEY};
+ ArrayList<String> selectionArgs = new ArrayList<>(4);
+ selectionArgs.add(account.getUuid());
+ String selectionString = SQLiteAxolotlStore.ACCOUNT + " = ?";
+ if (name != null) {
+ selectionArgs.add(name);
+ selectionString += " AND " + SQLiteAxolotlStore.NAME + " = ?";
+ }
+ if (fingerprint != null) {
+ selectionArgs.add(fingerprint);
+ selectionString += " AND " + SQLiteAxolotlStore.FINGERPRINT + " = ?";
+ }
+ if (own != null) {
+ selectionArgs.add(own ? "1" : "0");
+ selectionString += " AND " + SQLiteAxolotlStore.OWN + " = ?";
+ }
+ Cursor cursor = db.query(SQLiteAxolotlStore.IDENTITIES_TABLENAME,
+ columns,
+ selectionString,
+ selectionArgs.toArray(new String[selectionArgs.size()]),
+ null, null, null);
+
+ return cursor;
+ }
+
+ public IdentityKeyPair loadOwnIdentityKeyPair(Account account) {
+ SQLiteDatabase db = getReadableDatabase();
+ return loadOwnIdentityKeyPair(db, account);
+ }
+
+ private IdentityKeyPair loadOwnIdentityKeyPair(SQLiteDatabase db, Account account) {
+ String name = account.getJid().toBareJid().toString();
+ IdentityKeyPair identityKeyPair = null;
+ Cursor cursor = getIdentityKeyCursor(db, account, name, true);
+ if (cursor.getCount() != 0) {
+ cursor.moveToFirst();
+ try {
+ identityKeyPair = new IdentityKeyPair(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), Base64.DEFAULT));
+ } catch (InvalidKeyException e) {
+ Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Encountered invalid IdentityKey in database for account" + account.getJid().toBareJid() + ", address: " + name);
+ }
+ }
+ cursor.close();
+
+ return identityKeyPair;
+ }
+
+ public Set<IdentityKey> loadIdentityKeys(Account account, String name) {
+ return loadIdentityKeys(account, name, null);
+ }
+
+ public Set<IdentityKey> loadIdentityKeys(Account account, String name, XmppAxolotlSession.Trust trust) {
+ Set<IdentityKey> identityKeys = new HashSet<>();
+ Cursor cursor = getIdentityKeyCursor(account, name, false);
+
+ while (cursor.moveToNext()) {
+ if (trust != null &&
+ cursor.getInt(cursor.getColumnIndex(SQLiteAxolotlStore.TRUSTED))
+ != trust.getCode()) {
+ continue;
+ }
+ try {
+ identityKeys.add(new IdentityKey(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), Base64.DEFAULT), 0));
+ } catch (InvalidKeyException e) {
+ Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Encountered invalid IdentityKey in database for account" + account.getJid().toBareJid() + ", address: " + name);
+ }
+ }
+ cursor.close();
+
+ return identityKeys;
+ }
+
+ public long numTrustedKeys(Account account, String name) {
+ SQLiteDatabase db = getReadableDatabase();
+ String[] args = {
+ account.getUuid(),
+ name,
+ String.valueOf(XmppAxolotlSession.Trust.TRUSTED.getCode()),
+ String.valueOf(XmppAxolotlSession.Trust.TRUSTED_X509.getCode())
+ };
+ return DatabaseUtils.queryNumEntries(db, SQLiteAxolotlStore.IDENTITIES_TABLENAME,
+ SQLiteAxolotlStore.ACCOUNT + " = ?"
+ + " AND " + SQLiteAxolotlStore.NAME + " = ?"
+ + " AND (" + SQLiteAxolotlStore.TRUSTED + " = ? OR " + SQLiteAxolotlStore.TRUSTED + " = ?)",
+ args
+ );
+ }
+
+ 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();
+ ContentValues values = new ContentValues();
+ values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid());
+ values.put(SQLiteAxolotlStore.NAME, name);
+ values.put(SQLiteAxolotlStore.OWN, own ? 1 : 0);
+ values.put(SQLiteAxolotlStore.FINGERPRINT, fingerprint);
+ values.put(SQLiteAxolotlStore.KEY, base64Serialized);
+ values.put(SQLiteAxolotlStore.TRUSTED, trusted.getCode());
+ db.insert(SQLiteAxolotlStore.IDENTITIES_TABLENAME, null, values);
+ }
+
+ public XmppAxolotlSession.Trust isIdentityKeyTrusted(Account account, String fingerprint) {
+ Cursor cursor = getIdentityKeyCursor(account, fingerprint);
+ XmppAxolotlSession.Trust trust = null;
+ if (cursor.getCount() > 0) {
+ cursor.moveToFirst();
+ int trustValue = cursor.getInt(cursor.getColumnIndex(SQLiteAxolotlStore.TRUSTED));
+ trust = XmppAxolotlSession.Trust.fromCode(trustValue);
+ }
+ cursor.close();
+ return trust;
+ }
+
+ public boolean setIdentityKeyTrust(Account account, String fingerprint, XmppAxolotlSession.Trust trust) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ return setIdentityKeyTrust(db, account, fingerprint, trust);
+ }
+
+ private boolean setIdentityKeyTrust(SQLiteDatabase db, Account account, String fingerprint, XmppAxolotlSession.Trust trust) {
+ String[] selectionArgs = {
+ account.getUuid(),
+ fingerprint
+ };
+ ContentValues values = new ContentValues();
+ values.put(SQLiteAxolotlStore.TRUSTED, trust.getCode());
+ int rows = db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, values,
+ SQLiteAxolotlStore.ACCOUNT + " = ? AND "
+ + SQLiteAxolotlStore.FINGERPRINT + " = ? ",
+ selectionArgs);
+ return rows == 1;
+ }
+
+ public boolean setIdentityKeyCertificate(Account account, String fingerprint, X509Certificate x509Certificate) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ String[] selectionArgs = {
+ account.getUuid(),
+ fingerprint
+ };
+ try {
+ ContentValues values = new ContentValues();
+ values.put(SQLiteAxolotlStore.CERTIFICATE, x509Certificate.getEncoded());
+ return db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, values,
+ SQLiteAxolotlStore.ACCOUNT + " = ? AND "
+ + SQLiteAxolotlStore.FINGERPRINT + " = ? ",
+ selectionArgs) == 1;
+ } catch (CertificateEncodingException e) {
+ Log.d(Config.LOGTAG, "could not encode certificate");
+ return false;
+ }
+ }
+
+ public X509Certificate getIdentityKeyCertifcate(Account account, String fingerprint) {
+ SQLiteDatabase db = this.getReadableDatabase();
+ String[] selectionArgs = {
+ account.getUuid(),
+ fingerprint
+ };
+ String[] colums = {SQLiteAxolotlStore.CERTIFICATE};
+ String selection = SQLiteAxolotlStore.ACCOUNT + " = ? AND " + SQLiteAxolotlStore.FINGERPRINT + " = ? ";
+ Cursor cursor = db.query(SQLiteAxolotlStore.IDENTITIES_TABLENAME, colums, selection, selectionArgs, null, null, null);
+ if (cursor.getCount() < 1) {
+ return null;
+ } else {
+ cursor.moveToFirst();
+ byte[] certificate = cursor.getBlob(cursor.getColumnIndex(SQLiteAxolotlStore.CERTIFICATE));
+ if (certificate == null || certificate.length == 0) {
+ return null;
+ }
+ try {
+ CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
+ return (X509Certificate) certificateFactory.generateCertificate(new ByteArrayInputStream(certificate));
+ } catch (CertificateException e) {
+ Log.d(Config.LOGTAG,"certificate exception "+e.getMessage());
+ return null;
+ }
+ }
+ }
+
+ public void storeIdentityKey(Account account, String name, IdentityKey identityKey) {
+ storeIdentityKey(account, name, false, identityKey.getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT));
+ }
+
+ public void storeOwnIdentityKeyPair(Account account, IdentityKeyPair identityKeyPair) {
+ storeIdentityKey(account, account.getJid().toBareJid().toString(), true, identityKeyPair.getPublicKey().getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKeyPair.serialize(), Base64.DEFAULT), XmppAxolotlSession.Trust.TRUSTED);
+ }
+
+ public void recreateAxolotlDb() {
+ recreateAxolotlDb(getWritableDatabase());
+ }
+
+ public void recreateAxolotlDb(SQLiteDatabase db) {
+ Log.d(Config.LOGTAG, AxolotlServiceImpl.LOGPREFIX + " : " + ">>> (RE)CREATING AXOLOTL DATABASE <<<");
+ db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.SESSION_TABLENAME);
+ db.execSQL(CREATE_SESSIONS_STATEMENT);
+ db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.PREKEY_TABLENAME);
+ db.execSQL(CREATE_PREKEYS_STATEMENT);
+ db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME);
+ db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT);
+ db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.IDENTITIES_TABLENAME);
+ db.execSQL(CREATE_IDENTITIES_STATEMENT);
+ }
+
+ public void wipeAxolotlDb(Account account) {
+ String accountName = account.getUuid();
+ Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + ">>> WIPING AXOLOTL DATABASE FOR ACCOUNT " + accountName + " <<<");
+ SQLiteDatabase db = this.getWritableDatabase();
+ String[] deleteArgs = {
+ accountName
+ };
+ db.delete(SQLiteAxolotlStore.SESSION_TABLENAME,
+ SQLiteAxolotlStore.ACCOUNT + " = ?",
+ deleteArgs);
+ db.delete(SQLiteAxolotlStore.PREKEY_TABLENAME,
+ SQLiteAxolotlStore.ACCOUNT + " = ?",
+ deleteArgs);
+ db.delete(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
+ SQLiteAxolotlStore.ACCOUNT + " = ?",
+ deleteArgs);
+ db.delete(SQLiteAxolotlStore.IDENTITIES_TABLENAME,
+ SQLiteAxolotlStore.ACCOUNT + " = ?",
+ deleteArgs);
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java
new file mode 100644
index 00000000..c7cf722d
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java
@@ -0,0 +1,274 @@
+package eu.siacs.conversations.persistance;
+
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Environment;
+import android.util.Log;
+import android.webkit.MimeTypeMap;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+import de.thedevstack.android.logcat.Logging;
+import de.thedevstack.conversationsplus.ConversationsPlusApplication;
+import de.thedevstack.conversationsplus.ConversationsPlusPreferences;
+import de.thedevstack.conversationsplus.entities.FileParams;
+import de.thedevstack.conversationsplus.exceptions.FileCopyException;
+import de.thedevstack.conversationsplus.persistance.observers.FileDeletionObserver;
+import de.thedevstack.conversationsplus.utils.StreamUtil;
+import de.thedevstack.conversationsplus.utils.XmppConnectionServiceAccessor;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.services.XmppConnectionService;
+
+public class FileBackend {
+ private static final SimpleDateFormat imageDateFormat = new SimpleDateFormat("yyyyMMdd_HHmmssSSS", Locale.US);
+ private static FileBackend INSTANCE;
+
+ private FileDeletionObserver privateFilesDirectoryObserver;
+ private FileDeletionObserver privateFilesImageDirectoryObserver;
+ private FileDeletionObserver conversationsFilesDirectoryObserver;
+ private FileDeletionObserver conversationsImagesDirectoryObserver;
+
+ public static void init() {
+ if (null == INSTANCE) {
+ INSTANCE = new FileBackend();
+ }
+ INSTANCE.initFileObservers();
+ INSTANCE.createNoMedia();
+ }
+
+ private void initFileObservers() {
+ this.privateFilesDirectoryObserver = new FileDeletionObserver(FileBackend.getPrivateFileDirectoryPath());
+ this.privateFilesImageDirectoryObserver = new FileDeletionObserver(FileBackend.getConversationsFileDirectory());
+ this.conversationsFilesDirectoryObserver = new FileDeletionObserver(FileBackend.getConversationsImageDirectory());
+ this.conversationsImagesDirectoryObserver = new FileDeletionObserver(FileBackend.getPrivateImageDirectoryPath());
+
+ this.privateFilesDirectoryObserver.startWatching();
+ this.privateFilesImageDirectoryObserver.startWatching();
+ this.conversationsFilesDirectoryObserver.startWatching();
+ this.conversationsImagesDirectoryObserver.startWatching();
+ }
+
+ private void createNoMedia() {
+ final File nomedia = new File(getConversationsFileDirectory()+".nomedia");
+ if (!nomedia.exists()) {
+ try {
+ nomedia.createNewFile();
+ } catch (Exception e) {
+ Log.d(Config.LOGTAG, "could not create nomedia file");
+ }
+ }
+ }
+
+ public static void onFileTransferFolderChanged() {
+ INSTANCE.conversationsFilesDirectoryObserver.stopWatching();
+ INSTANCE.conversationsFilesDirectoryObserver = new FileDeletionObserver(FileBackend.getConversationsFileDirectory());
+ INSTANCE.conversationsFilesDirectoryObserver.startWatching();
+ INSTANCE.createNoMedia();
+ }
+
+ public static void onImageTransferFolderChanged() {
+ INSTANCE.conversationsImagesDirectoryObserver.stopWatching();
+ INSTANCE.conversationsImagesDirectoryObserver = new FileDeletionObserver(FileBackend.getConversationsImageDirectory());
+ INSTANCE.conversationsImagesDirectoryObserver.startWatching();
+ }
+
+ public static void updateMediaScanner(File file, XmppConnectionService xmppConnectionService) {
+ if (file.getAbsolutePath().startsWith(getConversationsImageDirectory())) {
+ Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
+ intent.setData(Uri.fromFile(file));
+ xmppConnectionService.sendBroadcast(intent);
+ }
+ }
+
+ public static boolean deleteFile(Message message) {
+ File file = getFile(message);
+ if (file.delete()) {
+ updateMediaScanner(file, XmppConnectionServiceAccessor.xmppConnectionService);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public static DownloadableFile getFile(Message message) {
+ return getFile(message, true);
+ }
+
+ public static DownloadableFile getFile(Message message, boolean decrypted) {
+ DownloadableFile downloadableFile = new DownloadableFile(getFilePath(message, decrypted));
+ FileParams fileParams = message.getFileParams();
+ if (null != fileParams) {
+ if (null != fileParams.getKey()) {
+ downloadableFile.setKey(fileParams.getKey());
+ }
+ if (null != fileParams.getIv()) {
+ downloadableFile.setIv(fileParams.getIv());
+ }
+ }
+
+ return downloadableFile;
+ }
+
+ protected static String getFilePath(Message message, String extension) {
+ String path = FileBackend.getFilePath(message, true);
+ if (!path.endsWith(extension)) {
+ path += "." + extension;
+ }
+
+ return path;
+ }
+
+ protected static String getFilePath(Message message, Uri uri) {
+ String mime = ConversationsPlusApplication.getInstance().getContentResolver().getType(uri);
+ String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mime);
+
+ return getFilePath(message, extension);
+ }
+
+ protected static String getFilePath(Message message, boolean decrypted) {
+ final boolean encrypted = !decrypted
+ && (message.getEncryption() == Message.ENCRYPTION_PGP
+ || message.getEncryption() == Message.ENCRYPTION_DECRYPTED);
+ FileParams fileParams = message.getFileParams();
+ if (null == fileParams) {
+ fileParams = new FileParams();
+ message.setFileParams(fileParams);
+ }
+ String path = fileParams.getPath();
+
+ if (null == path) { // File does not yet exist
+ path = message.getUuid();
+ String mime = message.getMimeType();
+ if (mime != null && mime.startsWith("image")) { // TODO: Check if this can be determined in a better way
+ path = getConversationsImageDirectory() + path;
+ } else {
+ path = getConversationsFileDirectory() + path;
+ }
+ String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mime);
+ path += "." + extension;
+
+ fileParams.setPath(path);
+ }
+ if (encrypted) {
+ path += ".pgp";
+ }
+ return path;
+ }
+
+ public static String getConversationsFileDirectory() {
+ return Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + ConversationsPlusPreferences.fileTransferFolder() + File.separator;
+ }
+
+ public static String getConversationsImageDirectory() {
+ return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getAbsolutePath() + File.separator + ConversationsPlusPreferences.imgTransferFolder() + File.separator;
+ }
+
+ public static String getPrivateFileDirectoryPath() {
+ return ConversationsPlusApplication.getPrivateFilesDir().getAbsolutePath();
+ }
+
+ private static String getPrivateImageDirectoryPath() {
+ return FileBackend.getPrivateFileDirectoryPath() + File.separator + "Images" + File.separator;
+ }
+
+ public static void copyFileToPrivateStorage(File file, Uri uri) throws FileCopyException {
+ file.getParentFile().mkdirs();
+ OutputStream os = null;
+ InputStream is = null;
+ try {
+ file.createNewFile();
+ os = new FileOutputStream(file);
+ is = StreamUtil.openInputStreamFromContentResolver(uri);
+ byte[] buffer = new byte[1024];
+ int length;
+ while ((length = is.read(buffer)) > 0) {
+ os.write(buffer, 0, length);
+ }
+ os.flush();
+ } catch(FileNotFoundException e) {
+ throw new FileCopyException(R.string.error_file_not_found);
+ } catch (IOException e) {
+ e.printStackTrace();
+ throw new FileCopyException(R.string.error_io_exception);
+ } finally {
+ StreamUtil.close(os);
+ StreamUtil.close(is);
+ }
+ Logging.d(Config.LOGTAG, "output file name " + file);
+ }
+
+ public static void copyFileToPrivateStorage(Message message, Uri uri) throws FileCopyException {
+ Log.d(Config.LOGTAG, "copy " + uri.toString() + " to private storage");
+ String path = getFilePath(message, uri);
+ message.getFileParams().setPath(path);
+ message.setRelativeFilePath(path); // TODO: Remove
+ copyFileToPrivateStorage(getFile(message), uri);
+ }
+
+ public static DownloadableFile compressImageAndCopyToPrivateStorage(Message message, Bitmap scaledBitmap) throws FileCopyException {
+ String path = getFilePath(message, "jpg");
+ message.getFileParams().setPath(path);
+ message.setRelativeFilePath(path); // TODO: Remove
+ DownloadableFile file = getFile(message);
+ file.getParentFile().mkdirs();
+ OutputStream os = null;
+ try {
+ file.createNewFile();
+ os = new FileOutputStream(file);
+
+ boolean success = scaledBitmap.compress(Bitmap.CompressFormat.JPEG, 75, os);
+ if (!success) {
+ throw new FileCopyException(R.string.error_compressing_image);
+ }
+ os.flush();
+ } catch (IOException e) {
+ throw new FileCopyException(R.string.error_io_exception, e);
+ } catch (SecurityException e) {
+ throw new FileCopyException(R.string.error_security_exception_during_image_copy);
+ } catch (NullPointerException e) {
+ throw new FileCopyException(R.string.error_io_exception);
+ } finally {
+ StreamUtil.close(os);
+ }
+ return file;
+ }
+
+ public static Uri getTakePhotoUri() {
+ StringBuilder pathBuilder = new StringBuilder();
+ pathBuilder.append(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM));
+ pathBuilder.append('/');
+ pathBuilder.append("Camera");
+ pathBuilder.append('/');
+ pathBuilder.append("IMG_" + imageDateFormat.format(new Date()) + ".jpg");
+ Uri uri = Uri.parse("file://" + pathBuilder.toString());
+ File file = new File(uri.toString());
+ file.getParentFile().mkdirs();
+ return uri;
+ }
+
+ public static Uri getJingleFileUri(Message message) {
+ File file = getFile(message);
+ return Uri.parse("file://" + file.getAbsolutePath());
+ }
+
+ public static boolean isFileAvailable(Message message) {
+ return getFile(message).exists();
+ }
+
+ private FileBackend() {
+ // Static helper class
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/persistance/OnPhoneContactsMerged.java b/src/main/java/eu/siacs/conversations/persistance/OnPhoneContactsMerged.java
new file mode 100644
index 00000000..6a457b17
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/persistance/OnPhoneContactsMerged.java
@@ -0,0 +1,5 @@
+package eu.siacs.conversations.persistance;
+
+public interface OnPhoneContactsMerged {
+ public void phoneContactsMerged();
+}
diff --git a/src/main/java/eu/siacs/conversations/providers/ConversationsPlusFileProvider.java b/src/main/java/eu/siacs/conversations/providers/ConversationsPlusFileProvider.java
new file mode 100644
index 00000000..2146ea53
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/providers/ConversationsPlusFileProvider.java
@@ -0,0 +1,20 @@
+package eu.siacs.conversations.providers;
+
+import android.net.Uri;
+import android.support.v4.content.FileProvider;
+
+import de.thedevstack.conversationsplus.ConversationsPlusApplication;
+import eu.siacs.conversations.entities.DownloadableFile;
+
+/**
+ * Created by lookshe on 27.03.16.
+ */
+public class ConversationsPlusFileProvider extends FileProvider {
+
+ private static final String SCHEME = "content";
+ private static final String AUTHORITY = "de.thedevstack.conversationsplus";
+
+ public static Uri createUriForPrivateFile(DownloadableFile file) {
+ return FileProvider.getUriForFile(ConversationsPlusApplication.getAppContext(), AUTHORITY, file);
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java b/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java
new file mode 100644
index 00000000..dbee8720
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java
@@ -0,0 +1,121 @@
+package eu.siacs.conversations.services;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.os.PowerManager;
+import android.util.Log;
+import android.util.Pair;
+
+import org.bouncycastle.crypto.engines.AESEngine;
+import org.bouncycastle.crypto.modes.AEADBlockCipher;
+import org.bouncycastle.crypto.modes.GCMBlockCipher;
+import org.bouncycastle.crypto.params.AEADParameters;
+import org.bouncycastle.crypto.params.KeyParameter;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+import javax.crypto.Cipher;
+import javax.crypto.CipherInputStream;
+import javax.crypto.CipherOutputStream;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+import de.thedevstack.conversationsplus.ConversationsPlusApplication;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.DownloadableFile;
+
+public class AbstractConnectionManager {
+
+ public boolean hasStoragePermission() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ return ConversationsPlusApplication.getAppContext().checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
+ } else {
+ return true;
+ }
+ }
+
+ public static Pair<InputStream,Integer> createInputStream(DownloadableFile file, boolean gcm) throws FileNotFoundException {
+ FileInputStream is;
+ int size;
+ is = new FileInputStream(file);
+ size = (int) file.getSize();
+ if (file.getKey() == null) {
+ return new Pair<InputStream,Integer>(is,size);
+ }
+ try {
+ if (gcm) {
+ AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine());
+ cipher.init(true, new AEADParameters(new KeyParameter(file.getKey()), 128, file.getIv()));
+ InputStream cis = new org.bouncycastle.crypto.io.CipherInputStream(is, cipher);
+ return new Pair<>(cis, cipher.getOutputSize(size));
+ } else {
+ IvParameterSpec ips = new IvParameterSpec(file.getIv());
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+ cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(file.getKey(), "AES"), ips);
+ Log.d(Config.LOGTAG, "opening encrypted input stream");
+ final int s = Config.REPORT_WRONG_FILESIZE_IN_OTR_JINGLE ? size : (size / 16 + 1) * 16;
+ return new Pair<InputStream,Integer>(new CipherInputStream(is, cipher),s);
+ }
+ } catch (InvalidKeyException e) {
+ return null;
+ } catch (NoSuchAlgorithmException e) {
+ return null;
+ } catch (NoSuchPaddingException e) {
+ return null;
+ } catch (InvalidAlgorithmParameterException e) {
+ return null;
+ }
+ }
+
+ public static OutputStream createAppendedOutputStream(DownloadableFile file) {
+ return createOutputStream(file, false, true);
+ }
+
+ public static OutputStream createOutputStream(DownloadableFile file, boolean gcm) {
+ return createOutputStream(file, gcm, false);
+ }
+
+ private static OutputStream createOutputStream(DownloadableFile file, boolean gcm, boolean append) {
+ FileOutputStream os;
+ try {
+ os = new FileOutputStream(file, append);
+ if (file.getKey() == null) {
+ return os;
+ }
+ } catch (FileNotFoundException e) {
+ return null;
+ }
+ try {
+ if (gcm) {
+ AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine());
+ cipher.init(false, new AEADParameters(new KeyParameter(file.getKey()), 128, file.getIv()));
+ return new org.bouncycastle.crypto.io.CipherOutputStream(os, cipher);
+ } else {
+ IvParameterSpec ips = new IvParameterSpec(file.getIv());
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+ cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(file.getKey(), "AES"), ips);
+ Log.d(Config.LOGTAG, "opening encrypted output stream");
+ return new CipherOutputStream(os, cipher);
+ }
+ } catch (InvalidKeyException e) {
+ return null;
+ } catch (NoSuchAlgorithmException e) {
+ return null;
+ } catch (NoSuchPaddingException e) {
+ return null;
+ } catch (InvalidAlgorithmParameterException e) {
+ return null;
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/services/AvatarService.java b/src/main/java/eu/siacs/conversations/services/AvatarService.java
new file mode 100644
index 00000000..16d9539a
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/services/AvatarService.java
@@ -0,0 +1,709 @@
+package eu.siacs.conversations.services;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.net.Uri;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+
+import de.thedevstack.android.logcat.Logging;
+import de.thedevstack.conversationsplus.ConversationsPlusApplication;
+import de.thedevstack.conversationsplus.utils.AvatarUtil;
+import de.thedevstack.conversationsplus.utils.ImageUtil;
+import de.thedevstack.conversationsplus.utils.UiUpdateHelper;
+import de.thedevstack.conversationsplus.utils.XmppSendUtil;
+import de.thedevstack.conversationsplus.xmpp.avatar.AvatarPacketGenerator;
+import de.thedevstack.conversationsplus.xmpp.avatar.AvatarPacketParser;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Bookmark;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.ListItem;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.MucOptions;
+import eu.siacs.conversations.generator.IqGenerator;
+import eu.siacs.conversations.persistance.DatabaseBackend;
+import eu.siacs.conversations.persistance.FileBackend;
+import eu.siacs.conversations.ui.UiCallback;
+import eu.siacs.conversations.utils.UIHelper;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded;
+import eu.siacs.conversations.xmpp.OnIqPacketReceived;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import eu.siacs.conversations.xmpp.pep.Avatar;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
+
+ private static final int FG_COLOR = 0xFFFAFAFA;
+ private static final int TRANSPARENT = 0x00000000;
+ private static final int PLACEHOLDER_COLOR = 0xFF202020;
+
+ private static final String PREFIX_CONTACT = "contact";
+ private static final String PREFIX_CONVERSATION = "conversation";
+ private static final String PREFIX_ACCOUNT = "account";
+ private static final String PREFIX_GENERIC = "generic";
+ private static final AvatarService INSTANCE = new AvatarService();
+
+ final private ArrayList<Integer> sizes = new ArrayList<>();
+ private final List<String> mInProgressAvatarFetches = new ArrayList<>();
+
+ public static AvatarService getInstance() {
+ return INSTANCE;
+ }
+
+ private Bitmap get(final Contact contact, final int size, boolean cachedOnly) {
+ final String KEY = key(contact, size);
+ Bitmap avatar = ImageUtil.getBitmapFromCache(KEY);
+ if (avatar != null || cachedOnly) {
+ return avatar;
+ }
+ if (contact.getProfilePhoto() != null) {
+ avatar = ImageUtil.cropCenterSquare(Uri.parse(contact.getProfilePhoto()), size);
+ }
+ if (avatar == null && contact.getAvatar() != null) {
+ avatar = AvatarUtil.getAvatar(contact.getAvatar(), size);
+ }
+ if (avatar == null) {
+ avatar = get(contact.getDisplayName(), size, cachedOnly);
+ }
+ ImageUtil.addBitmapToCache(KEY, avatar);
+ return avatar;
+ }
+
+ public Bitmap get(final MucOptions.User user, final int size, boolean cachedOnly) {
+ Contact c = user.getContact();
+ if (c != null && (c.getProfilePhoto() != null || c.getAvatar() != null)) {
+ return get(c, size, cachedOnly);
+ } else {
+ return getImpl(user, size, cachedOnly);
+ }
+ }
+
+ private Bitmap getImpl(final MucOptions.User user, final int size, boolean cachedOnly) {
+ final String KEY = key(user, size);
+ Bitmap avatar = ImageUtil.getBitmapFromCache(KEY);
+ if (avatar != null || cachedOnly) {
+ return avatar;
+ }
+ if (user.getAvatar() != null) {
+ avatar = AvatarUtil.getAvatar(user.getAvatar(), size);
+ }
+ if (avatar == null) {
+ Contact contact = user.getContact();
+ if (contact != null) {
+ avatar = get(contact, size, cachedOnly);
+ } else {
+ avatar = get(user.getName(), size, cachedOnly);
+ }
+ }
+ ImageUtil.addBitmapToCache(KEY, avatar);
+ return avatar;
+ }
+
+ public void clear(Contact contact) {
+ synchronized (this.sizes) {
+ for (Integer size : sizes) {
+ ImageUtil.removeBitmapFromCache(key(contact, size));
+ }
+ }
+ }
+
+ private String key(Contact contact, int size) {
+ synchronized (this.sizes) {
+ if (!this.sizes.contains(size)) {
+ this.sizes.add(size);
+ }
+ }
+ return PREFIX_CONTACT + "_" + contact.getAccount().getJid().toBareJid() + "_"
+ + contact.getJid() + "_" + String.valueOf(size);
+ }
+
+ private String key(MucOptions.User user, int size) {
+ synchronized (this.sizes) {
+ if (!this.sizes.contains(size)) {
+ this.sizes.add(size);
+ }
+ }
+ return PREFIX_CONTACT + "_" + user.getAccount().getJid().toBareJid() + "_"
+ + user.getFullJid() + "_" + String.valueOf(size);
+ }
+
+ public Bitmap get(ListItem item, int size) {
+ return get(item,size,false);
+ }
+
+ public Bitmap get(ListItem item, int size, boolean cachedOnly) {
+ if (item instanceof Contact) {
+ return get((Contact) item, size,cachedOnly);
+ } else if (item instanceof Bookmark) {
+ Bookmark bookmark = (Bookmark) item;
+ if (bookmark.getConversation() != null) {
+ return get(bookmark.getConversation(), size, cachedOnly);
+ } else {
+ return get(bookmark.getDisplayName(), size, cachedOnly);
+ }
+ } else {
+ return get(item.getDisplayName(), size, cachedOnly);
+ }
+ }
+
+ public Bitmap get(Conversation conversation, int size) {
+ return get(conversation,size,false);
+ }
+
+ public Bitmap get(Conversation conversation, int size, boolean cachedOnly) {
+ if (conversation.getMode() == Conversation.MODE_SINGLE) {
+ return get(conversation.getContact(), size, cachedOnly);
+ } else {
+ return get(conversation.getMucOptions(), size, cachedOnly);
+ }
+ }
+
+ public void clear(Conversation conversation) {
+ if (conversation.getMode() == Conversation.MODE_SINGLE) {
+ clear(conversation.getContact());
+ } else {
+ clear(conversation.getMucOptions());
+ }
+ }
+
+ private Bitmap get(MucOptions mucOptions, int size, boolean cachedOnly) {
+ final String KEY = key(mucOptions, size);
+ Bitmap bitmap = ImageUtil.getBitmapFromCache(KEY);
+ if (bitmap != null || cachedOnly) {
+ return bitmap;
+ }
+ final List<MucOptions.User> users = mucOptions.getUsers();
+ int count = users.size();
+ bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ bitmap.eraseColor(TRANSPARENT);
+
+ if (count == 0) {
+ String name = mucOptions.getConversation().getName();
+ drawTile(canvas, name, 0, 0, size, size);
+ } else if (count == 1) {
+ drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size);
+ drawTile(canvas, mucOptions.getConversation().getAccount(), size / 2 + 1, 0, size, size);
+ } else if (count == 2) {
+ drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size);
+ drawTile(canvas, users.get(1), size / 2 + 1, 0, size, size);
+ } else if (count == 3) {
+ drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size);
+ drawTile(canvas, users.get(1), size / 2 + 1, 0, size, size / 2 - 1);
+ drawTile(canvas, users.get(2), size / 2 + 1, size / 2 + 1, size,
+ size);
+ } else if (count == 4) {
+ drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size / 2 - 1);
+ drawTile(canvas, users.get(1), 0, size / 2 + 1, size / 2 - 1, size);
+ drawTile(canvas, users.get(2), size / 2 + 1, 0, size, size / 2 - 1);
+ drawTile(canvas, users.get(3), size / 2 + 1, size / 2 + 1, size,
+ size);
+ } else {
+ drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size / 2 - 1);
+ drawTile(canvas, users.get(1), 0, size / 2 + 1, size / 2 - 1, size);
+ drawTile(canvas, users.get(2), size / 2 + 1, 0, size, size / 2 - 1);
+ drawTile(canvas, "\u2026", PLACEHOLDER_COLOR, size / 2 + 1, size / 2 + 1,
+ size, size);
+ }
+ ImageUtil.addBitmapToCache(KEY, bitmap);
+ return bitmap;
+ }
+
+ public void clear(MucOptions options) {
+ synchronized (this.sizes) {
+ for (Integer size : sizes) {
+ ImageUtil.removeBitmapFromCache(key(options, size));
+ }
+ }
+ }
+
+ private String key(MucOptions options, int size) {
+ synchronized (this.sizes) {
+ if (!this.sizes.contains(size)) {
+ this.sizes.add(size);
+ }
+ }
+ return PREFIX_CONVERSATION + "_" + options.getConversation().getUuid()
+ + "_" + String.valueOf(size);
+ }
+
+ public Bitmap get(Account account, int size) {
+ return get(account, size, false);
+ }
+
+ public Bitmap get(Account account, int size, boolean cachedOnly) {
+ final String KEY = key(account, size);
+ Bitmap avatar = ImageUtil.getBitmapFromCache(KEY);
+ if (avatar != null || cachedOnly) {
+ return avatar;
+ }
+ avatar = AvatarUtil.getAvatar(account.getAvatar(), size);
+ if (avatar == null) {
+ avatar = get(account.getJid().toBareJid().toString(), size,false);
+ }
+ ImageUtil.addBitmapToCache(KEY, avatar);
+ return avatar;
+ }
+
+ public Bitmap get(Message message, int size, boolean cachedOnly) {
+ final Conversation conversation = message.getConversation();
+ if (message.getStatus() == Message.STATUS_RECEIVED) {
+ Contact c = message.getContact();
+ if (c != null && (c.getProfilePhoto() != null || c.getAvatar() != null)) {
+ return get(c, size, cachedOnly);
+ } else if (message.getConversation().getMode() == Conversation.MODE_MULTI){
+ MucOptions.User user = conversation.getMucOptions().findUser(message.getCounterpart().getResourcepart());
+ if (user != null) {
+ return getImpl(user,size,cachedOnly);
+ }
+ }
+ return get(UIHelper.getMessageDisplayName(message), size, cachedOnly);
+ } else {
+ return get(conversation.getAccount(), size, cachedOnly);
+ }
+ }
+
+ public void clear(Account account) {
+ synchronized (this.sizes) {
+ for (Integer size : sizes) {
+ ImageUtil.removeBitmapFromCache(key(account, size));
+ }
+ }
+ }
+
+ public void clear(MucOptions.User user) {
+ synchronized (this.sizes) {
+ for (Integer size : sizes) {
+ ImageUtil.removeBitmapFromCache(key(user, size));
+ }
+ }
+ }
+
+ private String key(Account account, int size) {
+ synchronized (this.sizes) {
+ if (!this.sizes.contains(size)) {
+ this.sizes.add(size);
+ }
+ }
+ return PREFIX_ACCOUNT + "_" + account.getUuid() + "_"
+ + String.valueOf(size);
+ }
+
+ public Bitmap get(String name, int size) {
+ return get(name,size,false);
+ }
+
+ public Bitmap get(final String name, final int size, boolean cachedOnly) {
+ final String KEY = key(name, size);
+ Bitmap bitmap = ImageUtil.getBitmapFromCache(KEY);
+ if (bitmap != null || cachedOnly) {
+ return bitmap;
+ }
+ bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ drawTile(canvas, name, 0, 0, size, size);
+ ImageUtil.addBitmapToCache(KEY, bitmap);
+ return bitmap;
+ }
+
+ private String key(String name, int size) {
+ synchronized (this.sizes) {
+ if (!this.sizes.contains(size)) {
+ this.sizes.add(size);
+ }
+ }
+ return PREFIX_GENERIC + "_" + name + "_" + String.valueOf(size);
+ }
+
+ private boolean drawTile(Canvas canvas, String letter, int tileColor,
+ int left, int top, int right, int bottom) {
+ letter = letter.toUpperCase(Locale.getDefault());
+ Paint tilePaint = new Paint(), textPaint = new Paint();
+ tilePaint.setColor(tileColor);
+ textPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
+ textPaint.setColor(FG_COLOR);
+ textPaint.setTypeface(Typeface.create("sans-serif-light",
+ Typeface.NORMAL));
+ textPaint.setTextSize((float) ((right - left) * 0.8));
+ Rect rect = new Rect();
+
+ canvas.drawRect(new Rect(left, top, right, bottom), tilePaint);
+ textPaint.getTextBounds(letter, 0, 1, rect);
+ float width = textPaint.measureText(letter);
+ canvas.drawText(letter, (right + left) / 2 - width / 2, (top + bottom)
+ / 2 + rect.height() / 2, textPaint);
+ return true;
+ }
+
+ private boolean drawTile(Canvas canvas, MucOptions.User user, int left,
+ int top, int right, int bottom) {
+ Contact contact = user.getContact();
+ if (contact != null) {
+ Uri uri = null;
+ if (contact.getProfilePhoto() != null) {
+ uri = Uri.parse(contact.getProfilePhoto());
+ } else if (contact.getAvatar() != null) {
+ uri = AvatarUtil.getAvatarUri(contact.getAvatar());
+ }
+ if (drawTile(canvas, uri, left, top, right, bottom)) {
+ return true;
+ }
+ } else if (user.getAvatar() != null) {
+ Uri uri = AvatarUtil.getAvatarUri(user.getAvatar());
+ if (drawTile(canvas, uri, left, top, right, bottom)) {
+ return true;
+ }
+ }
+ String name = contact != null ? contact.getDisplayName() : user.getName();
+ drawTile(canvas, name, left, top, right, bottom);
+ return true;
+ }
+
+ private boolean drawTile(Canvas canvas, Account account, int left, int top, int right, int bottom) {
+ String avatar = account.getAvatar();
+ if (avatar != null) {
+ Uri uri = AvatarUtil.getAvatarUri(avatar);
+ if (uri != null) {
+ if (drawTile(canvas, uri, left, top, right, bottom)) {
+ return true;
+ }
+ }
+ }
+ return drawTile(canvas, account.getJid().toBareJid().toString(), left, top, right, bottom);
+ }
+
+ private boolean drawTile(Canvas canvas, String name, int left, int top, int right, int bottom) {
+ if (name != null) {
+ String trimmedName = name.trim();
+ final String letter = trimmedName.isEmpty() ? "X" : trimmedName.substring(0, 1);
+ final int color = UIHelper.getColorForName(name);
+ drawTile(canvas, letter, color, left, top, right, bottom);
+ return true;
+ }
+ return false;
+ }
+
+ private boolean drawTile(Canvas canvas, Uri uri, int left, int top, int right, int bottom) {
+ if (uri != null) {
+ Bitmap bitmap = ImageUtil.cropCenter(uri, bottom - top, right - left);
+ if (bitmap != null) {
+ drawTile(canvas, bitmap, left, top, right, bottom);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean drawTile(Canvas canvas, Bitmap bm, int dstleft, int dsttop,
+ int dstright, int dstbottom) {
+ Rect dst = new Rect(dstleft, dsttop, dstright, dstbottom);
+ canvas.drawBitmap(bm, null, dst, null);
+ return true;
+ }
+
+ @Override
+ public void onAdvancedStreamFeaturesAvailable(Account account) {
+ XmppConnection.Features features = account.getXmppConnection().getFeatures();
+ if (features.pep() && !features.pepPersistent()) {
+ Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": has pep but is not persistent");
+ if (account.getAvatar() != null) {
+ republishAvatarIfNeeded(account);
+ }
+ }
+ }
+
+ public void publishAvatar(final Account account,
+ final Uri image,
+ final UiCallback<Avatar> callback) {
+ final Bitmap.CompressFormat format = Config.AVATAR_FORMAT;
+ final int size = Config.AVATAR_SIZE;
+ final Avatar avatar = AvatarUtil.getPepAvatar(image, size, format);
+ if (avatar != null) {
+ avatar.height = size;
+ avatar.width = size;
+ if (format.equals(Bitmap.CompressFormat.WEBP)) {
+ avatar.type = "image/webp";
+ } else if (format.equals(Bitmap.CompressFormat.JPEG)) {
+ avatar.type = "image/jpeg";
+ } else if (format.equals(Bitmap.CompressFormat.PNG)) {
+ avatar.type = "image/png";
+ }
+ if (!AvatarUtil.save(avatar)) {
+ callback.error(R.string.error_saving_avatar, avatar);
+ return;
+ }
+ sendAndReceiveIqPackages(avatar, account, callback);
+ } else {
+ callback.error(R.string.error_publish_avatar_converting, null);
+ }
+ }
+
+ public void publishAvatar(Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
+ final IqPacket packet = AvatarPacketGenerator.generatePublishAvatarPacket(avatar);
+ XmppSendUtil.sendIqPacket(account, packet, new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket result) {
+ if (result.getType() == IqPacket.TYPE.RESULT) {
+ sendAndReceiveIqPackages(avatar, account, callback);
+ } else {
+ if (callback != null) {
+ callback.error(
+ R.string.error_publish_avatar_server_reject,
+ avatar);
+ }
+ }
+ }
+ });
+ }
+
+ private static String generateFetchKey(Account account, final Avatar avatar) {
+ return account.getJid().toBareJid()+"_"+avatar.owner+"_"+avatar.sha1sum;
+ }
+
+ public void republishAvatarIfNeeded(Account account) {
+ if (account.getAxolotlService().isPepBroken()) {
+ Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": skipping republication of avatar because pep is broken");
+ return;
+ }
+ IqPacket packet = AvatarPacketGenerator.generateRetrieveAvatarMetadataPacket(null);
+ XmppSendUtil.sendIqPacket(account, packet, new OnIqPacketReceived() {
+
+ private Avatar parseAvatar(IqPacket packet) {
+ Element pubsub = packet.findChild("pubsub", "http://jabber.org/protocol/pubsub");
+ if (pubsub != null) {
+ Element items = pubsub.findChild("items");
+ if (items != null) {
+ return Avatar.parseMetadata(items);
+ }
+ }
+ return null;
+ }
+
+ private boolean errorIsItemNotFound(IqPacket packet) {
+ Element error = packet.findChild("error");
+ return packet.getType() == IqPacket.TYPE.ERROR
+ && error != null
+ && error.hasChild("item-not-found");
+ }
+
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE.RESULT || errorIsItemNotFound(packet)) {
+ Avatar serverAvatar = parseAvatar(packet);
+ if (serverAvatar == null && account.getAvatar() != null) {
+ Avatar avatar = AvatarUtil.getStoredPepAvatar(account.getAvatar());
+ if (avatar != null) {
+ Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": avatar on server was null. republishing");
+ publishAvatar(account, AvatarUtil.getStoredPepAvatar(account.getAvatar()), null);
+ } else {
+ Logging.e(Config.LOGTAG, account.getJid().toBareJid()+": error rereading avatar");
+ }
+ }
+ }
+ }
+ });
+ }
+
+ public void fetchAvatar(Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
+ final String KEY = generateFetchKey(account, avatar);
+ synchronized(this.mInProgressAvatarFetches) {
+ if (this.mInProgressAvatarFetches.contains(KEY)) {
+ return;
+ } else {
+ switch (avatar.origin) {
+ case PEP:
+ this.mInProgressAvatarFetches.add(KEY);
+ fetchAvatarPep(account, avatar, callback);
+ break;
+ case VCARD:
+ this.mInProgressAvatarFetches.add(KEY);
+ fetchAvatarVcard(account, avatar, callback);
+ break;
+ }
+ }
+ }
+ }
+
+ private void fetchAvatarPep(final Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
+ IqPacket packet = AvatarPacketGenerator.generateRetrieveAvatarPacket(avatar);
+ XmppSendUtil.sendIqPacket(account, packet, new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket result) {
+ synchronized (mInProgressAvatarFetches) {
+ mInProgressAvatarFetches.remove(generateFetchKey(account, avatar));
+ }
+ final String ERROR = account.getJid().toBareJid()
+ + ": fetching avatar for " + avatar.owner + " failed ";
+ if (result.getType() == IqPacket.TYPE.RESULT) {
+ avatar.image = AvatarPacketParser.parseAvatarData(result);
+ if (avatar.image != null) {
+ if (AvatarUtil.save(avatar)) {
+ if (account.getJid().toBareJid().equals(avatar.owner)) {
+ if (account.setAvatar(avatar.getFilename())) {
+ DatabaseBackend.getInstance(ConversationsPlusApplication.getAppContext()).updateAccount(account);
+ }
+ AvatarService.this.clear(account);
+ UiUpdateHelper.updateConversationUi();
+ UiUpdateHelper.updateAccountUi();
+ } else {
+ Contact contact = account.getRoster()
+ .getContact(avatar.owner);
+ contact.setAvatar(avatar);
+ AvatarService.this.clear(contact);
+ UiUpdateHelper.updateConversationUi();
+ UiUpdateHelper.updateRosterUi();
+ }
+ if (callback != null) {
+ callback.success(avatar);
+ }
+ Logging.d(Config.LOGTAG, account.getJid().toBareJid()
+ + ": succesfuly fetched pep avatar for " + avatar.owner);
+ return;
+ }
+ } else {
+
+ Logging.d(Config.LOGTAG, ERROR + "(parsing error)");
+ }
+ } else {
+ Element error = result.findChild("error");
+ if (error == null) {
+ Logging.d(Config.LOGTAG, ERROR + "(server error)");
+ } else {
+ Logging.d(Config.LOGTAG, ERROR + error.toString());
+ }
+ }
+ if (callback != null) {
+ callback.error(0, null);
+ }
+
+ }
+ });
+ }
+
+ private void fetchAvatarVcard(final Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
+ IqPacket packet = IqGenerator.retrieveVcardAvatar(avatar);
+ XmppSendUtil.sendIqPacket(account, packet, new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ synchronized (mInProgressAvatarFetches) {
+ mInProgressAvatarFetches.remove(generateFetchKey(account, avatar));
+ }
+ if (packet.getType() == IqPacket.TYPE.RESULT) {
+ Element vCard = packet.findChild("vCard", "vcard-temp");
+ Element photo = vCard != null ? vCard.findChild("PHOTO") : null;
+ String image = photo != null ? photo.findChildContent("BINVAL") : null;
+ if (image != null) {
+ avatar.image = image;
+ if (AvatarUtil.save(avatar)) {
+ Logging.d(Config.LOGTAG, account.getJid().toBareJid()
+ + ": successfully fetched vCard avatar for " + avatar.owner);
+ Contact contact = account.getRoster()
+ .getContact(avatar.owner);
+ contact.setAvatar(avatar);
+ AvatarService.this.clear(contact);
+ UiUpdateHelper.updateConversationUi();
+ UiUpdateHelper.updateRosterUi();
+ }
+ }
+ }
+ }
+ });
+ }
+
+ public void checkForAvatar(Account account, final UiCallback<Avatar> callback) {
+ IqPacket packet = AvatarPacketGenerator.generateRetrieveAvatarMetadataPacket(null);
+ XmppSendUtil.sendIqPacket(account, packet, new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE.RESULT) {
+ Element pubsub = packet.findChild("pubsub",
+ "http://jabber.org/protocol/pubsub");
+ if (pubsub != null) {
+ Element items = pubsub.findChild("items");
+ if (items != null) {
+ Avatar avatar = Avatar.parseMetadata(items);
+ if (avatar != null) {
+ avatar.owner = account.getJid().toBareJid();
+ if (AvatarUtil.isAvatarCached(avatar)) {
+ if (account.setAvatar(avatar.getFilename())) {
+ DatabaseBackend.getInstance(ConversationsPlusApplication.getAppContext()).updateAccount(account);
+ }
+ AvatarService.this.clear(account);
+ callback.success(avatar);
+ } else {
+ fetchAvatarPep(account, avatar, callback);
+ }
+ return;
+ }
+ }
+ }
+ }
+ callback.error(0, null);
+ }
+ });
+ }
+
+ public void fetchAvatar(Account account, Avatar avatar) {
+ fetchAvatar(account, avatar, null);
+ }
+
+ public void clearFetchInProgress(Account account) {
+ synchronized (this.mInProgressAvatarFetches) {
+ for(Iterator<String> iterator = this.mInProgressAvatarFetches.iterator(); iterator.hasNext();) {
+ final String KEY = iterator.next();
+ if (KEY.startsWith(account.getJid().toBareJid()+"_")) {
+ iterator.remove();
+ }
+ }
+ }
+ }
+
+ private void sendAndReceiveIqPackages(final Avatar avatar, final Account account, final UiCallback callback) {
+
+ final IqPacket packet = AvatarPacketGenerator.generatePublishAvatarPacket(avatar);
+ XmppSendUtil.sendIqPacket(account, packet, new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket result) {
+ if (result.getType() == IqPacket.TYPE.RESULT) {
+ final IqPacket packet = AvatarPacketGenerator.generatePublishAvatarMetadataPacket(avatar);
+ XmppSendUtil.sendIqPacket(account, packet, new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account,
+ IqPacket result) {
+ if (result.getType() == IqPacket.TYPE.RESULT) {
+ if (account.setAvatar(avatar.getFilename())) {
+ AvatarService.getInstance().clear(account);
+ DatabaseBackend.getInstance(ConversationsPlusApplication.getAppContext()).updateAccount(account);
+ }
+ callback.success(avatar);
+ } else {
+ callback.error(
+ R.string.error_publish_avatar_server_reject,
+ avatar);
+ }
+ }
+ });
+ } else {
+ callback.error(
+ R.string.error_publish_avatar_server_reject,
+ avatar);
+ }
+ }
+ });
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/services/ContactChooserTargetService.java b/src/main/java/eu/siacs/conversations/services/ContactChooserTargetService.java
new file mode 100644
index 00000000..e59c03d9
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/services/ContactChooserTargetService.java
@@ -0,0 +1,83 @@
+package eu.siacs.conversations.services;
+
+import android.annotation.TargetApi;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.graphics.drawable.Icon;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.service.chooser.ChooserTarget;
+import android.service.chooser.ChooserTargetService;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.ui.ShareWithActivity;
+
+@TargetApi(Build.VERSION_CODES.M)
+public class ContactChooserTargetService extends ChooserTargetService implements ServiceConnection {
+
+ private final Object lock = new Object();
+
+ private XmppConnectionService mXmppConnectionService;
+
+ private final int MAX_TARGETS = 5;
+
+ @Override
+ public List<ChooserTarget> onGetChooserTargets(ComponentName targetActivityName, IntentFilter matchedFilter) {
+ Intent intent = new Intent(this, XmppConnectionService.class);
+ intent.setAction("contact_chooser");
+ startService(intent);
+ bindService(intent, this, Context.BIND_AUTO_CREATE);
+ ArrayList<ChooserTarget> chooserTargets = new ArrayList<>();
+ try {
+ waitForService();
+ final ArrayList<Conversation> conversations = new ArrayList<>();
+ if (!mXmppConnectionService.areMessagesInitialized()) {
+ return chooserTargets;
+ }
+ mXmppConnectionService.populateWithOrderedConversations(conversations, false);
+ final ComponentName componentName = new ComponentName(this, ShareWithActivity.class);
+ final int pixel = (int) (48 * getResources().getDisplayMetrics().density);
+ for(int i = 0; i < Math.min(conversations.size(),MAX_TARGETS); ++i) {
+ final Conversation conversation = conversations.get(i);
+ final String name = conversation.getName();
+ final Icon icon = Icon.createWithBitmap(AvatarService.getInstance().get(conversation, pixel));
+ final float score = 1 - (1.0f / MAX_TARGETS) * i;
+ final Bundle extras = new Bundle();
+ extras.putString("uuid", conversation.getUuid());
+ chooserTargets.add(new ChooserTarget(name, icon, score, componentName, extras));
+ }
+ } catch (InterruptedException e) {
+ }
+ unbindService(this);
+ return chooserTargets;
+ }
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ XmppConnectionService.XmppConnectionBinder binder = (XmppConnectionService.XmppConnectionBinder) service;
+ mXmppConnectionService = binder.getService();
+ synchronized (this.lock) {
+ lock.notifyAll();
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ mXmppConnectionService = null;
+ }
+
+ private void waitForService() throws InterruptedException {
+ if (mXmppConnectionService == null) {
+ synchronized (this.lock) {
+ lock.wait();
+ }
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/services/EventReceiver.java b/src/main/java/eu/siacs/conversations/services/EventReceiver.java
new file mode 100644
index 00000000..ceab1592
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/services/EventReceiver.java
@@ -0,0 +1,25 @@
+package eu.siacs.conversations.services;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+import eu.siacs.conversations.persistance.DatabaseBackend;
+
+public class EventReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Intent mIntentForService = new Intent(context,
+ XmppConnectionService.class);
+ if (intent.getAction() != null) {
+ mIntentForService.setAction(intent.getAction());
+ } else {
+ mIntentForService.setAction("other");
+ }
+ if (intent.getAction().equals("ui")
+ || DatabaseBackend.getInstance(context).hasEnabledAccounts()) {
+ context.startService(mIntentForService);
+ }
+ }
+
+}
diff --git a/src/main/java/eu/siacs/conversations/services/ExportLogsService.java b/src/main/java/eu/siacs/conversations/services/ExportLogsService.java
new file mode 100644
index 00000000..38c8e1d4
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/services/ExportLogsService.java
@@ -0,0 +1,147 @@
+package eu.siacs.conversations.services;
+
+import android.app.NotificationManager;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+import android.support.v4.app.NotificationCompat;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.persistance.DatabaseBackend;
+import eu.siacs.conversations.persistance.FileBackend;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+public class ExportLogsService extends Service {
+
+ private static final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
+ 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 int NOTIFICATION_ID = 1;
+ private static AtomicBoolean running = new AtomicBoolean(false);
+ private DatabaseBackend mDatabaseBackend;
+ private List<Account> mAccounts;
+
+ @Override
+ public void onCreate() {
+ mDatabaseBackend = DatabaseBackend.getInstance(getBaseContext());
+ mAccounts = mDatabaseBackend.getAccounts();
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (running.compareAndSet(false, true)) {
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ export();
+ stopForeground(true);
+ running.set(false);
+ stopSelf();
+ }
+ }).start();
+ }
+ return START_NOT_STICKY;
+ }
+
+ private void export() {
+ List<Conversation> conversations = mDatabaseBackend.getConversations(Conversation.STATUS_AVAILABLE);
+ conversations.addAll(mDatabaseBackend.getConversations(Conversation.STATUS_ARCHIVED));
+ NotificationManager mNotifyManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext());
+ mBuilder.setContentTitle(getString(R.string.notification_export_logs_title))
+ .setSmallIcon(R.drawable.ic_import_export_white_24dp)
+ .setProgress(conversations.size(), 0, false);
+ startForeground(NOTIFICATION_ID, mBuilder.build());
+
+ int progress = 0;
+ for (Conversation conversation : conversations) {
+ writeToFile(conversation);
+ progress++;
+ mBuilder.setProgress(conversations.size(), progress, false);
+ mNotifyManager.notify(NOTIFICATION_ID, mBuilder.build());
+ }
+ }
+
+ private void writeToFile(Conversation conversation) {
+ Jid accountJid = resolveAccountUuid(conversation.getAccountUuid());
+ Jid contactJid = conversation.getJid();
+
+ File dir = new File(String.format(DIRECTORY_STRING_FORMAT,accountJid.toBareJid().toString()));
+ dir.mkdirs();
+
+ BufferedWriter bw = null;
+ try {
+ for (Message message : mDatabaseBackend.getMessagesIterable(conversation)) {
+ if (message.getType() == Message.TYPE_TEXT || message.hasFileOnRemoteHost()) {
+ String date = simpleDateFormat.format(new Date(message.getTimeSent()));
+ if (bw == null) {
+ bw = new BufferedWriter(new FileWriter(
+ new File(dir, contactJid.toBareJid().toString() + ".txt")));
+ }
+ String jid = null;
+ switch (message.getStatus()) {
+ case Message.STATUS_RECEIVED:
+ jid = getMessageCounterpart(message);
+ break;
+ case Message.STATUS_SEND:
+ case Message.STATUS_SEND_RECEIVED:
+ case Message.STATUS_SEND_DISPLAYED:
+ jid = accountJid.toBareJid().toString();
+ break;
+ }
+ if (jid != null) {
+ String body = message.hasFileOnRemoteHost() ? message.getFileParams().getUrl() : message.getBody();
+ bw.write(String.format(MESSAGE_STRING_FORMAT, date, jid,
+ body.replace("\\\n", "\\ \n").replace("\n", "\\ \n")));
+ }
+ }
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ try {
+ if (bw != null) {
+ bw.close();
+ }
+ } catch (IOException e1) {
+ e1.printStackTrace();
+ }
+ }
+ }
+
+ private Jid resolveAccountUuid(String accountUuid) {
+ for (Account account : mAccounts) {
+ if (account.getUuid().equals(accountUuid)) {
+ return account.getJid();
+ }
+ }
+ return null;
+ }
+
+ private String getMessageCounterpart(Message message) {
+ String trueCounterpart = (String) message.getContentValues().get(Message.TRUE_COUNTERPART);
+ if (trueCounterpart != null) {
+ return trueCounterpart;
+ } else {
+ return message.getCounterpart().toString();
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+} \ No newline at end of file
diff --git a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java
new file mode 100644
index 00000000..e76eb16b
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java
@@ -0,0 +1,422 @@
+package eu.siacs.conversations.services;
+
+import android.util.Pair;
+
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+
+import de.thedevstack.android.logcat.Logging;
+import de.thedevstack.conversationsplus.ConversationsPlusApplication;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.generator.AbstractGenerator;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded;
+import eu.siacs.conversations.xmpp.OnIqPacketReceived;
+import eu.siacs.conversations.xmpp.jid.Jid;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
+
+ private final XmppConnectionService mXmppConnectionService;
+
+ private final HashSet<Query> queries = new HashSet<>();
+ private final ArrayList<Query> pendingQueries = new ArrayList<>();
+
+ public enum PagingOrder {
+ NORMAL,
+ REVERSE
+ };
+
+ public MessageArchiveService(final XmppConnectionService service) {
+ this.mXmppConnectionService = service;
+ }
+
+ private void catchup(final Account account) {
+ synchronized (this.queries) {
+ for(Iterator<Query> iterator = this.queries.iterator(); iterator.hasNext();) {
+ Query query = iterator.next();
+ if (query.getAccount() == account) {
+ iterator.remove();
+ }
+ }
+ }
+ Pair<Long,String> pair = mXmppConnectionService.databaseBackend.getLastMessageReceived(account);
+ long startCatchup = pair == null ? 0 : pair.first;
+ long endCatchup = account.getXmppConnection().getLastSessionEstablished();
+ final Query query;
+ if (startCatchup == 0) {
+ return;
+ } else if (endCatchup - startCatchup >= Config.MAM_MAX_CATCHUP) {
+ startCatchup = endCatchup - Config.MAM_MAX_CATCHUP;
+ List<Conversation> conversations = mXmppConnectionService.getConversations();
+ for (Conversation conversation : conversations) {
+ if (conversation.getMode() == Conversation.MODE_SINGLE && conversation.getAccount() == account && startCatchup > conversation.getLastMessageTransmitted()) {
+ this.query(conversation,startCatchup);
+ }
+ }
+ query = new Query(account, startCatchup, endCatchup);
+ } else {
+ query = new Query(account, startCatchup, endCatchup);
+ query.reference = pair.second;
+ }
+ this.queries.add(query);
+ this.execute(query);
+ }
+
+ public void catchupMUC(final Conversation conversation) {
+ if (conversation.getLastMessageTransmitted() < 0 && conversation.countMessages() == 0) {
+ query(conversation,
+ 0,
+ System.currentTimeMillis());
+ } else {
+ query(conversation,
+ conversation.getLastMessageTransmitted(),
+ System.currentTimeMillis());
+ }
+ }
+
+ public Query query(final Conversation conversation) {
+ if (conversation.getLastMessageTransmitted() < 0 && conversation.countMessages() == 0) {
+ return query(conversation,
+ 0,
+ System.currentTimeMillis());
+ } else {
+ return query(conversation,
+ conversation.getLastMessageTransmitted(),
+ conversation.getAccount().getXmppConnection().getLastSessionEstablished());
+ }
+ }
+
+ public Query query(final Conversation conversation, long end) {
+ return this.query(conversation,conversation.getLastMessageTransmitted(),end);
+ }
+
+ public Query query(Conversation conversation, long start, long end) {
+ return this.query(conversation, start, end, null);
+ }
+
+ public Query query(Conversation conversation, long start, long end, XmppConnectionService.OnMoreMessagesLoaded callback) {
+ synchronized (this.queries) {
+ if (start > end) {
+ return null;
+ }
+ final Query query = new Query(conversation, start, end,PagingOrder.REVERSE);
+ query.reference = conversation.getFirstMamReference();
+ this.queries.add(query);
+ if (null != callback) {
+ query.setCallback(callback);
+ }
+ this.execute(query);
+ return query;
+ }
+ }
+
+ public void executePendingQueries(final Account account) {
+ List<Query> pending = new ArrayList<>();
+ synchronized(this.pendingQueries) {
+ for(Iterator<Query> iterator = this.pendingQueries.iterator(); iterator.hasNext();) {
+ Query query = iterator.next();
+ if (query.getAccount() == account) {
+ pending.add(query);
+ iterator.remove();
+ }
+ }
+ }
+ for(Query query : pending) {
+ this.execute(query);
+ }
+ }
+
+ private void execute(final Query query) {
+ final Account account= query.getAccount();
+ if (account.getStatus() == Account.State.ONLINE) {
+ Logging.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": running mam query " + query.toString());
+ IqPacket packet = this.mXmppConnectionService.getIqGenerator().queryMessageArchiveManagement(query);
+ this.mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE.TIMEOUT) {
+ synchronized (MessageArchiveService.this.queries) {
+ MessageArchiveService.this.queries.remove(query);
+ if (query.hasCallback()) {
+ query.callback();
+ }
+ }
+ } else if (packet.getType() != IqPacket.TYPE.RESULT) {
+ Logging.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": error executing mam: " + packet.toString());
+ finalizeQuery(query, true);
+ }
+ }
+ });
+ } else {
+ synchronized (this.pendingQueries) {
+ this.pendingQueries.add(query);
+ }
+ }
+ }
+
+ private void finalizeQuery(Query query, boolean done) {
+ synchronized (this.queries) {
+ this.queries.remove(query);
+ }
+ final Conversation conversation = query.getConversation();
+ if (conversation != null) {
+ conversation.sort();
+ conversation.setHasMessagesLeftOnServer(!done);
+ } else {
+ for(Conversation tmp : this.mXmppConnectionService.getConversations()) {
+ if (tmp.getAccount() == query.getAccount()) {
+ tmp.sort();
+ }
+ }
+ }
+ if (query.hasCallback()) {
+ query.callback();
+ } else {
+ if (null != conversation) {
+ conversation.setHasMessagesLeftOnServer(query.getMessageCount() > 0);
+ }
+ this.mXmppConnectionService.updateConversationUi();
+ }
+ }
+
+ public boolean queryInProgress(Conversation conversation, XmppConnectionService.OnMoreMessagesLoaded callback) {
+ synchronized (this.queries) {
+ for(Query query : queries) {
+ if (query.conversation == conversation) {
+ if (!query.hasCallback() && callback != null) {
+ query.setCallback(callback);
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
+ public boolean queryInProgress(Conversation conversation) {
+ return queryInProgress(conversation, null);
+ }
+
+ public void processFin(Element fin, Jid from) {
+ if (fin == null) {
+ return;
+ }
+ Query query = findQuery(fin.getAttribute("queryid"));
+ if (query == null || !query.validFrom(from)) {
+ return;
+ }
+ boolean complete = fin.getAttributeAsBoolean("complete");
+ Element set = fin.findChild("set","http://jabber.org/protocol/rsm");
+ Element last = set == null ? null : set.findChild("last");
+ Element first = set == null ? null : set.findChild("first");
+ Element relevant = query.getPagingOrder() == PagingOrder.NORMAL ? last : first;
+ boolean abort = (query.getStart() == 0 && query.getTotalCount() >= Config.PAGE_SIZE) || query.getTotalCount() >= Config.MAM_MAX_MESSAGES;
+ if (query.getConversation() != null) {
+ query.getConversation().setFirstMamReference(first == null ? null : first.getContent());
+ }
+ if (complete || relevant == null || abort) {
+ final boolean done = (complete || query.getMessageCount() == 0) && query.getStart() == 0;
+ this.finalizeQuery(query, done);
+ Logging.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) {
+ mXmppConnectionService.getNotificationService().finishBacklog(true);
+ }
+ } else {
+ final Query nextQuery;
+ if (query.getPagingOrder() == PagingOrder.NORMAL) {
+ nextQuery = query.next(last == null ? null : last.getContent());
+ } else {
+ nextQuery = query.prev(first == null ? null : first.getContent());
+ }
+ this.execute(nextQuery);
+ this.finalizeQuery(query, false);
+ synchronized (this.queries) {
+ this.queries.add(nextQuery);
+ }
+ }
+ }
+
+ public Query findQuery(String id) {
+ if (id == null) {
+ return null;
+ }
+ synchronized (this.queries) {
+ for(Query query : this.queries) {
+ if (query.getQueryId().equals(id)) {
+ return query;
+ }
+ }
+ return null;
+ }
+ }
+
+ @Override
+ public void onAdvancedStreamFeaturesAvailable(Account account) {
+ if (account.getXmppConnection() != null && account.getXmppConnection().getFeatures().mam()) {
+ this.catchup(account);
+ }
+ }
+
+ public class Query {
+ private int totalCount = 0;
+ private int messageCount = 0;
+ private long start;
+ private long end;
+ private String queryId;
+ private String reference = null;
+ private Account account;
+ private Conversation conversation;
+ private PagingOrder pagingOrder = PagingOrder.NORMAL;
+ private XmppConnectionService.OnMoreMessagesLoaded callback = null;
+
+
+ public Query(Conversation conversation, long start, long end) {
+ this(conversation.getAccount(), start, end);
+ this.conversation = conversation;
+ }
+
+ public Query(Conversation conversation, long start, long end, PagingOrder order) {
+ this(conversation,start,end);
+ this.pagingOrder = order;
+ }
+
+ public Query(Account account, long start, long end) {
+ this.account = account;
+ this.start = start;
+ this.end = end;
+ this.queryId = new BigInteger(50, ConversationsPlusApplication.getSecureRandom()).toString(32);
+ }
+
+ private Query page(String reference) {
+ Query query = new Query(this.account,this.start,this.end);
+ query.reference = reference;
+ query.conversation = conversation;
+ query.totalCount = totalCount;
+ query.callback = callback;
+ return query;
+ }
+
+ public Query next(String reference) {
+ Query query = page(reference);
+ query.pagingOrder = PagingOrder.NORMAL;
+ return query;
+ }
+
+ public Query prev(String reference) {
+ Query query = page(reference);
+ query.pagingOrder = PagingOrder.REVERSE;
+ return query;
+ }
+
+ public String getReference() {
+ return reference;
+ }
+
+ public PagingOrder getPagingOrder() {
+ return this.pagingOrder;
+ }
+
+ public String getQueryId() {
+ return queryId;
+ }
+
+ public Jid getWith() {
+ return conversation == null ? null : conversation.getJid().toBareJid();
+ }
+
+ public boolean muc() {
+ return conversation != null && conversation.getMode() == Conversation.MODE_MULTI;
+ }
+
+ public long getStart() {
+ return start;
+ }
+
+ public void setCallback(XmppConnectionService.OnMoreMessagesLoaded callback) {
+ this.callback = callback;
+ }
+
+ public void callback() {
+ if (this.callback != null) {
+ this.callback.onMoreMessagesLoaded(messageCount,conversation);
+ if (messageCount <= 0) {
+ this.callback.informUser(R.string.no_more_history_on_server);
+ }
+ }
+ }
+
+ public long getEnd() {
+ return end;
+ }
+
+ public Conversation getConversation() {
+ return conversation;
+ }
+
+ public Account getAccount() {
+ return this.account;
+ }
+
+ public void incrementMessageCount() {
+ this.messageCount++;
+ this.totalCount++;
+ }
+
+ public int getTotalCount() {
+ return this.totalCount;
+ }
+
+ public int getMessageCount() {
+ return this.messageCount;
+ }
+
+ public boolean validFrom(Jid from) {
+ if (muc()) {
+ return getWith().equals(from);
+ } else {
+ return (from == null) || account.getJid().toBareJid().equals(from.toBareJid());
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ if (this.muc()) {
+ builder.append("to=");
+ builder.append(this.getWith().toString());
+ } else {
+ builder.append("with=");
+ if (this.getWith() == null) {
+ builder.append("*");
+ } else {
+ builder.append(getWith().toString());
+ }
+ }
+ builder.append(", start=");
+ builder.append(AbstractGenerator.getTimestamp(this.start));
+ builder.append(", end=");
+ builder.append(AbstractGenerator.getTimestamp(this.end));
+ if (this.reference!=null) {
+ if (this.pagingOrder == PagingOrder.NORMAL) {
+ builder.append(", after=");
+ } else {
+ builder.append(", before=");
+ }
+ builder.append(this.reference);
+ }
+ return builder.toString();
+ }
+
+ public boolean hasCallback() {
+ return this.callback != null;
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java
new file mode 100644
index 00000000..d11661fa
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java
@@ -0,0 +1,584 @@
+package eu.siacs.conversations.services;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Build;
+import android.os.SystemClock;
+import android.support.v4.app.NotificationCompat;
+import android.support.v4.app.NotificationCompat.BigPictureStyle;
+import android.support.v4.app.NotificationCompat.Builder;
+import android.support.v4.app.TaskStackBuilder;
+import android.text.Html;
+import android.util.DisplayMetrics;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.io.FileNotFoundException;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+
+import de.thedevstack.conversationsplus.ConversationsPlusApplication;
+import de.thedevstack.conversationsplus.ConversationsPlusColors;
+import de.thedevstack.conversationsplus.ConversationsPlusPreferences;
+import de.thedevstack.conversationsplus.utils.ImageUtil;
+import de.thedevstack.conversationsplus.utils.MessageUtil;
+import de.tzur.conversations.Settings;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.ui.ConversationActivity;
+import eu.siacs.conversations.ui.ManageAccountActivity;
+import eu.siacs.conversations.utils.GeoHelper;
+import eu.siacs.conversations.utils.UIHelper;
+
+public class NotificationService {
+
+ private final XmppConnectionService mXmppConnectionService;
+
+ private final LinkedHashMap<String, ArrayList<Message>> notifications = new LinkedHashMap<>();
+
+ public static final int NOTIFICATION_ID = 0x2342;
+ public static final int FOREGROUND_NOTIFICATION_ID = 0x8899;
+ public static final int ERROR_NOTIFICATION_ID = 0x5678;
+
+ private Conversation mOpenConversation;
+ private boolean mIsInForeground;
+ private long mLastNotification;
+
+ public NotificationService(final XmppConnectionService service) {
+ this.mXmppConnectionService = service;
+ }
+
+ public boolean notify(final Message message) {
+ return (message.getStatus() == Message.STATUS_RECEIVED)
+ && !message.isRead()
+ && ConversationsPlusPreferences.showNotification()
+ && !message.getConversation().isMuted()
+ && (message.getConversation().alwaysNotify() || wasHighlightedOrPrivate(message)
+ );
+ }
+
+ public void notifyPebble(final Message message) {
+ final Intent i = new Intent("com.getpebble.action.SEND_NOTIFICATION");
+
+ final Conversation conversation = message.getConversation();
+ final JSONObject jsonData = new JSONObject(new HashMap<String, String>(2) {{
+ put("title", conversation.getName());
+ put("body", message.getBody());
+ }});
+ final String notificationData = new JSONArray().put(jsonData).toString();
+
+ i.putExtra("messageType", "PEBBLE_ALERT");
+ i.putExtra("sender", ConversationsPlusApplication.getName());
+ i.putExtra("notificationData", notificationData);
+ // notify Pebble App
+ i.setPackage("com.getpebble.android");
+ mXmppConnectionService.sendBroadcast(i);
+ // notify Gadgetbridge
+ i.setPackage("nodomain.freeyourgadget.gadgetbridge");
+ mXmppConnectionService.sendBroadcast(i);
+ }
+
+ public boolean isQuietHours() {
+ if (!ConversationsPlusPreferences.enableQuietHours()) {
+ return false;
+ }
+ final long startTime = ConversationsPlusPreferences.quietHoursStart() % Config.MILLISECONDS_IN_DAY;
+ final long endTime = ConversationsPlusPreferences.quietHoursEnd() % Config.MILLISECONDS_IN_DAY;
+ final long nowTime = Calendar.getInstance().getTimeInMillis() % Config.MILLISECONDS_IN_DAY;
+
+ if (endTime < startTime) {
+ return nowTime > startTime || nowTime < endTime;
+ } else {
+ return nowTime > startTime && nowTime < endTime;
+ }
+ }
+
+ public void pushFromBacklog(final Message message) {
+ if (notify(message)) {
+ synchronized (notifications) {
+ pushToStack(message);
+ }
+ }
+ }
+
+ public void finishBacklog(boolean notify) {
+ synchronized (notifications) {
+ mXmppConnectionService.updateUnreadCountBadge();
+ updateNotification(notify);
+ }
+ }
+
+ private void pushToStack(final Message message) {
+ final String conversationUuid = message.getConversationUuid();
+ if (notifications.containsKey(conversationUuid)) {
+ notifications.get(conversationUuid).add(message);
+ } else {
+ final ArrayList<Message> mList = new ArrayList<>();
+ mList.add(message);
+ notifications.put(conversationUuid, mList);
+ }
+ }
+
+ public void push(final Message message) {
+ if (!notify(message)) {
+ return;
+ }
+ mXmppConnectionService.updateUnreadCountBadge();
+ final boolean isScreenOn = mXmppConnectionService.isInteractive();
+ if (this.mIsInForeground && isScreenOn && this.mOpenConversation == message.getConversation()) {
+ return;
+ }
+ synchronized (notifications) {
+ pushToStack(message);
+ final Account account = message.getConversation().getAccount();
+ final boolean doNotify = (!(this.mIsInForeground && this.mOpenConversation == null) || !isScreenOn)
+ && !account.inGracePeriod()
+ && !this.inMiniGracePeriod(account);
+ updateNotification(doNotify);
+ if (doNotify) {
+ notifyPebble(message);
+ }
+ }
+ }
+
+ public void clear() {
+ synchronized (notifications) {
+ notifications.clear();
+ updateNotification(false);
+ }
+ }
+
+ public void clear(final Conversation conversation) {
+ synchronized (notifications) {
+ notifications.remove(conversation.getUuid());
+ updateNotification(false);
+ }
+ }
+
+ private void setNotificationColor(final Builder mBuilder) {
+ mBuilder.setColor(ConversationsPlusColors.notification());
+ }
+
+ public void updateNotification(final boolean notify) {
+ final NotificationManager notificationManager = (NotificationManager) ConversationsPlusApplication.getAppContext().getSystemService(Context.NOTIFICATION_SERVICE);
+
+ final String ringtone = ConversationsPlusPreferences.notificationRingtone();
+ final boolean vibrate = ConversationsPlusPreferences.vibrateOnNotification();
+ final boolean led = ConversationsPlusPreferences.led();
+
+ if (notifications.size() == 0) {
+ notificationManager.cancel(NOTIFICATION_ID);
+ } else {
+ if (notify) {
+ this.markLastNotification();
+ }
+ final Builder mBuilder;
+ if (notifications.size() == 1) {
+ mBuilder = buildSingleConversations(notify);
+ } else {
+ mBuilder = buildMultipleConversation();
+ }
+ if (notify && !isQuietHours()) {
+ if (vibrate) {
+ final int dat = 70;
+ final long[] pattern = {0, 3 * dat, dat, dat};
+ mBuilder.setVibrate(pattern);
+ }
+ if (ringtone != null) {
+ mBuilder.setSound(Uri.parse(ringtone));
+ }
+ }
+ if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ mBuilder.setCategory(Notification.CATEGORY_MESSAGE);
+ }
+ setNotificationColor(mBuilder);
+ mBuilder.setDefaults(0);
+ mBuilder.setSmallIcon(R.drawable.ic_notification);
+ mBuilder.setDeleteIntent(createDeleteIntent());
+ if (led) {
+ mBuilder.setLights(Settings.LED_COLOR, 2000, 4000);
+ }
+ final Notification notification = mBuilder.build();
+ notificationManager.notify(NOTIFICATION_ID, notification);
+ }
+ }
+
+ private Builder buildMultipleConversation() {
+ final Builder mBuilder = new NotificationCompat.Builder(
+ mXmppConnectionService);
+ final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
+ style.setBigContentTitle(notifications.size()
+ + " "
+ + mXmppConnectionService
+ .getString(R.string.unread_conversations));
+ final StringBuilder names = new StringBuilder();
+ Conversation conversation = null;
+ for (final ArrayList<Message> messages : notifications.values()) {
+ if (messages.size() > 0) {
+ conversation = messages.get(0).getConversation();
+ final String name = conversation.getName();
+ if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) {
+ int count = messages.size();
+ style.addLine(Html.fromHtml("<b>"+name+"</b>: "+mXmppConnectionService.getResources().getQuantityString(R.plurals.x_messages,count,count)));
+ } else {
+ style.addLine(Html.fromHtml("<b>" + name + "</b>: "
+ + UIHelper.getMessagePreview(mXmppConnectionService, messages.get(0)).first));
+ }
+ names.append(name);
+ names.append(", ");
+ }
+ }
+ if (names.length() >= 2) {
+ names.delete(names.length() - 2, names.length());
+ }
+ mBuilder.setContentTitle(notifications.size()
+ + " "
+ + mXmppConnectionService
+ .getString(R.string.unread_conversations));
+ mBuilder.setContentText(names.toString());
+ mBuilder.setStyle(style);
+ if (conversation != null) {
+ mBuilder.setContentIntent(createContentIntent(conversation));
+ }
+ return mBuilder;
+ }
+
+ private Builder buildSingleConversations(final boolean notify) {
+ final Builder mBuilder = new NotificationCompat.Builder(
+ mXmppConnectionService);
+ final ArrayList<Message> messages = notifications.values().iterator().next();
+ if (messages.size() >= 1) {
+ final Conversation conversation = messages.get(0).getConversation();
+ mBuilder.setLargeIcon(AvatarService.getInstance().get(conversation, getPixel(64)));
+ mBuilder.setContentTitle(conversation.getName());
+ if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) {
+ int count = messages.size();
+ mBuilder.setContentText(mXmppConnectionService.getResources().getQuantityString(R.plurals.x_messages,count,count));
+ } else {
+ Message message;
+ if ((message = getImage(messages)) != null) {
+ modifyForImage(mBuilder, message, messages, notify);
+ } else if (conversation.getMode() == Conversation.MODE_MULTI) {
+ modifyForConference(mBuilder, conversation, messages, notify);
+ } else {
+ modifyForTextOnly(mBuilder, messages, notify);
+ }
+ if ((message = getFirstDownloadableMessage(messages)) != null) {
+ mBuilder.addAction(
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ?
+ R.drawable.ic_file_download_white_24dp : R.drawable.ic_action_download,
+ mXmppConnectionService.getResources().getString(R.string.download_x_file,
+ UIHelper.getFileDescriptionString(mXmppConnectionService, message)),
+ createDownloadIntent(message)
+ );
+ }
+ if ((message = getFirstLocationMessage(messages)) != null) {
+ mBuilder.addAction(R.drawable.ic_room_white_24dp,
+ mXmppConnectionService.getString(R.string.show_location),
+ createShowLocationIntent(message));
+ }
+ }
+ mBuilder.setContentIntent(createContentIntent(conversation));
+ }
+ return mBuilder;
+ }
+
+ private void modifyForImage(final Builder builder, final Message message,
+ final ArrayList<Message> messages, final boolean notify) {
+ try {
+ final Bitmap bitmap = ImageUtil.getThumbnail(message, getPixel(288), false);
+ final ArrayList<Message> tmp = new ArrayList<>();
+ for (final Message msg : messages) {
+ if (msg.getType() == Message.TYPE_TEXT
+ && msg.getTransferable() == null) {
+ tmp.add(msg);
+ }
+ }
+ final BigPictureStyle bigPictureStyle = new NotificationCompat.BigPictureStyle();
+ bigPictureStyle.bigPicture(bitmap);
+ if (tmp.size() > 0) {
+ bigPictureStyle.setSummaryText(getMergedBodies(tmp));
+ builder.setContentText(UIHelper.getMessagePreview(mXmppConnectionService, tmp.get(0)).first);
+ } else {
+ builder.setContentText(mXmppConnectionService.getString(
+ R.string.received_x_file,
+ UIHelper.getFileDescriptionString(mXmppConnectionService, message)));
+ }
+ builder.setStyle(bigPictureStyle);
+ } catch (final FileNotFoundException e) {
+ modifyForTextOnly(builder, messages, notify);
+ }
+ }
+
+ private void modifyForTextOnly(final Builder builder,
+ final ArrayList<Message> messages, final boolean notify) {
+ builder.setStyle(new NotificationCompat.BigTextStyle().bigText(getMergedBodies(messages)));
+ builder.setContentText(UIHelper.getMessagePreview(mXmppConnectionService, messages.get(0)).first);
+ if (notify) {
+ builder.setTicker(UIHelper.getMessagePreview(mXmppConnectionService, messages.get(messages.size() - 1)).first);
+ }
+ }
+
+ private void modifyForConference(Builder builder, Conversation conversation, List<Message> messages, boolean notify) {
+ final Message first = messages.get(0);
+ final Message last = messages.get(messages.size() - 1);
+ final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
+ style.setBigContentTitle(conversation.getName());
+
+ for(Message message : messages) {
+ if (message.hasMeCommand()) {
+ style.addLine(UIHelper.getMessagePreview(mXmppConnectionService,message).first);
+ } else {
+ style.addLine(Html.fromHtml("<b>" + UIHelper.getMessageDisplayName(message) + "</b>: " + UIHelper.getMessagePreview(mXmppConnectionService, message).first));
+ }
+ }
+ builder.setContentText((first.hasMeCommand() ? "" :UIHelper.getMessageDisplayName(first)+ ": ") +UIHelper.getMessagePreview(mXmppConnectionService, first).first);
+ builder.setStyle(style);
+ if (notify) {
+ builder.setTicker((last.hasMeCommand() ? "" : UIHelper.getMessageDisplayName(last) + ": ") + UIHelper.getMessagePreview(mXmppConnectionService,last).first);
+ }
+ }
+
+ private Message getImage(final Iterable<Message> messages) {
+ for (final Message message : messages) {
+ if (message.getType() != Message.TYPE_TEXT
+ && message.getTransferable() == null
+ && message.getEncryption() != Message.ENCRYPTION_PGP
+ && message.getFileParams() != null
+ && message.getFileParams().getHeight() > 0) { // TODO Use FileParams.getMimeType()
+ return message;
+ }
+ }
+ return null;
+ }
+
+ private Message getFirstDownloadableMessage(final Iterable<Message> messages) {
+ for (final Message message : messages) {
+ if ((message.getType() == Message.TYPE_FILE || message.getType() == Message.TYPE_IMAGE) &&
+ message.getTransferable() != null) {
+ return message;
+ }
+ }
+ return null;
+ }
+
+ private Message getFirstLocationMessage(final Iterable<Message> messages) {
+ for (final Message message : messages) {
+ if (GeoHelper.isGeoUri(message.getBody())) {
+ return message;
+ }
+ }
+ return null;
+ }
+
+ private CharSequence getMergedBodies(final ArrayList<Message> messages) {
+ final StringBuilder text = new StringBuilder();
+ for (int i = 0; i < messages.size(); ++i) {
+ text.append(UIHelper.getMessagePreview(mXmppConnectionService, messages.get(i)).first);
+ if (i != messages.size() - 1) {
+ text.append("\n");
+ }
+ }
+ return text.toString();
+ }
+
+ private PendingIntent createShowLocationIntent(final Message message) {
+ Iterable<Intent> intents = GeoHelper.createGeoIntentsFromMessage(message);
+ for (Intent intent : intents) {
+ if (intent.resolveActivity(mXmppConnectionService.getPackageManager()) != null) {
+ return PendingIntent.getActivity(mXmppConnectionService, 18, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+ }
+ }
+ return createOpenConversationsIntent();
+ }
+
+ private PendingIntent createContentIntent(final String conversationUuid, final String downloadMessageUuid) {
+ final TaskStackBuilder stackBuilder = TaskStackBuilder
+ .create(mXmppConnectionService);
+ stackBuilder.addParentStack(ConversationActivity.class);
+
+ final Intent viewConversationIntent = new Intent(mXmppConnectionService,
+ ConversationActivity.class);
+ if (downloadMessageUuid != null) {
+ viewConversationIntent.setAction(ConversationActivity.ACTION_DOWNLOAD);
+ } else {
+ viewConversationIntent.setAction(Intent.ACTION_VIEW);
+ }
+ if (conversationUuid != null) {
+ viewConversationIntent.putExtra(ConversationActivity.CONVERSATION, conversationUuid);
+ viewConversationIntent.setType(ConversationActivity.VIEW_CONVERSATION);
+ }
+ if (downloadMessageUuid != null) {
+ viewConversationIntent.putExtra(ConversationActivity.MESSAGE, downloadMessageUuid);
+ }
+
+ stackBuilder.addNextIntent(viewConversationIntent);
+
+ return stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
+ }
+
+ private PendingIntent createDownloadIntent(final Message message) {
+ return createContentIntent(message.getConversationUuid(), message.getUuid());
+ }
+
+ private PendingIntent createContentIntent(final Conversation conversation) {
+ return createContentIntent(conversation.getUuid(), null);
+ }
+
+ private PendingIntent createDeleteIntent() {
+ final Intent intent = new Intent(mXmppConnectionService,
+ XmppConnectionService.class);
+ intent.setAction(XmppConnectionService.ACTION_CLEAR_NOTIFICATION);
+ return PendingIntent.getService(mXmppConnectionService, 0, intent, 0);
+ }
+
+ private PendingIntent createDisableForeground() {
+ final Intent intent = new Intent(mXmppConnectionService,
+ XmppConnectionService.class);
+ intent.setAction(XmppConnectionService.ACTION_DISABLE_FOREGROUND);
+ return PendingIntent.getService(mXmppConnectionService, 34, intent, 0);
+ }
+
+ private PendingIntent createTryAgainIntent() {
+ final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
+ intent.setAction(XmppConnectionService.ACTION_TRY_AGAIN);
+ return PendingIntent.getService(mXmppConnectionService, 45, intent, 0);
+ }
+
+ private PendingIntent createDisableAccountIntent(final Account account) {
+ final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
+ intent.setAction(XmppConnectionService.ACTION_DISABLE_ACCOUNT);
+ intent.putExtra("account", account.getJid().toBareJid().toString());
+ return PendingIntent.getService(mXmppConnectionService, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+ }
+
+ private boolean wasHighlightedOrPrivate(final Message message) {
+ return MessageUtil.wasHighlightedOrPrivate(message);
+ }
+
+ public void setOpenConversation(final Conversation conversation) {
+ this.mOpenConversation = conversation;
+ }
+
+ public void setIsInForeground(final boolean foreground) {
+ this.mIsInForeground = foreground;
+ }
+
+ private int getPixel(final int dp) {
+ final DisplayMetrics metrics = mXmppConnectionService.getResources()
+ .getDisplayMetrics();
+ return ((int) (dp * metrics.density));
+ }
+
+ private void markLastNotification() {
+ this.mLastNotification = SystemClock.elapsedRealtime();
+ }
+
+ private boolean inMiniGracePeriod(final Account account) {
+ final int miniGrace = account.getStatus() == Account.State.ONLINE ? Config.MINI_GRACE_PERIOD
+ : Config.MINI_GRACE_PERIOD * 2;
+ return SystemClock.elapsedRealtime() < (this.mLastNotification + miniGrace);
+ }
+
+ public Notification createForegroundNotification() {
+ final NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService);
+
+ mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.conversations_foreground_service));
+ if (Config.SHOW_CONNECTED_ACCOUNTS) {
+ List<Account> accounts = mXmppConnectionService.getAccounts();
+ int enabled = 0;
+ int connected = 0;
+ for (Account account : accounts) {
+ if (account.isOnlineAndConnected()) {
+ connected++;
+ enabled++;
+ } else if (!account.isOptionSet(Account.OPTION_DISABLED)) {
+ enabled++;
+ }
+ }
+ mBuilder.setContentText(mXmppConnectionService.getString(R.string.connected_accounts, connected, enabled));
+ } else {
+ mBuilder.setContentText(mXmppConnectionService.getString(R.string.touch_to_open_conversations));
+ }
+ mBuilder.setContentIntent(createOpenConversationsIntent());
+ mBuilder.setWhen(0);
+ mBuilder.setPriority(Config.SHOW_CONNECTED_ACCOUNTS ? NotificationCompat.PRIORITY_DEFAULT : NotificationCompat.PRIORITY_MIN);
+ final int cancelIcon;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ mBuilder.setCategory(Notification.CATEGORY_SERVICE);
+ cancelIcon = R.drawable.ic_cancel_white_24dp;
+ } else {
+ cancelIcon = R.drawable.ic_action_cancel;
+ }
+ mBuilder.setSmallIcon(R.drawable.ic_link_white_24dp);
+ mBuilder.addAction(cancelIcon,
+ mXmppConnectionService.getString(R.string.disable_foreground_service),
+ createDisableForeground());
+ return mBuilder.build();
+ }
+
+ private PendingIntent createOpenConversationsIntent() {
+ return PendingIntent.getActivity(mXmppConnectionService, 0, new Intent(mXmppConnectionService, ConversationActivity.class), 0);
+ }
+
+ public void updateErrorNotification() {
+ final NotificationManager notificationManager = (NotificationManager) mXmppConnectionService.getSystemService(Context.NOTIFICATION_SERVICE);
+ final List<Account> errors = new ArrayList<>();
+ for (final Account account : mXmppConnectionService.getAccounts()) {
+ if (account.hasErrorStatus()) {
+ errors.add(account);
+ }
+ }
+ if (ConversationsPlusPreferences.keepForegroundService()) {
+ notificationManager.notify(FOREGROUND_NOTIFICATION_ID, createForegroundNotification());
+ }
+ final NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService);
+ if (errors.size() == 0) {
+ notificationManager.cancel(ERROR_NOTIFICATION_ID);
+ return;
+ } else if (errors.size() == 1) {
+ mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.problem_connecting_to_account));
+ mBuilder.setContentText(errors.get(0).getJid().toBareJid().toString());
+ } else {
+ mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.problem_connecting_to_accounts));
+ mBuilder.setContentText(mXmppConnectionService.getString(R.string.touch_to_fix));
+ }
+ mBuilder.addAction(R.drawable.ic_autorenew_white_24dp,
+ mXmppConnectionService.getString(R.string.try_again),
+ createTryAgainIntent());
+ if (errors.size() == 1) {
+ mBuilder.addAction(R.drawable.ic_block_white_24dp,
+ mXmppConnectionService.getString(R.string.disable_account),
+ createDisableAccountIntent(errors.get(0)));
+ }
+ mBuilder.setOngoing(true);
+ //mBuilder.setLights(0xffffffff, 2000, 4000);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ mBuilder.setSmallIcon(R.drawable.ic_warning_white_24dp);
+ } else {
+ mBuilder.setSmallIcon(R.drawable.ic_stat_alert_warning);
+ }
+ final TaskStackBuilder stackBuilder = TaskStackBuilder.create(mXmppConnectionService);
+ stackBuilder.addParentStack(ConversationActivity.class);
+
+ final Intent manageAccountsIntent = new Intent(mXmppConnectionService, ManageAccountActivity.class);
+ stackBuilder.addNextIntent(manageAccountsIntent);
+
+ final PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
+
+ mBuilder.setContentIntent(resultPendingIntent);
+ notificationManager.notify(ERROR_NOTIFICATION_ID, mBuilder.build());
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
new file mode 100644
index 00000000..90f9027a
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
@@ -0,0 +1,2775 @@
+package eu.siacs.conversations.services;
+
+import android.annotation.SuppressLint;
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.database.ContentObserver;
+import android.graphics.Bitmap;
+import android.media.AudioManager;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.Binder;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.os.SystemClock;
+import android.provider.ContactsContract;
+import android.security.KeyChain;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.LruCache;
+import android.util.Pair;
+
+import net.java.otr4j.OtrException;
+import net.java.otr4j.session.Session;
+import net.java.otr4j.session.SessionID;
+import net.java.otr4j.session.SessionImpl;
+import net.java.otr4j.session.SessionStatus;
+
+import org.openintents.openpgp.IOpenPgpService2;
+import org.openintents.openpgp.util.OpenPgpApi;
+import org.openintents.openpgp.util.OpenPgpServiceConnection;
+
+import java.math.BigInteger;
+import java.security.SecureRandom;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import de.duenndns.ssl.MemorizingTrustManager;
+import de.thedevstack.android.logcat.Logging;
+import de.thedevstack.conversationsplus.ConversationsPlusApplication;
+import de.thedevstack.conversationsplus.ConversationsPlusPreferences;
+import de.thedevstack.conversationsplus.services.filetransfer.FileTransferManager;
+import de.thedevstack.conversationsplus.utils.ImageUtil;
+import de.thedevstack.conversationsplus.utils.MessageUtil;
+import de.thedevstack.conversationsplus.utils.UiUpdateHelper;
+import de.thedevstack.conversationsplus.utils.XmppConnectionServiceAccessor;
+import de.thedevstack.conversationsplus.utils.XmppSendUtil;
+import de.tzur.conversations.Settings;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.crypto.PgpEngine;
+import eu.siacs.conversations.crypto.axolotl.AxolotlService;
+import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Blockable;
+import eu.siacs.conversations.entities.Bookmark;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.MucOptions;
+import eu.siacs.conversations.entities.MucOptions.OnRenameListener;
+import eu.siacs.conversations.entities.Presence;
+import eu.siacs.conversations.entities.Roster;
+import eu.siacs.conversations.entities.ServiceDiscoveryResult;
+import eu.siacs.conversations.entities.Transferable;
+import eu.siacs.conversations.entities.TransferablePlaceholder;
+import eu.siacs.conversations.generator.IqGenerator;
+import eu.siacs.conversations.generator.MessageGenerator;
+import eu.siacs.conversations.generator.PresenceGenerator;
+import eu.siacs.conversations.http.HttpConnectionManager;
+import eu.siacs.conversations.parser.IqParser;
+import eu.siacs.conversations.parser.MessageParser;
+import eu.siacs.conversations.parser.PresenceParser;
+import eu.siacs.conversations.persistance.DatabaseBackend;
+import eu.siacs.conversations.persistance.FileBackend;
+import eu.siacs.conversations.ui.UiCallback;
+import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.utils.ExceptionHelper;
+import eu.siacs.conversations.utils.FileUtils;
+import eu.siacs.conversations.utils.OnPhoneContactsLoadedListener;
+import eu.siacs.conversations.utils.PhoneHelper;
+import eu.siacs.conversations.utils.Xmlns;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.OnBindListener;
+import eu.siacs.conversations.xmpp.OnContactStatusChanged;
+import eu.siacs.conversations.xmpp.OnIqPacketReceived;
+import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
+import eu.siacs.conversations.xmpp.OnMessageAcknowledged;
+import eu.siacs.conversations.xmpp.OnMessagePacketReceived;
+import eu.siacs.conversations.xmpp.OnPresencePacketReceived;
+import eu.siacs.conversations.xmpp.OnStatusChanged;
+import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import eu.siacs.conversations.xmpp.chatstate.ChatState;
+import eu.siacs.conversations.xmpp.forms.Data;
+import eu.siacs.conversations.xmpp.forms.Field;
+import eu.siacs.conversations.xmpp.jid.InvalidJidException;
+import eu.siacs.conversations.xmpp.jid.Jid;
+import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
+import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
+import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
+import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
+
+import me.leolin.shortcutbadger.ShortcutBadger;
+
+public class XmppConnectionService extends Service implements OnPhoneContactsLoadedListener {
+
+ public static final String ACTION_CLEAR_NOTIFICATION = "clear_notification";
+ public static final String ACTION_DISABLE_FOREGROUND = "disable_foreground";
+ public static final String ACTION_TRY_AGAIN = "try_again";
+ public static final String ACTION_DISABLE_ACCOUNT = "disable_account";
+ private static final String ACTION_MERGE_PHONE_CONTACTS = "merge_phone_contacts";
+ public static final String ACTION_GCM_TOKEN_REFRESH = "gcm_token_refresh";
+ public static final String ACTION_GCM_MESSAGE_RECEIVED = "gcm_message_received";
+ private final IBinder mBinder = new XmppConnectionBinder();
+ private final List<Conversation> conversations = new CopyOnWriteArrayList<>();
+ private final IqGenerator mIqGenerator = new IqGenerator();
+ private final List<String> mInProgressAvatarFetches = new ArrayList<>();
+ public DatabaseBackend databaseBackend;
+ private ContentObserver contactObserver = new ContentObserver(null) {
+ @Override
+ public void onChange(boolean selfChange) {
+ super.onChange(selfChange);
+ Intent intent = new Intent(getApplicationContext(),
+ XmppConnectionService.class);
+ intent.setAction(ACTION_MERGE_PHONE_CONTACTS);
+ startService(intent);
+ }
+ };
+ private NotificationService mNotificationService = new NotificationService(
+ this);
+ private OnMessagePacketReceived mMessageParser = new MessageParser(this);
+ private OnPresencePacketReceived mPresenceParser = new PresenceParser(this);
+ private IqParser mIqParser = new IqParser(this);
+ private OnIqPacketReceived mDefaultIqHandler = new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ if (packet.getType() != IqPacket.TYPE.RESULT) {
+ Element error = packet.findChild("error");
+ String text = error != null ? error.findChildContent("text") : null;
+ if (text != null) {
+ Logging.d(Config.LOGTAG, account.getJid().toBareJid() + ": received iq error - " + text);
+ }
+ }
+ }
+ };
+ private MessageGenerator mMessageGenerator = new MessageGenerator();
+ private PresenceGenerator mPresenceGenerator = new PresenceGenerator();
+ private List<Account> accounts;
+ private JingleConnectionManager mJingleConnectionManager = new JingleConnectionManager();
+ public OnContactStatusChanged onContactStatusChanged = new OnContactStatusChanged() {
+
+ @Override
+ public void onContactStatusChanged(Contact contact, boolean online) {
+ Conversation conversation = find(getConversations(), contact);
+ if (conversation != null) {
+ if (online) {
+ conversation.endOtrIfNeeded();
+ if (contact.getPresences().size() == 1) {
+ sendUnsentMessages(conversation);
+ }
+ } else {
+ if (contact.getPresences().size() >= 1) {
+ if (conversation.hasValidOtrSession()) {
+ String otrResource = conversation.getOtrSession().getSessionID().getUserID();
+ if (!(Arrays.asList(contact.getPresences().asStringArray()).contains(otrResource))) {
+ conversation.endOtrIfNeeded();
+ }
+ }
+ } else {
+ conversation.endOtrIfNeeded();
+ }
+ }
+ }
+ }
+ };
+ private MessageArchiveService mMessageArchiveService = new MessageArchiveService(this);
+ private PushManagementService mPushManagementService = new PushManagementService(this);
+ private OnConversationUpdate mOnConversationUpdate = null;
+ private final OnJinglePacketReceived jingleListener = new OnJinglePacketReceived() {
+
+ @Override
+ public void onJinglePacketReceived(Account account, JinglePacket packet) {
+ mJingleConnectionManager.deliverPacket(account, packet);
+ }
+ };
+ private final OnMessageAcknowledged mOnMessageAcknowledgedListener = new OnMessageAcknowledged() {
+
+ @Override
+ public void onMessageAcknowledged(Account account, String uuid) {
+ for (final Conversation conversation : getConversations()) {
+ if (conversation.getAccount() == account) {
+ Message message = conversation.findUnsentMessageWithUuid(uuid);
+ if (message != null) {
+ MessageUtil.markMessage(message, Message.STATUS_SEND);
+ }
+ }
+ }
+ }
+ };
+ private int convChangedListenerCount = 0;
+ private OnShowErrorToast mOnShowErrorToast = null;
+ private int showErrorToastListenerCount = 0;
+ private int unreadCount = -1;
+ private OnAccountUpdate mOnAccountUpdate = null;
+ private OnCaptchaRequested mOnCaptchaRequested = null;
+ private int accountChangedListenerCount = 0;
+ private int captchaRequestedListenerCount = 0;
+ private OnRosterUpdate mOnRosterUpdate = null;
+ private OnUpdateBlocklist mOnUpdateBlocklist = null;
+ private int updateBlocklistListenerCount = 0;
+ private int rosterChangedListenerCount = 0;
+ private OnMucRosterUpdate mOnMucRosterUpdate = null;
+ private int mucRosterChangedListenerCount = 0;
+ private OnKeyStatusUpdated mOnKeyStatusUpdated = null;
+ private int keyStatusUpdatedListenerCount = 0;
+ private LruCache<Pair<String,String>,ServiceDiscoveryResult> discoCache = new LruCache<>(20);
+ private final OnBindListener mOnBindListener = new OnBindListener() {
+
+ @Override
+ public void onBind(final Account account) {
+ synchronized (mInProgressAvatarFetches) {
+ for (Iterator<String> iterator = mInProgressAvatarFetches.iterator(); iterator.hasNext(); ) {
+ final String KEY = iterator.next();
+ if (KEY.startsWith(account.getJid().toBareJid() + "_")) {
+ iterator.remove();
+ }
+ }
+ }
+ account.getRoster().clearPresences();
+ mJingleConnectionManager.cancelInTransmission();
+ fetchRosterFromServer(account);
+ fetchBookmarks(account);
+ sendPresence(account);
+ if (mPushManagementService.available(account)) {
+ mPushManagementService.registerPushTokenOnServer(account);
+ }
+ connectMultiModeConversations(account);
+ syncDirtyContacts(account);
+ }
+ };
+ private OnStatusChanged statusListener = new OnStatusChanged() {
+
+ @Override
+ public void onStatusChanged(final Account account) {
+ XmppConnection connection = account.getXmppConnection();
+ if (mOnAccountUpdate != null) {
+ mOnAccountUpdate.onAccountUpdate();
+ }
+ if (account.getStatus() == Account.State.ONLINE) {
+ mMessageArchiveService.executePendingQueries(account);
+ if (connection != null && connection.getFeatures().csi()) {
+ if (checkListeners()) {
+ Logging.d(Config.LOGTAG, account.getJid().toBareJid() + " sending csi//inactive");
+ connection.sendInactive();
+ } else {
+ Logging.d(Config.LOGTAG, account.getJid().toBareJid() + " sending csi//active");
+ connection.sendActive();
+ }
+ }
+ List<Conversation> conversations = getConversations();
+ for (Conversation conversation : conversations) {
+ if (conversation.getAccount() == account
+ && !account.pendingConferenceJoins.contains(conversation)) {
+ if (!conversation.startOtrIfNeeded()) {
+ Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": couldn't start OTR with "+conversation.getContact().getJid()+" when needed");
+ }
+ sendUnsentMessages(conversation);
+ }
+ }
+ for (Conversation conversation : account.pendingConferenceLeaves) {
+ leaveMuc(conversation);
+ }
+ account.pendingConferenceLeaves.clear();
+ for (Conversation conversation : account.pendingConferenceJoins) {
+ joinMuc(conversation);
+ }
+ account.pendingConferenceJoins.clear();
+ scheduleWakeUpCall(Config.PING_MAX_INTERVAL, account.getUuid().hashCode());
+ } else if (account.getStatus() == Account.State.OFFLINE) {
+ resetSendingToWaiting(account);
+ final boolean disabled = account.isOptionSet(Account.OPTION_DISABLED);
+ final boolean pushMode = Config.CLOSE_TCP_WHEN_SWITCHING_TO_BACKGROUND
+ && mPushManagementService.available(account)
+ && checkListeners();
+ Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": push mode "+Boolean.toString(pushMode));
+ if (!disabled && !pushMode) {
+ int timeToReconnect = ConversationsPlusApplication.getSecureRandom().nextInt(20) + 10;
+ scheduleWakeUpCall(timeToReconnect, 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)) {
+ if (connection != null) {
+ int next = connection.getTimeToNextAttempt();
+ Logging.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();
+ }
+ };
+ private OpenPgpServiceConnection pgpServiceConnection;
+ private PgpEngine mPgpEngine = null;
+ private WakeLock wakeLock;
+ private Thread mPhoneContactMergerThread;
+ private EventReceiver mEventReceiver = new EventReceiver();
+
+ private boolean mRestoredFromDatabase = false;
+
+ public boolean areMessagesInitialized() {
+ return this.mRestoredFromDatabase;
+ }
+
+ public PgpEngine getPgpEngine() {
+ if (!Config.supportOpenPgp()) {
+ return null;
+ } else if (pgpServiceConnection != null && pgpServiceConnection.isBound()) {
+ if (this.mPgpEngine == null) {
+ this.mPgpEngine = new PgpEngine(new OpenPgpApi(
+ getApplicationContext(),
+ pgpServiceConnection.getService()), this);
+ }
+ return mPgpEngine;
+ } else {
+ return null;
+ }
+ }
+
+ public Conversation find(Bookmark bookmark) {
+ return find(bookmark.getAccount(), bookmark.getJid());
+ }
+
+ public Conversation find(final Account account, final Jid jid) {
+ return find(getConversations(), account, jid);
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ final String action = intent == null ? null : intent.getAction();
+ boolean interactive = false;
+ if (action != null) {
+ switch (action) {
+ case ConnectivityManager.CONNECTIVITY_ACTION:
+ if (hasInternetConnection() && Config.RESET_ATTEMPT_COUNT_ON_NETWORK_CHANGE) {
+ resetAllAttemptCounts(true);
+ }
+ break;
+ case ACTION_MERGE_PHONE_CONTACTS:
+ if (mRestoredFromDatabase) {
+ loadPhoneContacts();
+ }
+ return START_STICKY;
+ case Intent.ACTION_SHUTDOWN:
+ logoutAndSave(true);
+ return START_NOT_STICKY;
+ case ACTION_CLEAR_NOTIFICATION:
+ mNotificationService.clear();
+ break;
+ case ACTION_DISABLE_FOREGROUND:
+ ConversationsPlusPreferences.commitKeepForegroundService(false);
+ toggleForegroundService();
+ break;
+ case ACTION_TRY_AGAIN:
+ resetAllAttemptCounts(false);
+ interactive = true;
+ break;
+ case ACTION_DISABLE_ACCOUNT:
+ try {
+ String jid = intent.getStringExtra("account");
+ Account account = jid == null ? null : findAccountByJid(Jid.fromString(jid));
+ if (account != null) {
+ account.setOption(Account.OPTION_DISABLED, true);
+ updateAccount(account);
+ }
+ } catch (final InvalidJidException ignored) {
+ break;
+ }
+ break;
+ case AudioManager.RINGER_MODE_CHANGED_ACTION:
+ if (ConversationsPlusPreferences.xaOnSilentMode()) {
+ refreshAllPresences();
+ }
+ break;
+ case Intent.ACTION_SCREEN_OFF:
+ case Intent.ACTION_SCREEN_ON:
+ if (ConversationsPlusPreferences.awayWhenScreenOff()) {
+ refreshAllPresences();
+ }
+ break;
+ case ACTION_GCM_TOKEN_REFRESH:
+ refreshAllGcmTokens();
+ break;
+ case ACTION_GCM_MESSAGE_RECEIVED:
+ Log.d(Config.LOGTAG,"gcm push message arrived in service. extras="+intent.getExtras());
+ }
+ }
+ this.wakeLock.acquire();
+
+ for (Account account : accounts) {
+ if (!account.isOptionSet(Account.OPTION_DISABLED)) {
+ if (!hasInternetConnection()) {
+ account.setStatus(Account.State.NO_INTERNET);
+ if (statusListener != null) {
+ statusListener.onStatusChanged(account);
+ }
+ } else {
+ if (account.getStatus() == Account.State.NO_INTERNET) {
+ account.setStatus(Account.State.OFFLINE);
+ if (statusListener != null) {
+ statusListener.onStatusChanged(account);
+ }
+ }
+ if (account.getStatus() == Account.State.ONLINE) {
+ long lastReceived = account.getXmppConnection().getLastPacketReceived();
+ long lastSent = account.getXmppConnection().getLastPingSent();
+ long pingInterval = "ui".equals(action) ? Config.PING_MIN_INTERVAL * 1000 : Config.PING_MAX_INTERVAL * 1000;
+ long msToNextPing = (Math.max(lastReceived, lastSent) + pingInterval) - SystemClock.elapsedRealtime();
+ long pingTimeoutIn = (lastSent + Config.PING_TIMEOUT * 1000) - SystemClock.elapsedRealtime();
+ if (lastSent > lastReceived) {
+ if (pingTimeoutIn < 0) {
+ Logging.d(Config.LOGTAG, account.getJid().toBareJid() + ": ping timeout");
+ this.reconnectAccount(account, true, interactive);
+ } else {
+ int secs = (int) (pingTimeoutIn / 1000);
+ this.scheduleWakeUpCall(secs, account.getUuid().hashCode());
+ }
+ } else if (msToNextPing <= 0) {
+ account.getXmppConnection().sendPing();
+ Logging.d(Config.LOGTAG, account.getJid().toBareJid() + " send ping");
+ this.scheduleWakeUpCall(Config.PING_TIMEOUT, account.getUuid().hashCode());
+ } else {
+ this.scheduleWakeUpCall((int) (msToNextPing / 1000), account.getUuid().hashCode());
+ }
+ } 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) {
+ Logging.d(Config.LOGTAG, account.getJid() + ": time out during connect reconnecting");
+ 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 {
+ if (account.getXmppConnection().getTimeToNextAttempt() <= 0) {
+ reconnectAccount(account, true, interactive);
+ }
+ }
+
+ }
+ if (mOnAccountUpdate != null) {
+ mOnAccountUpdate.onAccountUpdate();
+ }
+ }
+ }
+ if (wakeLock.isHeld()) {
+ try {
+ wakeLock.release();
+ } catch (final RuntimeException ignored) {
+ }
+ }
+ return START_STICKY;
+ }
+
+ private Presence.Status getTargetPresence() {
+ if (ConversationsPlusPreferences.xaOnSilentMode() && isPhoneSilenced()) {
+ return Presence.Status.XA;
+ } else if (ConversationsPlusPreferences.awayWhenScreenOff() && !isInteractive()) {
+ return Presence.Status.AWAY;
+ } else {
+ return Presence.Status.ONLINE;
+ }
+ }
+
+ @SuppressLint("NewApi")
+ @SuppressWarnings("deprecation")
+ public boolean isInteractive() {
+ final PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
+
+ final boolean isScreenOn;
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+ isScreenOn = pm.isScreenOn();
+ } else {
+ isScreenOn = pm.isInteractive();
+ }
+ return isScreenOn;
+ }
+
+ private boolean isPhoneSilenced() {
+ AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
+ if (ConversationsPlusPreferences.treatVibrateAsSilent()) {
+ return audioManager.getRingerMode() != AudioManager.RINGER_MODE_NORMAL;
+ } else {
+ return audioManager.getRingerMode() == AudioManager.RINGER_MODE_SILENT;
+ }
+ }
+
+ private void resetAllAttemptCounts(boolean reallyAll) {
+ Logging.d(Config.LOGTAG, "resetting all attempt counts");
+ for (Account account : accounts) {
+ if (account.hasErrorStatus() || reallyAll) {
+ final XmppConnection connection = account.getXmppConnection();
+ if (connection != null) {
+ connection.resetAttemptCount();
+ }
+ }
+ }
+ }
+
+ public boolean hasInternetConnection() {
+ ConnectivityManager cm = (ConnectivityManager) getApplicationContext()
+ .getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
+ return activeNetwork != null && activeNetwork.isConnected();
+ }
+
+ /**
+ * check whether we are allowed to download at the moment
+ */
+ public boolean isDownloadAllowedInConnection() {
+ if (ConversationsPlusPreferences.autoDownloadFileWLAN()) {
+ return isWifiConnected();
+ }
+ return true;
+ }
+
+ /**
+ * check whether wifi is connected
+ */
+ public boolean isWifiConnected() {
+ ConnectivityManager cm = (ConnectivityManager) ConversationsPlusApplication.getInstance().getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo niWifi = cm.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
+ return niWifi.isConnected();
+ }
+
+ @SuppressLint("TrulyRandom")
+ @Override
+ public void onCreate() {
+ ExceptionHelper.init(getApplicationContext());
+ final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
+ final int cacheSize = maxMemory / 8;
+
+ this.databaseBackend = DatabaseBackend.getInstance(getApplicationContext());
+ this.accounts = databaseBackend.getAccounts();
+
+ restoreFromDatabase();
+
+ getContentResolver().registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, contactObserver);
+
+ if (Config.supportOpenPgp()) {
+ this.pgpServiceConnection = new OpenPgpServiceConnection(getApplicationContext(), "org.sufficientlysecure.keychain", new OpenPgpServiceConnection.OnBound() {
+ @Override
+ public void onBound(IOpenPgpService2 service) {
+ for (Account account : accounts) {
+ if (account.getPgpDecryptionService() != null) {
+ account.getPgpDecryptionService().onOpenPgpServiceBound();
+ }
+ }
+ }
+
+ @Override
+ public void onError(Exception e) {
+ }
+ });
+ this.pgpServiceConnection.bindToService();
+ }
+
+ this.wakeLock = ConversationsPlusApplication.createPartialWakeLock("XmppConnectionService");
+ toggleForegroundService();
+ updateUnreadCountBadge();
+ UiUpdateHelper.initXmppConnectionService(this);
+ XmppConnectionServiceAccessor.initXmppConnectionService(this);
+ toggleScreenEventReceiver();
+ }
+
+ @Override
+ public void onTrimMemory(int level) {
+ super.onTrimMemory(level);
+ if (level >= TRIM_MEMORY_COMPLETE) {
+ Log.d(Config.LOGTAG, "clear cache due to low memory");
+ ImageUtil.evictBitmapCache();
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ try {
+ unregisterReceiver(this.mEventReceiver);
+ } catch (IllegalArgumentException e) {
+ //ignored
+ }
+ super.onDestroy();
+ }
+
+ public void toggleScreenEventReceiver() {
+ if (ConversationsPlusPreferences.awayWhenScreenOff()) {
+ final IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_ON);
+ filter.addAction(Intent.ACTION_SCREEN_OFF);
+ registerReceiver(this.mEventReceiver, filter);
+ } else {
+ try {
+ unregisterReceiver(this.mEventReceiver);
+ } catch (IllegalArgumentException e) {
+ //ignored
+ }
+ }
+ }
+
+ public void toggleForegroundService() {
+ if (ConversationsPlusPreferences.keepForegroundService()) {
+ startForeground(NotificationService.FOREGROUND_NOTIFICATION_ID, this.mNotificationService.createForegroundNotification());
+ } else {
+ stopForeground(true);
+ }
+ }
+
+ @Override
+ public void onTaskRemoved(final Intent rootIntent) {
+ super.onTaskRemoved(rootIntent);
+ if (!ConversationsPlusPreferences.keepForegroundService()) {
+ this.logoutAndSave(false);
+ }
+ }
+
+ private void logoutAndSave(boolean stop) {
+ int activeAccounts = 0;
+ for (final Account account : accounts) {
+ if (account.getStatus() != Account.State.DISABLED) {
+ activeAccounts++;
+ }
+ databaseBackend.writeRoster(account.getRoster());
+ if (account.getXmppConnection() != null) {
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ disconnect(account, false);
+ }
+ }).start();
+ }
+ }
+ if (stop || activeAccounts == 0) {
+ Logging.d(Config.LOGTAG, "good bye");
+ stopSelf();
+ }
+ }
+
+ 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) {
+ final long timeToWake = SystemClock.elapsedRealtime() + (seconds < 0 ? 1 : seconds + 1) * 1000;
+
+ Context context = getApplicationContext();
+ AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+
+ Intent intent = new Intent(context, EventReceiver.class);
+ intent.setAction("ping");
+ PendingIntent alarmIntent = PendingIntent.getBroadcast(context, requestCode, intent, 0);
+ alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, alarmIntent);
+ }
+
+ public XmppConnection createConnection(final Account account) {
+ account.setResource(ConversationsPlusPreferences.resource().toLowerCase(Locale.getDefault()));
+ final XmppConnection connection = new XmppConnection(account, this);
+ connection.setOnMessagePacketReceivedListener(this.mMessageParser);
+ connection.setOnStatusChangedListener(this.statusListener);
+ connection.setOnPresencePacketReceivedListener(this.mPresenceParser);
+ connection.setOnUnregisteredIqPacketReceivedListener(this.mIqParser);
+ connection.setOnJinglePacketReceivedListener(this.jingleListener);
+ connection.setOnBindListener(this.mOnBindListener);
+ connection.setOnMessageAcknowledgeListener(this.mOnMessageAcknowledgedListener);
+ connection.addOnAdvancedStreamFeaturesAvailableListener(this.mMessageArchiveService);
+ connection.addOnAdvancedStreamFeaturesAvailableListener(AvatarService.getInstance());
+ AxolotlService axolotlService = account.getAxolotlService();
+ if (axolotlService != null) {
+ connection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService);
+ }
+ return connection;
+ }
+
+ public void sendChatState(Conversation conversation) {
+ if (ConversationsPlusPreferences.chatStates()) {
+ MessagePacket packet = mMessageGenerator.generateChatState(conversation);
+ sendMessagePacket(conversation.getAccount(), packet);
+ }
+ }
+
+ public void sendMessage(final Message message) {
+ sendMessage(message, false, false);
+ }
+
+ private void sendMessage(final Message message, final boolean resend, final boolean delay) {
+ final Account account = message.getConversation().getAccount();
+ final Conversation conversation = message.getConversation();
+ account.deactivateGracePeriod();
+ MessagePacket packet = null;
+ final boolean addToConversation = (conversation.getMode() != Conversation.MODE_MULTI
+ || account.getServerIdentity() != XmppConnection.Identity.SLACK)
+ && !message.edited();
+ boolean saveInDb = addToConversation;
+ message.setStatus(Message.STATUS_WAITING);
+
+ if (!resend && message.getEncryption() != Message.ENCRYPTION_OTR) {
+ message.getConversation().endOtrIfNeeded();
+ message.getConversation().findUnsentMessagesWithEncryption(Message.ENCRYPTION_OTR,
+ new Conversation.OnMessageFound() {
+ @Override
+ public void onMessageFound(Message message) {
+ MessageUtil.markMessage(message, Message.STATUS_SEND_FAILED);
+ }
+ });
+ }
+
+ if (account.isOnlineAndConnected()) {
+ FileTransferManager fileTransferManager = FileTransferManager.getInstance();
+ switch (message.getEncryption()) {
+ case Message.ENCRYPTION_NONE:
+ if (fileTransferManager.accept(message)) {
+ if (!fileTransferManager.transferFile(message, delay)) {
+ break;
+ }
+ } else {
+ packet = mMessageGenerator.generateChat(message);
+ }
+ break;
+ case Message.ENCRYPTION_PGP:
+ case Message.ENCRYPTION_DECRYPTED:
+ if (fileTransferManager.accept(message)) {
+ if (!fileTransferManager.transferFile(message, delay)) {
+ break;
+ }
+ } else {
+ packet = mMessageGenerator.generatePgpChat(message);
+ }
+ break;
+ case Message.ENCRYPTION_OTR:
+ SessionImpl otrSession = conversation.getOtrSession();
+ if (otrSession != null && otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) {
+ try {
+ message.setCounterpart(Jid.fromSessionID(otrSession.getSessionID()));
+ } catch (InvalidJidException e) {
+ break;
+ }
+ if (message.needsUploading()) { //TODO: Use FileTransferManager with a preselection of filetransfer method
+ mJingleConnectionManager.createNewConnection(message);
+ } else {
+ packet = mMessageGenerator.generateOtrChat(message);
+ }
+ } else if (otrSession == null) {
+ if (message.fixCounterpart()) {
+ conversation.startOtrSession(message.getCounterpart().getResourcepart(), true);
+ } else {
+ Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": could not fix counterpart for OTR message to contact "+message.getContact().getJid());
+ break;
+ }
+ } else {
+ Logging.d(Config.LOGTAG,account.getJid().toBareJid()+" OTR session with "+message.getContact()+" is in wrong state: "+otrSession.getSessionStatus().toString());
+ }
+ break;
+ case Message.ENCRYPTION_AXOLOTL:
+ message.setFingerprint(account.getAxolotlService().getOwnFingerprint());
+ if (fileTransferManager.accept(message)) {
+ if (!fileTransferManager.transferFile(message, delay)) {
+ break;
+ }
+ } else {
+ XmppAxolotlMessage axolotlMessage = account.getAxolotlService().fetchAxolotlMessageFromCache(message);
+ if (axolotlMessage == null) {
+ account.getAxolotlService().preparePayloadMessage(message, delay);
+ } else {
+ packet = mMessageGenerator.generateAxolotlChat(message, axolotlMessage);
+ }
+ }
+ break;
+
+ }
+ if (packet != null) {
+ if (account.getXmppConnection().getFeatures().sm() || conversation.getMode() == Conversation.MODE_MULTI) {
+ message.setStatus(Message.STATUS_UNSEND);
+ } else {
+ message.setStatus(Message.STATUS_SEND);
+ }
+ }
+ } else {
+ switch (message.getEncryption()) {
+ case Message.ENCRYPTION_DECRYPTED:
+ if (!message.needsUploading()) {
+ String pgpBody = message.getEncryptedBody();
+ String decryptedBody = message.getBody();
+ message.setBody(pgpBody);
+ message.setEncryption(Message.ENCRYPTION_PGP);
+ databaseBackend.createMessage(message);
+ saveInDb = false;
+ message.setBody(decryptedBody);
+ message.setEncryption(Message.ENCRYPTION_DECRYPTED);
+ }
+ break;
+ case Message.ENCRYPTION_OTR:
+ if (!conversation.hasValidOtrSession() && message.getCounterpart() != null) {
+ Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": create otr session without starting for "+message.getContact().getJid());
+ conversation.startOtrSession(message.getCounterpart().getResourcepart(), false);
+ }
+ break;
+ case Message.ENCRYPTION_AXOLOTL:
+ message.setFingerprint(account.getAxolotlService().getOwnFingerprint());
+ break;
+ }
+ }
+
+ if (resend) {
+ if (packet != null && addToConversation) {
+ if (account.getXmppConnection().getFeatures().sm() || conversation.getMode() == Conversation.MODE_MULTI) {
+ MessageUtil.markMessage(message, Message.STATUS_UNSEND);
+ } else {
+ MessageUtil.markMessage(message, Message.STATUS_SEND);
+ }
+ }
+ } else {
+ if (addToConversation) {
+ conversation.add(message);
+ }
+ if (message.getEncryption() == Message.ENCRYPTION_NONE || !ConversationsPlusPreferences.dontSaveEncrypted()) {
+ if (saveInDb) {
+ databaseBackend.createMessage(message);
+ } else if (message.edited()) {
+ databaseBackend.updateMessage(message, message.getEditedId());
+ }
+ }
+ updateConversationUi();
+ }
+ if (packet != null) {
+ if (delay) {
+ mMessageGenerator.addDelay(packet, message.getTimeSent());
+ }
+ if (conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) {
+ if (ConversationsPlusPreferences.chatStates()) {
+ packet.addChild(ChatState.toElement(conversation.getOutgoingChatState()));
+ }
+ }
+ sendMessagePacket(account, packet);
+ }
+ }
+
+ private void sendUnsentMessages(final Conversation conversation) {
+ conversation.findWaitingMessages(new Conversation.OnMessageFound() {
+
+ @Override
+ public void onMessageFound(Message message) {
+ resendMessage(message, true);
+ }
+ });
+ }
+
+ public void resendMessage(final Message message, final boolean delay) {
+ sendMessage(message, true, delay);
+ }
+
+ public void fetchRosterFromServer(final Account account) {
+ final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.GET);
+ if (!"".equals(account.getRosterVersion())) {
+ Logging.d(Config.LOGTAG, account.getJid().toBareJid()
+ + ": fetching roster version " + account.getRosterVersion());
+ } else {
+ Logging.d(Config.LOGTAG, account.getJid().toBareJid() + ": fetching roster");
+ }
+ iqPacket.query(Xmlns.ROSTER).setAttribute("ver", account.getRosterVersion());
+ sendIqPacket(account, iqPacket, mIqParser);
+ }
+
+ public void fetchBookmarks(final Account account) {
+ final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.GET);
+ final Element query = iqPacket.query("jabber:iq:private");
+ query.addChild("storage", "storage:bookmarks");
+ final OnIqPacketReceived callback = new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(final Account account, final IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE.RESULT) {
+ final Element query = packet.query();
+ final HashMap<Jid, Bookmark> bookmarks = new HashMap<>();
+ final Element storage = query.findChild("storage", "storage:bookmarks");
+ if (storage != null) {
+ for (final Element item : storage.getChildren()) {
+ if (item.getName().equals("conference")) {
+ final Bookmark bookmark = Bookmark.parse(item, account);
+ Bookmark old = bookmarks.put(bookmark.getJid(), bookmark);
+ if (old != null && old.getBookmarkName() != null && bookmark.getBookmarkName() == null) {
+ bookmark.setBookmarkName(old.getBookmarkName());
+ }
+ Conversation conversation = find(bookmark);
+ if (conversation != null) {
+ conversation.setBookmark(bookmark);
+ } else if (bookmark.autojoin() && bookmark.getJid() != null && ConversationsPlusPreferences.autojoin()) {
+ conversation = findOrCreateConversation(
+ account, bookmark.getJid(), true);
+ conversation.setBookmark(bookmark);
+ joinMuc(conversation);
+ }
+ }
+ }
+ }
+ account.setBookmarks(new ArrayList<>(bookmarks.values()));
+ } else {
+ Logging.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not fetch bookmarks");
+ }
+ }
+ };
+ sendIqPacket(account, iqPacket, callback);
+ }
+
+ public void pushBookmarks(Account account) {
+ Logging.d(Config.LOGTAG, account.getJid().toBareJid()+": pushing bookmarks");
+ IqPacket iqPacket = new IqPacket(IqPacket.TYPE.SET);
+ Element query = iqPacket.query("jabber:iq:private");
+ Element storage = query.addChild("storage", "storage:bookmarks");
+ for (Bookmark bookmark : account.getBookmarks()) {
+ storage.addChild(bookmark);
+ }
+ sendIqPacket(account, iqPacket, mDefaultIqHandler);
+ }
+
+ public void onPhoneContactsLoaded(final List<Bundle> phoneContacts) {
+ if (mPhoneContactMergerThread != null) {
+ mPhoneContactMergerThread.interrupt();
+ }
+ mPhoneContactMergerThread = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ Logging.d(Config.LOGTAG, "start merging phone contacts with roster");
+ for (Account account : accounts) {
+ List<Contact> withSystemAccounts = account.getRoster().getWithSystemAccounts();
+ for (Bundle phoneContact : phoneContacts) {
+ if (Thread.interrupted()) {
+ Logging.d(Config.LOGTAG,"interrupted merging phone contacts");
+ return;
+ }
+ Jid jid;
+ try {
+ jid = Jid.fromString(phoneContact.getString("jid"));
+ } catch (final InvalidJidException e) {
+ continue;
+ }
+ final Contact contact = account.getRoster().getContact(jid);
+ String systemAccount = phoneContact.getInt("phoneid")
+ + "#"
+ + phoneContact.getString("lookup");
+ contact.setSystemAccount(systemAccount);
+ if (contact.setPhotoUri(phoneContact.getString("photouri"))) {
+ AvatarService.getInstance().clear(contact);
+ }
+ contact.setSystemName(phoneContact.getString("displayname"));
+ withSystemAccounts.remove(contact);
+ }
+ for (Contact contact : withSystemAccounts) {
+ contact.setSystemAccount(null);
+ contact.setSystemName(null);
+ if (contact.setPhotoUri(null)) {
+ AvatarService.getInstance().clear(contact);
+ }
+ }
+ }
+ Logging.d(Config.LOGTAG,"finished merging phone contacts");
+ updateAccountUi();
+ }
+ });
+ mPhoneContactMergerThread.start();
+ }
+
+ private void restoreFromDatabase() {
+ synchronized (this.conversations) {
+ final Map<String, Account> accountLookupTable = new Hashtable<>();
+ for (Account account : this.accounts) {
+ accountLookupTable.put(account.getUuid(), account);
+ }
+ this.conversations.addAll(databaseBackend.getConversations(Conversation.STATUS_AVAILABLE));
+ for (Conversation conversation : this.conversations) {
+ Account account = accountLookupTable.get(conversation.getAccountUuid());
+ conversation.setAccount(account);
+ }
+ Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ Logging.d(Config.LOGTAG, "restoring roster");
+ for (Account account : accounts) {
+ databaseBackend.readRoster(account.getRoster());
+ account.initAccountServices(XmppConnectionService.this); //roster needs to be loaded at this stage
+ }
+ ImageUtil.evictBitmapCache();
+ Looper.prepare();
+ loadPhoneContacts();
+ Logging.d(Config.LOGTAG, "restoring messages");
+ for (Conversation conversation : conversations) {
+ conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE));
+ checkDeletedFiles(conversation);
+ conversation.findUnreadMessages(new Conversation.OnMessageFound() {
+ @Override
+ public void onMessageFound(Message message) {
+ mNotificationService.pushFromBacklog(message);
+ }
+ });
+ }
+ mNotificationService.finishBacklog(false);
+ mRestoredFromDatabase = true;
+ Logging.d(Config.LOGTAG,"restored all messages");
+ updateConversationUi();
+ }
+ };
+ ConversationsPlusApplication.executeDatabaseOperation(runnable);
+ }
+ }
+
+ public void loadPhoneContacts() {
+ PhoneHelper.loadPhoneContacts(getApplicationContext(),
+ new CopyOnWriteArrayList<Bundle>(),
+ XmppConnectionService.this);
+ }
+
+ public List<Conversation> getConversations() {
+ return this.conversations;
+ }
+
+ private void checkDeletedFiles(Conversation conversation) {
+ conversation.findMessagesWithFiles(new Conversation.OnMessageFound() {
+
+ @Override
+ public void onMessageFound(Message message) {
+ if (!FileBackend.isFileAvailable(message)) {
+ message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED));
+ final int s = message.getStatus();
+ if (s == Message.STATUS_WAITING || s == Message.STATUS_OFFERED || s == Message.STATUS_UNSEND) {
+ MessageUtil.markMessage(message, Message.STATUS_SEND_FAILED);
+ }
+ }
+ }
+ });
+ }
+
+ public void populateWithOrderedConversations(final List<Conversation> list) {
+ populateWithOrderedConversations(list, true);
+ }
+
+ public void populateWithOrderedConversations(final List<Conversation> list, boolean includeNoFileUpload) {
+ list.clear();
+ if (includeNoFileUpload) {
+ list.addAll(getConversations());
+ } else {
+ for (Conversation conversation : getConversations()) {
+ if (conversation.getMode() == Conversation.MODE_SINGLE
+ || conversation.getAccount().httpUploadAvailable()) {
+ list.add(conversation);
+ }
+ }
+ }
+ Collections.sort(list, new Comparator<Conversation>() {
+ @Override
+ public int compare(Conversation lhs, Conversation rhs) {
+ Message left = lhs.getLatestMessage();
+ Message right = rhs.getLatestMessage();
+ if (left.getTimeSent() > right.getTimeSent()) {
+ return -1;
+ } else if (left.getTimeSent() < right.getTimeSent()) {
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+ });
+ }
+
+ public void loadMoreMessages(final Conversation conversation, final long timestamp, final OnMoreMessagesLoaded callback) {
+ if (XmppConnectionService.this.getMessageArchiveService().queryInProgress(conversation, callback)) {
+ Logging.d("mam", "Query in progress");
+ return;
+ } else if (timestamp == 0) {
+ Logging.d("mam", "Query stopped due to timestamp");
+ return;
+ }
+ //TODO Create a separate class for this runnable to store if messages are getting loaded or not. Not really a good idea to do this in the callback.
+ Logging.d(Config.LOGTAG, "load more messages for " + conversation.getName() + " prior to " + MessageGenerator.getTimestamp(timestamp));
+ Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ if (null == callback || !callback.isLoadingInProgress()) { // if a callback is set, ensure that there is no loading in progress
+ if (null != callback) {
+ callback.setLoadingInProgress(); // Tell the callback that the loading is in progress
+ }
+ final Account account = conversation.getAccount();
+ List<Message> messages = databaseBackend.getMessages(conversation, 50, timestamp);
+ Logging.d("mam", "runnable load more messages");
+ if (messages.size() > 0) {
+ Logging.d("mam", "At least one message");
+ conversation.addAll(0, messages);
+ checkDeletedFiles(conversation);
+ callback.onMoreMessagesLoaded(messages.size(), conversation);
+ } else if (conversation.hasMessagesLeftOnServer()
+ && account.isOnlineAndConnected()) {
+ Logging.d("mam", "account online and connected and messages left on server");
+ //TODO Check if this needs to be checked before trying anything with regards to MAM
+ if ((conversation.getMode() == Conversation.MODE_SINGLE && account.getXmppConnection().getFeatures().mam())
+ || (conversation.getMode() == Conversation.MODE_MULTI && conversation.getMucOptions().mamSupport())) {
+ Logging.d("mam", "mam active");
+ getMessageArchiveService().query(conversation, 0, timestamp - 1, callback);
+ callback.informUser(R.string.fetching_history_from_server);
+ } else {
+ Logging.d("mam", "mam inactive");
+ callback.onMoreMessagesLoaded(0, conversation);
+ }
+ } else {
+ Logging.d("mam", ((!conversation.hasMessagesLeftOnServer()) ? "no" : "")
+ + " more messages left on server, mam "
+ + ((account.isOnlineAndConnected() && account.getXmppConnection().getFeatures().mam()) ? "" : "not")
+ + " activated, account is " + ((account.isOnlineAndConnected()) ? "" : "not")
+ + " online or connected)");
+ callback.onMoreMessagesLoaded(0, conversation);
+ callback.informUser(R.string.no_more_history_on_server);
+ }
+ }
+ }
+ };
+ ConversationsPlusApplication.executeDatabaseOperation(runnable);
+ }
+
+ public List<Account> getAccounts() {
+ return this.accounts;
+ }
+
+ public Conversation find(final Iterable<Conversation> haystack, final Contact contact) {
+ for (final Conversation conversation : haystack) {
+ if (conversation.getContact() == contact) {
+ return conversation;
+ }
+ }
+ return null;
+ }
+
+ public Conversation find(final Iterable<Conversation> haystack, final Account account, final Jid jid) {
+ if (jid == null) {
+ return null;
+ }
+ for (final Conversation conversation : haystack) {
+ if ((account == null || conversation.getAccount() == account)
+ && (conversation.getJid().toBareJid().equals(jid.toBareJid()))) {
+ return conversation;
+ }
+ }
+ return null;
+ }
+
+ public Conversation findOrCreateConversation(final Account account, final Jid jid, final boolean muc) {
+ return this.findOrCreateConversation(account, jid, muc, null);
+ }
+
+ public Conversation findOrCreateConversation(final Account account, final Jid jid, final boolean muc, final MessageArchiveService.Query query) {
+ synchronized (this.conversations) {
+ Conversation conversation = find(account, jid);
+ if (conversation != null) {
+ return conversation;
+ }
+ conversation = databaseBackend.findConversation(account, jid);
+ if (conversation != null) {
+ conversation.setStatus(Conversation.STATUS_AVAILABLE);
+ conversation.setAccount(account);
+ if (muc) {
+ conversation.setMode(Conversation.MODE_MULTI);
+ conversation.setContactJid(jid);
+ } else {
+ conversation.setMode(Conversation.MODE_SINGLE);
+ conversation.setContactJid(jid.toBareJid());
+ }
+ conversation.setNextEncryption(-1);
+ conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE));
+ this.databaseBackend.updateConversation(conversation);
+ } else {
+ String conversationName;
+ Contact contact = account.getRoster().getContact(jid);
+ if (contact != null) {
+ conversationName = contact.getDisplayName();
+ } else {
+ conversationName = jid.getLocalpart();
+ }
+ if (muc) {
+ conversation = new Conversation(conversationName, account, jid,
+ Conversation.MODE_MULTI);
+ } else {
+ conversation = new Conversation(conversationName, account, jid.toBareJid(),
+ Conversation.MODE_SINGLE);
+ }
+ this.databaseBackend.createConversation(conversation);
+ }
+ if (account.getXmppConnection() != null
+ && account.getXmppConnection().getFeatures().mam()
+ && !muc) {
+ if (query == null) {
+ this.mMessageArchiveService.query(conversation);
+ } else {
+ if (query.getConversation() == null) {
+ this.mMessageArchiveService.query(conversation, query.getStart());
+ }
+ }
+ }
+ checkDeletedFiles(conversation);
+ this.conversations.add(conversation);
+ updateConversationUi();
+ return conversation;
+ }
+ }
+
+ public void archiveConversation(Conversation conversation) {
+ getNotificationService().clear(conversation);
+ conversation.setStatus(Conversation.STATUS_ARCHIVED);
+ conversation.setNextEncryption(-1);
+ synchronized (this.conversations) {
+ if (conversation.getMode() == Conversation.MODE_MULTI) {
+ if (conversation.getAccount().getStatus() == Account.State.ONLINE) {
+ Bookmark bookmark = conversation.getBookmark();
+ if (bookmark != null && bookmark.autojoin() && ConversationsPlusPreferences.autojoin()) {
+ bookmark.setAutojoin(false);
+ pushBookmarks(bookmark.getAccount());
+ }
+ }
+ leaveMuc(conversation);
+ } else {
+ conversation.endOtrIfNeeded();
+ if (conversation.getContact().getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
+ Log.d(Config.LOGTAG, "Canceling presence request from " + conversation.getJid().toString());
+ sendPresencePacket(
+ conversation.getAccount(),
+ mPresenceGenerator.stopPresenceUpdatesTo(conversation.getContact())
+ );
+ }
+ }
+ this.databaseBackend.updateConversation(conversation);
+ this.conversations.remove(conversation);
+ updateConversationUi();
+ }
+ }
+
+ public void createAccount(final Account account) {
+ account.initAccountServices(this);
+ databaseBackend.createAccount(account);
+ this.accounts.add(account);
+ this.reconnectAccountInBackground(account);
+ updateAccountUi();
+ }
+
+ public void createAccountFromKey(final String alias, final OnAccountCreated callback) {
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ X509Certificate[] chain = KeyChain.getCertificateChain(XmppConnectionService.this, alias);
+ Pair<Jid, String> info = CryptoHelper.extractJidAndName(chain[0]);
+ if (findAccountByJid(info.first) == null) {
+ Account account = new Account(info.first, "");
+ account.setPrivateKeyAlias(alias);
+ account.setOption(Account.OPTION_DISABLED, true);
+ account.setDisplayName(info.second);
+ createAccount(account);
+ callback.onAccountCreated(account);
+ if (Config.X509_VERIFICATION) {
+ try {
+ ConversationsPlusApplication.getMemorizingTrustManager().getNonInteractive().checkClientTrusted(chain, "RSA");
+ } catch (CertificateException e) {
+ callback.informUser(R.string.certificate_chain_is_not_trusted);
+ }
+ }
+ } else {
+ callback.informUser(R.string.account_already_exists);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ callback.informUser(R.string.unable_to_parse_certificate);
+ }
+ }
+ }).start();
+
+ }
+
+ public void updateKeyInAccount(final Account account, final String alias) {
+ Log.d(Config.LOGTAG, "update key in account " + alias);
+ try {
+ X509Certificate[] chain = KeyChain.getCertificateChain(XmppConnectionService.this, alias);
+ Pair<Jid, String> info = CryptoHelper.extractJidAndName(chain[0]);
+ if (account.getJid().toBareJid().equals(info.first)) {
+ account.setPrivateKeyAlias(alias);
+ account.setDisplayName(info.second);
+ databaseBackend.updateAccount(account);
+ if (Config.X509_VERIFICATION) {
+ try {
+ ConversationsPlusApplication.getMemorizingTrustManager().getNonInteractive().checkClientTrusted(chain, "RSA");
+ } catch (CertificateException e) {
+ showErrorToastInUi(R.string.certificate_chain_is_not_trusted);
+ }
+ account.getAxolotlService().regenerateKeys(true);
+ }
+ } else {
+ showErrorToastInUi(R.string.jid_does_not_match_certificate);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void updateAccount(final Account account) {
+ this.statusListener.onStatusChanged(account);
+ databaseBackend.updateAccount(account);
+ reconnectAccountInBackground(account);
+ updateAccountUi();
+ getNotificationService().updateErrorNotification();
+ }
+
+ public void updateAccountPasswordOnServer(final Account account, final String newPassword, final OnAccountPasswordChanged callback) {
+ final IqPacket iq = getIqGenerator().generateSetPassword(account, newPassword);
+ sendIqPacket(account, iq, new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(final Account account, final IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE.RESULT) {
+ account.setPassword(newPassword);
+ databaseBackend.updateAccount(account);
+ callback.onPasswordChangeSucceeded();
+ } else {
+ callback.onPasswordChangeFailed();
+ }
+ }
+ });
+ }
+
+ public void deleteAccount(final Account account) {
+ synchronized (this.conversations) {
+ for (final Conversation conversation : conversations) {
+ if (conversation.getAccount() == account) {
+ if (conversation.getMode() == Conversation.MODE_MULTI) {
+ leaveMuc(conversation);
+ } else if (conversation.getMode() == Conversation.MODE_SINGLE) {
+ conversation.endOtrIfNeeded();
+ }
+ conversations.remove(conversation);
+ }
+ }
+ if (account.getXmppConnection() != null) {
+ this.disconnect(account, true);
+ }
+ Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ databaseBackend.deleteAccount(account);
+ }
+ };
+ ConversationsPlusApplication.executeDatabaseOperation(runnable);
+ this.accounts.remove(account);
+ updateAccountUi();
+ getNotificationService().updateErrorNotification();
+ }
+ }
+
+ public void setOnConversationListChangedListener(OnConversationUpdate listener) {
+ synchronized (this) {
+ if (checkListeners()) {
+ switchToForeground();
+ }
+ this.mOnConversationUpdate = listener;
+ this.mNotificationService.setIsInForeground(true);
+ if (this.convChangedListenerCount < 2) {
+ this.convChangedListenerCount++;
+ }
+ }
+ }
+
+ public void removeOnConversationListChangedListener() {
+ synchronized (this) {
+ this.convChangedListenerCount--;
+ if (this.convChangedListenerCount <= 0) {
+ this.convChangedListenerCount = 0;
+ this.mOnConversationUpdate = null;
+ this.mNotificationService.setIsInForeground(false);
+ if (checkListeners()) {
+ switchToBackground();
+ }
+ }
+ }
+ }
+
+ public void setOnShowErrorToastListener(OnShowErrorToast onShowErrorToast) {
+ synchronized (this) {
+ if (checkListeners()) {
+ switchToForeground();
+ }
+ this.mOnShowErrorToast = onShowErrorToast;
+ if (this.showErrorToastListenerCount < 2) {
+ this.showErrorToastListenerCount++;
+ }
+ }
+ this.mOnShowErrorToast = onShowErrorToast;
+ }
+
+ public void removeOnShowErrorToastListener() {
+ synchronized (this) {
+ this.showErrorToastListenerCount--;
+ if (this.showErrorToastListenerCount <= 0) {
+ this.showErrorToastListenerCount = 0;
+ this.mOnShowErrorToast = null;
+ if (checkListeners()) {
+ switchToBackground();
+ }
+ }
+ }
+ }
+
+ public void setOnAccountListChangedListener(OnAccountUpdate listener) {
+ synchronized (this) {
+ if (checkListeners()) {
+ switchToForeground();
+ }
+ this.mOnAccountUpdate = listener;
+ if (this.accountChangedListenerCount < 2) {
+ this.accountChangedListenerCount++;
+ }
+ }
+ }
+
+ public void removeOnAccountListChangedListener() {
+ synchronized (this) {
+ this.accountChangedListenerCount--;
+ if (this.accountChangedListenerCount <= 0) {
+ this.mOnAccountUpdate = null;
+ this.accountChangedListenerCount = 0;
+ if (checkListeners()) {
+ switchToBackground();
+ }
+ }
+ }
+ }
+
+ public void setOnCaptchaRequestedListener(OnCaptchaRequested listener) {
+ synchronized (this) {
+ if (checkListeners()) {
+ switchToForeground();
+ }
+ this.mOnCaptchaRequested = listener;
+ if (this.captchaRequestedListenerCount < 2) {
+ this.captchaRequestedListenerCount++;
+ }
+ }
+ }
+
+ public void removeOnCaptchaRequestedListener() {
+ synchronized (this) {
+ this.captchaRequestedListenerCount--;
+ if (this.captchaRequestedListenerCount <= 0) {
+ this.mOnCaptchaRequested = null;
+ this.captchaRequestedListenerCount = 0;
+ if (checkListeners()) {
+ switchToBackground();
+ }
+ }
+ }
+ }
+
+ public void setOnRosterUpdateListener(final OnRosterUpdate listener) {
+ synchronized (this) {
+ if (checkListeners()) {
+ switchToForeground();
+ }
+ this.mOnRosterUpdate = listener;
+ if (this.rosterChangedListenerCount < 2) {
+ this.rosterChangedListenerCount++;
+ }
+ }
+ }
+
+ public void removeOnRosterUpdateListener() {
+ synchronized (this) {
+ this.rosterChangedListenerCount--;
+ if (this.rosterChangedListenerCount <= 0) {
+ this.rosterChangedListenerCount = 0;
+ this.mOnRosterUpdate = null;
+ if (checkListeners()) {
+ switchToBackground();
+ }
+ }
+ }
+ }
+
+ public void setOnUpdateBlocklistListener(final OnUpdateBlocklist listener) {
+ synchronized (this) {
+ if (checkListeners()) {
+ switchToForeground();
+ }
+ this.mOnUpdateBlocklist = listener;
+ if (this.updateBlocklistListenerCount < 2) {
+ this.updateBlocklistListenerCount++;
+ }
+ }
+ }
+
+ public void removeOnUpdateBlocklistListener() {
+ synchronized (this) {
+ this.updateBlocklistListenerCount--;
+ if (this.updateBlocklistListenerCount <= 0) {
+ this.updateBlocklistListenerCount = 0;
+ this.mOnUpdateBlocklist = null;
+ if (checkListeners()) {
+ switchToBackground();
+ }
+ }
+ }
+ }
+
+ public void setOnKeyStatusUpdatedListener(final OnKeyStatusUpdated listener) {
+ synchronized (this) {
+ if (checkListeners()) {
+ switchToForeground();
+ }
+ this.mOnKeyStatusUpdated = listener;
+ if (this.keyStatusUpdatedListenerCount < 2) {
+ this.keyStatusUpdatedListenerCount++;
+ }
+ }
+ }
+
+ public void removeOnNewKeysAvailableListener() {
+ synchronized (this) {
+ this.keyStatusUpdatedListenerCount--;
+ if (this.keyStatusUpdatedListenerCount <= 0) {
+ this.keyStatusUpdatedListenerCount = 0;
+ this.mOnKeyStatusUpdated = null;
+ if (checkListeners()) {
+ switchToBackground();
+ }
+ }
+ }
+ }
+
+ public void setOnMucRosterUpdateListener(OnMucRosterUpdate listener) {
+ synchronized (this) {
+ if (checkListeners()) {
+ switchToForeground();
+ }
+ this.mOnMucRosterUpdate = listener;
+ if (this.mucRosterChangedListenerCount < 2) {
+ this.mucRosterChangedListenerCount++;
+ }
+ }
+ }
+
+ public void removeOnMucRosterUpdateListener() {
+ synchronized (this) {
+ this.mucRosterChangedListenerCount--;
+ if (this.mucRosterChangedListenerCount <= 0) {
+ this.mucRosterChangedListenerCount = 0;
+ this.mOnMucRosterUpdate = null;
+ if (checkListeners()) {
+ switchToBackground();
+ }
+ }
+ }
+ }
+
+ private boolean checkListeners() {
+ return (this.mOnAccountUpdate == null
+ && this.mOnConversationUpdate == null
+ && this.mOnRosterUpdate == null
+ && this.mOnCaptchaRequested == null
+ && this.mOnUpdateBlocklist == null
+ && this.mOnShowErrorToast == null
+ && this.mOnKeyStatusUpdated == null);
+ }
+
+ private void switchToForeground() {
+ for (Conversation conversation : getConversations()) {
+ conversation.setIncomingChatState(ChatState.ACTIVE);
+ }
+ for (Account account : getAccounts()) {
+ if (account.getStatus() == Account.State.ONLINE) {
+ XmppConnection connection = account.getXmppConnection();
+ if (connection != null && connection.getFeatures().csi()) {
+ connection.sendActive();
+ }
+ }
+ }
+ Logging.d(Config.LOGTAG, "app switched into foreground");
+ }
+
+ private void switchToBackground() {
+ for (Account account : getAccounts()) {
+ if (account.getStatus() == Account.State.ONLINE) {
+ XmppConnection connection = account.getXmppConnection();
+ if (connection != null) {
+ if (connection.getFeatures().csi()) {
+ connection.sendInactive();
+ }
+ if (Config.CLOSE_TCP_WHEN_SWITCHING_TO_BACKGROUND && mPushManagementService.available(account)) {
+ connection.waitForPush();
+ cancelWakeUpCall(account.getUuid().hashCode());
+ }
+ }
+ }
+ }
+ this.mNotificationService.setIsInForeground(false);
+ Logging.d(Config.LOGTAG, "app switched into background");
+ }
+
+ private void connectMultiModeConversations(Account account) {
+ List<Conversation> conversations = getConversations();
+ for (Conversation conversation : conversations) {
+ if (conversation.getMode() == Conversation.MODE_MULTI && conversation.getAccount() == account) {
+ joinMuc(conversation);
+ }
+ }
+ }
+
+ public void joinMuc(Conversation conversation) {
+ joinMuc(conversation, null);
+ }
+
+ private void joinMuc(Conversation conversation, final OnConferenceJoined onConferenceJoined) {
+ Account account = conversation.getAccount();
+ account.pendingConferenceJoins.remove(conversation);
+ account.pendingConferenceLeaves.remove(conversation);
+ if (account.getStatus() == Account.State.ONLINE) {
+ conversation.resetMucOptions();
+ conversation.setHasMessagesLeftOnServer(false);
+ fetchConferenceConfiguration(conversation, new OnConferenceConfigurationFetched() {
+
+ private void join(Conversation conversation) {
+ Account account = conversation.getAccount();
+ final MucOptions mucOptions = conversation.getMucOptions();
+ final Jid joinJid = mucOptions.getSelf().getFullJid();
+ Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": joining conversation " + joinJid.toString());
+ PresencePacket packet = new PresencePacket();
+ packet.setFrom(conversation.getAccount().getJid());
+ packet.setTo(joinJid);
+ Element x = packet.addChild("x", "http://jabber.org/protocol/muc");
+ if (conversation.getMucOptions().getPassword() != null) {
+ x.addChild("password").setContent(conversation.getMucOptions().getPassword());
+ }
+
+ if (mucOptions.mamSupport()) {
+ // Use MAM instead of the limited muc history to get history
+ x.addChild("history").setAttribute("maxchars", "0");
+ } else {
+ // Fallback to muc history
+ x.addChild("history").setAttribute("since", PresenceGenerator.getTimestamp(conversation.getLastMessageTransmitted()));
+ }
+ String sig = account.getPgpSignature();
+ if (sig != null) {
+ packet.addChild("x", "jabber:x:signed").setContent(sig);
+ }
+ sendPresencePacket(account, packet);
+ if (onConferenceJoined != null) {
+ onConferenceJoined.onConferenceJoined(conversation);
+ }
+ if (!joinJid.equals(conversation.getJid())) {
+ conversation.setContactJid(joinJid);
+ databaseBackend.updateConversation(conversation);
+ }
+
+ if (mucOptions.mamSupport()) {
+ getMessageArchiveService().catchupMUC(conversation);
+ }
+ if (mucOptions.membersOnly() && mucOptions.nonanonymous()) {
+ fetchConferenceMembers(conversation);
+ }
+ sendUnsentMessages(conversation);
+ }
+
+ @Override
+ public void onConferenceConfigurationFetched(Conversation conversation) {
+ join(conversation);
+ }
+
+ @Override
+ public void onFetchFailed(final Conversation conversation, Element error) {
+ join(conversation);
+ fetchConferenceConfiguration(conversation);
+ }
+ });
+ updateConversationUi();
+ } else {
+ account.pendingConferenceJoins.add(conversation);
+ conversation.resetMucOptions();
+ conversation.setHasMessagesLeftOnServer(false);
+ updateConversationUi();
+ }
+ }
+
+ private void fetchConferenceMembers(final Conversation conversation) {
+ final Account account = conversation.getAccount();
+ final String[] affiliations = {"member","admin","owner"};
+ OnIqPacketReceived callback = new OnIqPacketReceived() {
+
+ private int i = 0;
+
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ Element query = packet.query("http://jabber.org/protocol/muc#admin");
+ if (packet.getType() == IqPacket.TYPE.RESULT && query != null) {
+ for(Element child : query.getChildren()) {
+ if ("item".equals(child.getName())) {
+ conversation.getMucOptions().putMember(child.getAttributeAsJid("jid"));
+ }
+ }
+ } else {
+ Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could not request affiliation "+affiliations[i]+" in "+conversation.getJid().toBareJid());
+ }
+ ++i;
+ if (i >= affiliations.length) {
+ Log.d(Config.LOGTAG,account.getJid().toBareJid()+": retrieved members for "+conversation.getJid().toBareJid()+": "+conversation.getMucOptions().getMembers());
+ }
+ }
+ };
+ for(String affiliation : affiliations) {
+ sendIqPacket(account, mIqGenerator.queryAffiliation(conversation, affiliation), callback);
+ }
+ Log.d(Config.LOGTAG,account.getJid().toBareJid()+": fetching members for "+conversation.getName());
+ }
+
+ public void providePasswordForMuc(Conversation conversation, String password) {
+ if (conversation.getMode() == Conversation.MODE_MULTI) {
+ conversation.getMucOptions().setPassword(password);
+ if (conversation.getBookmark() != null) {
+ conversation.getBookmark().setAutojoin(ConversationsPlusPreferences.autojoin());
+ pushBookmarks(conversation.getAccount());
+ }
+ databaseBackend.updateConversation(conversation);
+ joinMuc(conversation);
+ }
+ }
+
+ public void renameInMuc(final Conversation conversation, final String nick, final UiCallback<Conversation> callback) {
+ final MucOptions options = conversation.getMucOptions();
+ final Jid joinJid = options.createJoinJid(nick);
+ if (options.online()) {
+ Account account = conversation.getAccount();
+ options.setOnRenameListener(new OnRenameListener() {
+
+ @Override
+ public void onSuccess() {
+ conversation.setContactJid(joinJid);
+ databaseBackend.updateConversation(conversation);
+ Bookmark bookmark = conversation.getBookmark();
+ if (bookmark != null) {
+ bookmark.setNick(nick);
+ pushBookmarks(bookmark.getAccount());
+ }
+ callback.success(conversation);
+ }
+
+ @Override
+ public void onFailure() {
+ callback.error(R.string.nick_in_use, conversation);
+ }
+ });
+
+ PresencePacket packet = new PresencePacket();
+ packet.setTo(joinJid);
+ packet.setFrom(conversation.getAccount().getJid());
+
+ String sig = account.getPgpSignature();
+ if (sig != null) {
+ packet.addChild("status").setContent("online");
+ packet.addChild("x", "jabber:x:signed").setContent(sig);
+ }
+ sendPresencePacket(account, packet);
+ } else {
+ conversation.setContactJid(joinJid);
+ databaseBackend.updateConversation(conversation);
+ if (conversation.getAccount().getStatus() == Account.State.ONLINE) {
+ Bookmark bookmark = conversation.getBookmark();
+ if (bookmark != null) {
+ bookmark.setNick(nick);
+ pushBookmarks(bookmark.getAccount());
+ }
+ joinMuc(conversation);
+ }
+ }
+ }
+
+ public void leaveMuc(Conversation conversation) {
+ leaveMuc(conversation, false);
+ }
+
+ private void leaveMuc(Conversation conversation, boolean now) {
+ Account account = conversation.getAccount();
+ account.pendingConferenceJoins.remove(conversation);
+ account.pendingConferenceLeaves.remove(conversation);
+ if (account.getStatus() == Account.State.ONLINE || now) {
+ PresencePacket packet = new PresencePacket();
+ packet.setTo(conversation.getMucOptions().getSelf().getFullJid());
+ packet.setFrom(conversation.getAccount().getJid());
+ packet.setAttribute("type", "unavailable");
+ sendPresencePacket(conversation.getAccount(), packet);
+ conversation.getMucOptions().setOffline();
+ conversation.deregisterWithBookmark();
+ Logging.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid()
+ + ": leaving muc " + conversation.getJid());
+ } else {
+ account.pendingConferenceLeaves.add(conversation);
+ }
+ }
+
+ private String findConferenceServer(final Account account) {
+ String server;
+ if (account.getXmppConnection() != null) {
+ server = account.getXmppConnection().getMucServer();
+ if (server != null) {
+ return server;
+ }
+ }
+ for (Account other : getAccounts()) {
+ if (other != account && other.getXmppConnection() != null) {
+ server = other.getXmppConnection().getMucServer();
+ if (server != null) {
+ return server;
+ }
+ }
+ }
+ return null;
+ }
+
+ public void createAdhocConference(final Account account, final Iterable<Jid> jids, final UiCallback<Conversation> callback) {
+ Logging.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": creating adhoc conference with " + jids.toString());
+ if (account.getStatus() == Account.State.ONLINE) {
+ try {
+ String server = findConferenceServer(account);
+ if (server == null) {
+ if (callback != null) {
+ callback.error(R.string.no_conference_server_found, null);
+ }
+ return;
+ }
+ String name = new BigInteger(75, ConversationsPlusApplication.getSecureRandom()).toString(32);
+ Jid jid = Jid.fromParts(name, server, null);
+ final Conversation conversation = findOrCreateConversation(account, jid, true);
+ joinMuc(conversation, new OnConferenceJoined() {
+ @Override
+ public void onConferenceJoined(final Conversation conversation) {
+ Bundle options = new Bundle();
+ options.putString("muc#roomconfig_persistentroom", "1");
+ options.putString("muc#roomconfig_membersonly", "1");
+ options.putString("muc#roomconfig_publicroom", "0");
+ options.putString("muc#roomconfig_whois", "anyone");
+ pushConferenceConfiguration(conversation, options, new OnConferenceOptionsPushed() {
+ @Override
+ public void onPushSucceeded() {
+ for (Jid invite : jids) {
+ invite(conversation, invite);
+ }
+ if (account.countPresences() > 1) {
+ directInvite(conversation, account.getJid().toBareJid());
+ }
+ if (callback != null) {
+ callback.success(conversation);
+ }
+ }
+
+ @Override
+ public void onPushFailed() {
+ if (callback != null) {
+ callback.error(R.string.conference_creation_failed, conversation);
+ }
+ }
+ });
+ }
+ });
+ } catch (InvalidJidException e) {
+ if (callback != null) {
+ callback.error(R.string.conference_creation_failed, null);
+ }
+ }
+ } else {
+ if (callback != null) {
+ callback.error(R.string.not_connected_try_again, null);
+ }
+ }
+ }
+
+ public void fetchConferenceConfiguration(final Conversation conversation) {
+ fetchConferenceConfiguration(conversation, null);
+ }
+
+ public void fetchConferenceConfiguration(final Conversation conversation, final OnConferenceConfigurationFetched callback) {
+ IqPacket request = new IqPacket(IqPacket.TYPE.GET);
+ request.setTo(conversation.getJid().toBareJid());
+ request.query("http://jabber.org/protocol/disco#info");
+ sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ Element query = packet.findChild("query","http://jabber.org/protocol/disco#info");
+ if (packet.getType() == IqPacket.TYPE.RESULT && query != null) {
+ ArrayList<String> features = new ArrayList<>();
+ for (Element child : query.getChildren()) {
+ if (child != null && child.getName().equals("feature")) {
+ String var = child.getAttribute("var");
+ if (var != null) {
+ features.add(var);
+ }
+ }
+ }
+ Element form = query.findChild("x", "jabber:x:data");
+ if (form != null) {
+ conversation.getMucOptions().updateFormData(Data.parse(form));
+ }
+ conversation.getMucOptions().updateFeatures(features);
+ if (callback != null) {
+ callback.onConferenceConfigurationFetched(conversation);
+ }
+ Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": fetched muc configuration for " + conversation.getJid().toBareJid() + " - " + features.toString());
+ updateConversationUi();
+ } else if (packet.getType() == IqPacket.TYPE.ERROR) {
+ if (callback != null) {
+ callback.onFetchFailed(conversation, packet.getError());
+ }
+ }
+ }
+ });
+ }
+
+ public void pushConferenceConfiguration(final Conversation conversation, final Bundle options, final OnConferenceOptionsPushed callback) {
+ IqPacket request = new IqPacket(IqPacket.TYPE.GET);
+ request.setTo(conversation.getJid().toBareJid());
+ request.query("http://jabber.org/protocol/muc#owner");
+ sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE.RESULT) {
+ Data data = Data.parse(packet.query().findChild("x", "jabber:x:data"));
+ for (Field field : data.getFields()) {
+ if (options.containsKey(field.getFieldName())) {
+ field.setValue(options.getString(field.getFieldName()));
+ }
+ }
+ data.submit();
+ IqPacket set = new IqPacket(IqPacket.TYPE.SET);
+ set.setTo(conversation.getJid().toBareJid());
+ set.query("http://jabber.org/protocol/muc#owner").addChild(data);
+ sendIqPacket(account, set, new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ if (callback != null) {
+ if (packet.getType() == IqPacket.TYPE.RESULT) {
+ callback.onPushSucceeded();
+ } else {
+ callback.onPushFailed();
+ }
+ }
+ }
+ });
+ } else {
+ if (callback != null) {
+ callback.onPushFailed();
+ }
+ }
+ }
+ });
+ }
+
+ public void pushSubjectToConference(final Conversation conference, final String subject) {
+ MessagePacket packet = this.getMessageGenerator().conferenceSubject(conference, subject);
+ this.sendMessagePacket(conference.getAccount(), packet);
+ final MucOptions mucOptions = conference.getMucOptions();
+ final MucOptions.User self = mucOptions.getSelf();
+ if (!mucOptions.persistent() && self.getAffiliation().ranks(MucOptions.Affiliation.OWNER)) {
+ Bundle options = new Bundle();
+ options.putString("muc#roomconfig_persistentroom", "1");
+ this.pushConferenceConfiguration(conference, options, null);
+ }
+ }
+
+ public void changeAffiliationInConference(final Conversation conference, Jid user, MucOptions.Affiliation affiliation, final OnAffiliationChanged callback) {
+ final Jid jid = user.toBareJid();
+ IqPacket request = this.mIqGenerator.changeAffiliation(conference, jid, affiliation.toString());
+ sendIqPacket(conference.getAccount(), request, new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE.RESULT) {
+ callback.onAffiliationChangedSuccessful(jid);
+ } else {
+ callback.onAffiliationChangeFailed(jid, R.string.could_not_change_affiliation);
+ }
+ }
+ });
+ }
+
+ public void changeAffiliationsInConference(final Conversation conference, MucOptions.Affiliation before, MucOptions.Affiliation after) {
+ List<Jid> jids = new ArrayList<>();
+ for (MucOptions.User user : conference.getMucOptions().getUsers()) {
+ if (user.getAffiliation() == before && user.getJid() != null) {
+ jids.add(user.getJid());
+ }
+ }
+ IqPacket request = this.mIqGenerator.changeAffiliation(conference, jids, after.toString());
+ sendIqPacket(conference.getAccount(), request, mDefaultIqHandler);
+ }
+
+ public void changeRoleInConference(final Conversation conference, final String nick, MucOptions.Role role, final OnRoleChanged callback) {
+ IqPacket request = this.mIqGenerator.changeRole(conference, nick, role.toString());
+ Logging.d(Config.LOGTAG, request.toString());
+ sendIqPacket(conference.getAccount(), request, new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ Logging.d(Config.LOGTAG, packet.toString());
+ if (packet.getType() == IqPacket.TYPE.RESULT) {
+ callback.onRoleChangedSuccessful(nick);
+ } else {
+ callback.onRoleChangeFailed(nick, R.string.could_not_change_role);
+ }
+ }
+ });
+ }
+
+ private void disconnect(Account account, boolean force) {
+ if ((account.getStatus() == Account.State.ONLINE)
+ || (account.getStatus() == Account.State.DISABLED)) {
+ if (!force) {
+ List<Conversation> conversations = getConversations();
+ for (Conversation conversation : conversations) {
+ if (conversation.getAccount() == account) {
+ if (conversation.getMode() == Conversation.MODE_MULTI) {
+ leaveMuc(conversation, true);
+ } else {
+ if (conversation.endOtrIfNeeded()) {
+ Logging.d(Config.LOGTAG, account.getJid().toBareJid()
+ + ": ended otr session with "
+ + conversation.getJid());
+ }
+ }
+ }
+ }
+ sendOfflinePresence(account);
+ }
+ account.getXmppConnection().disconnect(force);
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+ public void updateMessage(Message message) {
+ databaseBackend.updateMessage(message);
+ updateConversationUi();
+ }
+
+ protected void syncDirtyContacts(Account account) {
+ for (Contact contact : account.getRoster().getContacts()) {
+ if (contact.getOption(Contact.Options.DIRTY_PUSH)) {
+ pushContactToServer(contact);
+ }
+ if (contact.getOption(Contact.Options.DIRTY_DELETE)) {
+ deleteContactOnServer(contact);
+ }
+ }
+ }
+
+ public void createContact(Contact contact) {
+ if (ConversationsPlusPreferences.grantNewContacts()) {
+ contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
+ contact.setOption(Contact.Options.ASKING);
+ }
+ pushContactToServer(contact);
+ }
+
+ public void onOtrSessionEstablished(Conversation conversation) {
+ final Account account = conversation.getAccount();
+ final Session otrSession = conversation.getOtrSession();
+ Logging.d(Config.LOGTAG,
+ account.getJid().toBareJid() + " otr session established with "
+ + conversation.getJid() + "/"
+ + otrSession.getSessionID().getUserID());
+ conversation.findUnsentMessagesWithEncryption(Message.ENCRYPTION_OTR, new Conversation.OnMessageFound() {
+
+ @Override
+ public void onMessageFound(Message message) {
+ SessionID id = otrSession.getSessionID();
+ try {
+ message.setCounterpart(Jid.fromString(id.getAccountID() + "/" + id.getUserID()));
+ } catch (InvalidJidException e) {
+ return;
+ }
+ if (message.needsUploading()) {
+ mJingleConnectionManager.createNewConnection(message);
+ } else {
+ MessagePacket outPacket = mMessageGenerator.generateOtrChat(message);
+ if (outPacket != null) {
+ mMessageGenerator.addDelay(outPacket, message.getTimeSent());
+ message.setStatus(Message.STATUS_SEND);
+ databaseBackend.updateMessage(message);
+ sendMessagePacket(account, outPacket);
+ }
+ }
+ updateConversationUi();
+ }
+ });
+ }
+
+ public boolean renewSymmetricKey(Conversation conversation) {
+ Account account = conversation.getAccount();
+ byte[] symmetricKey = new byte[32];
+ ConversationsPlusApplication.getSecureRandom().nextBytes(symmetricKey);
+ Session otrSession = conversation.getOtrSession();
+ if (otrSession != null) {
+ MessagePacket packet = new MessagePacket();
+ packet.setType(MessagePacket.TYPE_CHAT);
+ packet.setFrom(account.getJid());
+ MessageGenerator.addMessageHints(packet);
+ packet.setAttribute("to", otrSession.getSessionID().getAccountID() + "/"
+ + otrSession.getSessionID().getUserID());
+ try {
+ packet.setBody(otrSession
+ .transformSending(CryptoHelper.FILETRANSFER
+ + CryptoHelper.bytesToHex(symmetricKey))[0]);
+ sendMessagePacket(account, packet);
+ conversation.setSymmetricKey(symmetricKey);
+ return true;
+ } catch (OtrException e) {
+ return false;
+ }
+ }
+ return false;
+ }
+
+ public void pushContactToServer(final Contact contact) {
+ contact.resetOption(Contact.Options.DIRTY_DELETE);
+ contact.setOption(Contact.Options.DIRTY_PUSH);
+ final Account account = contact.getAccount();
+ if (account.getStatus() == Account.State.ONLINE) {
+ final boolean ask = contact.getOption(Contact.Options.ASKING);
+ final boolean sendUpdates = contact
+ .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)
+ && contact.getOption(Contact.Options.PREEMPTIVE_GRANT);
+ final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
+ iq.query(Xmlns.ROSTER).addChild(contact.asElement());
+ account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler);
+ if (sendUpdates) {
+ sendPresencePacket(account,
+ mPresenceGenerator.sendPresenceUpdatesTo(contact));
+ }
+ if (ask) {
+ sendPresencePacket(account,
+ mPresenceGenerator.requestPresenceUpdatesFrom(contact));
+ }
+ }
+ }
+
+ public void deleteContactOnServer(Contact contact) {
+ contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
+ contact.resetOption(Contact.Options.DIRTY_PUSH);
+ contact.setOption(Contact.Options.DIRTY_DELETE);
+ Account account = contact.getAccount();
+ if (account.getStatus() == Account.State.ONLINE) {
+ IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
+ Element item = iq.query(Xmlns.ROSTER).addChild("item");
+ item.setAttribute("jid", contact.getJid().toString());
+ item.setAttribute("subscription", "remove");
+ account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler);
+ }
+ }
+
+ public void updateConversation(Conversation conversation) {
+ this.databaseBackend.updateConversation(conversation);
+ }
+
+ private void reconnectAccount(final Account account, final boolean force, final boolean interactive) {
+ synchronized (account) {
+ XmppConnection connection = account.getXmppConnection();
+ if (connection == null) {
+ connection = createConnection(account);
+ account.setXmppConnection(connection);
+ }
+ if (!account.isOptionSet(Account.OPTION_DISABLED)) {
+ if (!force) {
+ disconnect(account, false);
+ }
+ Thread thread = new Thread(connection);
+ connection.setInteractive(interactive);
+ connection.prepareNewConnection();
+ thread.start();
+ scheduleWakeUpCall(Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode());
+ } else {
+ disconnect(account, force);
+ account.getRoster().clearPresences();
+ connection.resetEverything();
+ account.getAxolotlService().resetBrokenness();
+ }
+ }
+ }
+
+ public void reconnectAccountInBackground(final Account account) {
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ reconnectAccount(account, false, true);
+ }
+ }).start();
+ }
+
+ public void invite(Conversation conversation, Jid contact) {
+ Logging.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid() + ": inviting " + contact + " to " + conversation.getJid().toBareJid());
+ MessagePacket packet = mMessageGenerator.invite(conversation, contact);
+ sendMessagePacket(conversation.getAccount(), packet);
+ }
+
+ public void directInvite(Conversation conversation, Jid jid) {
+ MessagePacket packet = mMessageGenerator.directInvite(conversation, jid);
+ sendMessagePacket(conversation.getAccount(), packet);
+ }
+
+ public void resetSendingToWaiting(Account account) {
+ for (Conversation conversation : getConversations()) {
+ if (conversation.getAccount() == account) {
+ conversation.findUnsentTextMessages(new Conversation.OnMessageFound() {
+
+ @Override
+ public void onMessageFound(Message message) {
+ MessageUtil.markMessage(message, Message.STATUS_WAITING);
+ }
+ });
+ }
+ }
+ }
+
+ public Message markMessage(final Account account, final Jid recipient, final String uuid, final int status) {
+ if (uuid == null) {
+ return null;
+ }
+ for (Conversation conversation : getConversations()) {
+ if (conversation.getJid().toBareJid().equals(recipient) && conversation.getAccount() == account) {
+ final Message message = conversation.findSentMessageWithUuidOrRemoteId(uuid);
+ if (message != null) {
+ MessageUtil.markMessage(message, status);
+ }
+ return message;
+ }
+ }
+ return null;
+ }
+
+ public int unreadCount() {
+ int count = 0;
+ for (Conversation conversation : getConversations()) {
+ count += conversation.unreadCount();
+ }
+ return count;
+ }
+
+
+ public void showErrorToastInUi(int resId) {
+ if (mOnShowErrorToast != null) {
+ mOnShowErrorToast.onShowErrorToast(resId);
+ }
+ }
+
+ public void updateConversationUi() {
+ if (mOnConversationUpdate != null) {
+ mOnConversationUpdate.onConversationUpdate();
+ }
+ }
+
+ public void updateAccountUi() {
+ if (mOnAccountUpdate != null) {
+ mOnAccountUpdate.onAccountUpdate();
+ }
+ }
+
+ public void updateRosterUi() {
+ if (mOnRosterUpdate != null) {
+ mOnRosterUpdate.onRosterUpdate();
+ }
+ }
+
+ public boolean displayCaptchaRequest(Account account, String id, Data data, Bitmap captcha) {
+ boolean rc = false;
+ if (mOnCaptchaRequested != null) {
+ DisplayMetrics metrics = getApplicationContext().getResources().getDisplayMetrics();
+ Bitmap scaled = Bitmap.createScaledBitmap(captcha, (int) (captcha.getWidth() * metrics.scaledDensity),
+ (int) (captcha.getHeight() * metrics.scaledDensity), false);
+
+ mOnCaptchaRequested.onCaptchaRequested(account, id, data, scaled);
+ rc = true;
+ }
+
+ return rc;
+ }
+
+ public void updateBlocklistUi(final OnUpdateBlocklist.Status status) {
+ if (mOnUpdateBlocklist != null) {
+ mOnUpdateBlocklist.OnUpdateBlocklist(status);
+ }
+ }
+
+ public void updateMucRosterUi() {
+ if (mOnMucRosterUpdate != null) {
+ mOnMucRosterUpdate.onMucRosterUpdate();
+ }
+ }
+
+ public void keyStatusUpdated(AxolotlService.FetchStatus report) {
+ if (mOnKeyStatusUpdated != null) {
+ mOnKeyStatusUpdated.onKeyStatusUpdated(report);
+ }
+ }
+
+ public Account findAccountByJid(final Jid accountJid) {
+ for (Account account : this.accounts) {
+ if (account.getJid().toBareJid().equals(accountJid.toBareJid())) {
+ return account;
+ }
+ }
+ return null;
+ }
+
+ public Conversation findConversationByUuid(String uuid) {
+ for (Conversation conversation : getConversations()) {
+ if (conversation.getUuid().equals(uuid)) {
+ return conversation;
+ }
+ }
+ return null;
+ }
+
+ public boolean markRead(final Conversation conversation) {
+ mNotificationService.clear(conversation);
+ final List<Message> readMessages = conversation.markRead();
+ if (readMessages.size() > 0) {
+ Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ for (Message message : readMessages) {
+ databaseBackend.updateMessage(message);
+ }
+ }
+ };
+ ConversationsPlusApplication.executeDatabaseOperation(runnable);
+ updateUnreadCountBadge();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public synchronized void updateUnreadCountBadge() {
+ int count = unreadCount();
+ if (unreadCount != count) {
+ Logging.d(Config.LOGTAG, "update unread count to " + count);
+ if (count > 0) {
+ ShortcutBadger.applyCount(getApplicationContext(), count);
+ } else {
+ ShortcutBadger.removeCount(getApplicationContext());
+ }
+ unreadCount = count;
+ }
+ }
+
+ public void sendReadMarker(final Conversation conversation) {
+ final Message markable = conversation.getLatestMarkableMessage();
+ Logging.d("markRead", "XmppConnectionService.sendReadMarker (" + conversation.getName() + ")");
+ if (this.markRead(conversation)) {
+ updateConversationUi();
+ }
+ if (Settings.CONFIRM_MESSAGE_READ && markable != null && markable.getRemoteMsgId() != null) {
+ Logging.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid() + ": sending read marker to " + markable.getCounterpart().toString());
+ Account account = conversation.getAccount();
+ final Jid to = markable.getCounterpart();
+ MessagePacket packet = mMessageGenerator.confirm(account, to, markable.getRemoteMsgId());
+ this.sendMessagePacket(conversation.getAccount(), packet);
+ }
+ }
+
+ public void syncRosterToDisk(final Account account) {
+ Runnable runnable = new Runnable() {
+
+ @Override
+ public void run() {
+ databaseBackend.writeRoster(account.getRoster());
+ }
+ };
+ ConversationsPlusApplication.executeDatabaseOperation(runnable);
+ }
+
+ public List<String> getKnownHosts() {
+ final List<String> hosts = new ArrayList<>();
+ for (final Account account : getAccounts()) {
+ if (!hosts.contains(account.getServer().toString())) {
+ hosts.add(account.getServer().toString());
+ }
+ for (final Contact contact : account.getRoster().getContacts()) {
+ if (contact.showInRoster()) {
+ final String server = contact.getServer().toString();
+ if (server != null && !hosts.contains(server)) {
+ hosts.add(server);
+ }
+ }
+ }
+ }
+ return hosts;
+ }
+
+ public List<String> getKnownConferenceHosts() {
+ final ArrayList<String> mucServers = new ArrayList<>();
+ for (final Account account : accounts) {
+ if (account.getXmppConnection() != null) {
+ final String server = account.getXmppConnection().getMucServer();
+ if (server != null && !mucServers.contains(server)) {
+ mucServers.add(server);
+ }
+ }
+ }
+ return mucServers;
+ }
+
+ @Deprecated
+ public void sendMessagePacket(Account account, MessagePacket packet) {
+ XmppSendUtil.sendMessagePacket(account, packet);
+ }
+
+ @Deprecated
+ public void sendPresencePacket(Account account, PresencePacket packet) {
+ XmppSendUtil.sendPresencePacket(account, packet);
+ }
+
+ public void sendCreateAccountWithCaptchaPacket(Account account, String id, Data data) {
+ XmppConnection connection = account.getXmppConnection();
+ if (connection != null) {
+ connection.sendCaptchaRegistryRequest(id, data);
+ }
+ }
+
+ @Deprecated
+ public void sendIqPacket(final Account account, final IqPacket packet, final OnIqPacketReceived callback) {
+ XmppSendUtil.sendIqPacket(account, packet, callback);
+ }
+
+ public void sendPresence(final Account account) {
+ XmppSendUtil.sendPresencePacket(account, mPresenceGenerator.selfPresence(account, getTargetPresence()));
+ }
+
+ public void refreshAllPresences() {
+ for (Account account : getAccounts()) {
+ if (!account.isOptionSet(Account.OPTION_DISABLED)) {
+ sendPresence(account);
+ }
+ }
+ }
+
+ private void refreshAllGcmTokens() {
+ for(Account account : getAccounts()) {
+ if (account.isOnlineAndConnected() && mPushManagementService.available(account)) {
+ mPushManagementService.registerPushTokenOnServer(account);
+ }
+ }
+ }
+
+ public void sendOfflinePresence(final Account account) {
+ XmppSendUtil.sendPresencePacket(account, mPresenceGenerator.sendOfflinePresence(account));
+ }
+
+ public MessageGenerator getMessageGenerator() {
+ return this.mMessageGenerator;
+ }
+
+ public PresenceGenerator getPresenceGenerator() {
+ return this.mPresenceGenerator;
+ }
+
+ public IqGenerator getIqGenerator() {
+ return this.mIqGenerator;
+ }
+
+ public IqParser getIqParser() {
+ return this.mIqParser;
+ }
+
+ public JingleConnectionManager getJingleConnectionManager() {
+ return this.mJingleConnectionManager;
+ }
+
+ public MessageArchiveService getMessageArchiveService() {
+ return this.mMessageArchiveService;
+ }
+
+ public List<Contact> findContacts(Jid jid) {
+ ArrayList<Contact> contacts = new ArrayList<>();
+ for (Account account : getAccounts()) {
+ if (!account.isOptionSet(Account.OPTION_DISABLED)) {
+ Contact contact = account.getRoster().getContactFromRoster(jid);
+ if (contact != null) {
+ contacts.add(contact);
+ }
+ }
+ }
+ return contacts;
+ }
+
+ public NotificationService getNotificationService() {
+ return this.mNotificationService;
+ }
+
+ public void resendFailedMessages(final Message message) {
+ if (message.getStatus() == Message.STATUS_SEND_FAILED) {
+ message.setTime(System.currentTimeMillis());
+ MessageUtil.markMessage(message, Message.STATUS_WAITING);
+ this.resendMessage(message, false);
+ }
+ }
+
+ public void clearConversationHistory(final Conversation conversation) {
+ conversation.clearMessages();
+ /*
+ * In case the history was loaded completely before.
+ * The flag "hasMessagesLeftOnServer" is set to false and no messages will be loaded anymore
+ * Therefore set this flag to true and try to get messages from server
+ */
+ conversation.setHasMessagesLeftOnServer(true);
+ Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ databaseBackend.deleteMessagesInConversation(conversation);
+ }
+ };
+ ConversationsPlusApplication.executeDatabaseOperation(runnable);
+ }
+
+ public void sendBlockRequest(final Blockable blockable) {
+ if (blockable != null && blockable.getBlockedJid() != null) {
+ final Jid jid = blockable.getBlockedJid();
+ this.sendIqPacket(blockable.getAccount(), getIqGenerator().generateSetBlockRequest(jid), new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(final Account account, final IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE.RESULT) {
+ account.getBlocklist().add(jid);
+ updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
+ }
+ }
+ });
+ }
+ }
+
+ public void sendUnblockRequest(final Blockable blockable) {
+ if (blockable != null && blockable.getJid() != null) {
+ final Jid jid = blockable.getBlockedJid();
+ this.sendIqPacket(blockable.getAccount(), getIqGenerator().generateSetUnblockRequest(jid), new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(final Account account, final IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE.RESULT) {
+ account.getBlocklist().remove(jid);
+ updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED);
+ }
+ }
+ });
+ }
+ }
+
+ public void publishDisplayName(Account account) {
+ String displayName = account.getDisplayName();
+ if (displayName != null && !displayName.isEmpty()) {
+ IqPacket publish = mIqGenerator.publishNick(displayName);
+ sendIqPacket(account, publish, new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE.ERROR) {
+ Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": could not publish nick");
+ }
+ }
+ });
+ }
+ }
+
+ private ServiceDiscoveryResult getCachedServiceDiscoveryResult(Pair<String,String> key) {
+ ServiceDiscoveryResult result = discoCache.get(key);
+ if (result != null) {
+ return result;
+ } else {
+ result = databaseBackend.findDiscoveryResult(key.first, key.second);
+ if (result != null) {
+ discoCache.put(key, result);
+ }
+ return result;
+ }
+ }
+
+ public void fetchCaps(Account account, final Jid jid, final Presence presence) {
+ final Pair<String,String> key = new Pair<>(presence.getHash(), presence.getVer());
+ ServiceDiscoveryResult disco = getCachedServiceDiscoveryResult(key);
+ if (disco != null) {
+ presence.setServiceDiscoveryResult(disco);
+ } else {
+ if (!account.inProgressDiscoFetches.contains(key)) {
+ account.inProgressDiscoFetches.add(key);
+ IqPacket request = new IqPacket(IqPacket.TYPE.GET);
+ request.setTo(jid);
+ request.query("http://jabber.org/protocol/disco#info");
+ Log.d(Config.LOGTAG,account.getJid().toBareJid()+": making disco request for "+key.second+" to "+jid);
+ sendIqPacket(account, request, new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket discoPacket) {
+ if (discoPacket.getType() == IqPacket.TYPE.RESULT) {
+ ServiceDiscoveryResult disco = new ServiceDiscoveryResult(discoPacket);
+ if (presence.getVer().equals(disco.getVer())) {
+ databaseBackend.insertDiscoveryResult(disco);
+ injectServiceDiscorveryResult(account.getRoster(), presence.getHash(), presence.getVer(), disco);
+ } else {
+ Logging.d(Config.LOGTAG, account.getJid().toBareJid() + ": mismatch in caps for contact " + jid + " " + presence.getVer() + " vs " + disco.getVer());
+ }
+ }
+ account.inProgressDiscoFetches.remove(key);
+ }
+ });
+ }
+ }
+ }
+
+ private void injectServiceDiscorveryResult(Roster roster, String hash, String ver, ServiceDiscoveryResult disco) {
+ for(Contact contact : roster.getContacts()) {
+ for(Presence presence : contact.getPresences().getPresences().values()) {
+ if (hash.equals(presence.getHash()) && ver.equals(presence.getVer())) {
+ presence.setServiceDiscoveryResult(disco);
+ }
+ }
+ }
+ }
+
+ public void fetchMamPreferences(Account account, final OnMamPreferencesFetched callback) {
+ IqPacket request = new IqPacket(IqPacket.TYPE.GET);
+ request.addChild("prefs","urn:xmpp:mam:0");
+ sendIqPacket(account, request, new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ Element prefs = packet.findChild("prefs","urn:xmpp:mam:0");
+ if (packet.getType() == IqPacket.TYPE.RESULT && prefs != null) {
+ callback.onPreferencesFetched(prefs);
+ } else {
+ callback.onPreferencesFetchFailed();
+ }
+ }
+ });
+ }
+
+ public PushManagementService getPushManagementService() {
+ return mPushManagementService;
+ }
+
+ public interface OnMamPreferencesFetched {
+ void onPreferencesFetched(Element prefs);
+ void onPreferencesFetchFailed();
+ }
+
+ public void pushMamPreferences(Account account, Element prefs) {
+ IqPacket set = new IqPacket(IqPacket.TYPE.SET);
+ set.addChild(prefs);
+ sendIqPacket(account, set, null);
+ }
+
+ public interface OnAccountCreated {
+ void onAccountCreated(Account account);
+
+ void informUser(int r);
+ }
+
+ public interface OnMoreMessagesLoaded {
+ void onMoreMessagesLoaded(int count, Conversation conversation);
+
+ void informUser(int r);
+
+ void setLoadingInProgress();
+
+ boolean isLoadingInProgress();
+ }
+
+ public interface OnAccountPasswordChanged {
+ void onPasswordChangeSucceeded();
+
+ void onPasswordChangeFailed();
+ }
+
+ public interface OnAffiliationChanged {
+ void onAffiliationChangedSuccessful(Jid jid);
+
+ void onAffiliationChangeFailed(Jid jid, int resId);
+ }
+
+ public interface OnRoleChanged {
+ void onRoleChangedSuccessful(String nick);
+
+ void onRoleChangeFailed(String nick, int resid);
+ }
+
+ public interface OnConversationUpdate {
+ void onConversationUpdate();
+ }
+
+ public interface OnAccountUpdate {
+ void onAccountUpdate();
+ }
+
+ public interface OnCaptchaRequested {
+ void onCaptchaRequested(Account account,
+ String id,
+ Data data,
+ Bitmap captcha);
+ }
+
+ public interface OnRosterUpdate {
+ void onRosterUpdate();
+ }
+
+ public interface OnMucRosterUpdate {
+ void onMucRosterUpdate();
+ }
+
+ public interface OnConferenceConfigurationFetched {
+ void onConferenceConfigurationFetched(Conversation conversation);
+
+ void onFetchFailed(Conversation conversation, Element error);
+ }
+
+ public interface OnConferenceJoined {
+ void onConferenceJoined(Conversation conversation);
+ }
+
+ public interface OnConferenceOptionsPushed {
+ void onPushSucceeded();
+
+ void onPushFailed();
+ }
+
+ public interface OnShowErrorToast {
+ void onShowErrorToast(int resId);
+ }
+
+ public class XmppConnectionBinder extends Binder {
+ public XmppConnectionService getService() {
+ return XmppConnectionService.this;
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/AboutActivity.java b/src/main/java/eu/siacs/conversations/ui/AboutActivity.java
new file mode 100644
index 00000000..a61b872a
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/AboutActivity.java
@@ -0,0 +1,15 @@
+package eu.siacs.conversations.ui;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+import eu.siacs.conversations.R;
+
+public class AboutActivity extends Activity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_about);
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/AboutPreference.java b/src/main/java/eu/siacs/conversations/ui/AboutPreference.java
new file mode 100644
index 00000000..b9e5c367
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/AboutPreference.java
@@ -0,0 +1,32 @@
+package eu.siacs.conversations.ui;
+
+import android.content.Context;
+import android.content.Intent;
+import android.preference.Preference;
+import android.util.AttributeSet;
+
+import de.thedevstack.conversationsplus.ConversationsPlusApplication;
+
+public class AboutPreference extends Preference {
+ public AboutPreference(final Context context, final AttributeSet attrs, final int defStyle) {
+ super(context, attrs, defStyle);
+ setSummary();
+ }
+
+ public AboutPreference(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ setSummary();
+ }
+
+ @Override
+ protected void onClick() {
+ super.onClick();
+ final Intent intent = new Intent(getContext(), AboutActivity.class);
+ getContext().startActivity(intent);
+ }
+
+ private void setSummary() {
+ setSummary(ConversationsPlusApplication.getNameAndVersion());
+ }
+}
+
diff --git a/src/main/java/eu/siacs/conversations/ui/AbstractSearchableListItemActivity.java b/src/main/java/eu/siacs/conversations/ui/AbstractSearchableListItemActivity.java
new file mode 100644
index 00000000..1a9fc95c
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/AbstractSearchableListItemActivity.java
@@ -0,0 +1,124 @@
+package eu.siacs.conversations.ui;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.ListView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.ListItem;
+import eu.siacs.conversations.ui.adapter.ListItemAdapter;
+
+public abstract class AbstractSearchableListItemActivity extends XmppActivity {
+ private ListView mListView;
+ private final List<ListItem> listItems = new ArrayList<>();
+ private ArrayAdapter<ListItem> mListItemsAdapter;
+
+ private EditText mSearchEditText;
+
+ private final MenuItem.OnActionExpandListener mOnActionExpandListener = new MenuItem.OnActionExpandListener() {
+
+ @Override
+ public boolean onMenuItemActionExpand(final MenuItem item) {
+ mSearchEditText.post(new Runnable() {
+
+ @Override
+ public void run() {
+ mSearchEditText.requestFocus();
+ final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.showSoftInput(mSearchEditText,
+ InputMethodManager.SHOW_IMPLICIT);
+ }
+ });
+
+ return true;
+ }
+
+ @Override
+ public boolean onMenuItemActionCollapse(final MenuItem item) {
+ final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.hideSoftInputFromWindow(mSearchEditText.getWindowToken(),
+ InputMethodManager.HIDE_IMPLICIT_ONLY);
+ mSearchEditText.setText("");
+ filterContacts();
+ return true;
+ }
+ };
+
+ private final TextWatcher mSearchTextWatcher = new TextWatcher() {
+
+ @Override
+ public void afterTextChanged(final Editable editable) {
+ filterContacts(editable.toString());
+ }
+
+ @Override
+ public void beforeTextChanged(final CharSequence s, final int start, final int count,
+ final int after) {
+ }
+
+ @Override
+ public void onTextChanged(final CharSequence s, final int start, final int before,
+ final int count) {
+ }
+ };
+
+ public ListView getListView() {
+ return mListView;
+ }
+
+ public List<ListItem> getListItems() {
+ return listItems;
+ }
+
+ public EditText getSearchEditText() {
+ return mSearchEditText;
+ }
+
+ public ArrayAdapter<ListItem> getListItemAdapter() {
+ return mListItemsAdapter;
+ }
+
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_choose_contact);
+ mListView = (ListView) findViewById(R.id.choose_contact_list);
+ mListView.setFastScrollEnabled(true);
+ mListItemsAdapter = new ListItemAdapter(this, listItems);
+ mListView.setAdapter(mListItemsAdapter);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(final Menu menu) {
+ getMenuInflater().inflate(R.menu.choose_contact, menu);
+ final MenuItem menuSearchView = menu.findItem(R.id.action_search);
+ final View mSearchView = menuSearchView.getActionView();
+ mSearchEditText = (EditText) mSearchView
+ .findViewById(R.id.search_field);
+ mSearchEditText.addTextChangedListener(mSearchTextWatcher);
+ menuSearchView.setOnActionExpandListener(mOnActionExpandListener);
+ return true;
+ }
+
+ protected void filterContacts() {
+ filterContacts(null);
+ }
+
+ protected abstract void filterContacts(final String needle);
+
+ @Override
+ void onBackendConnected() {
+ filterContacts();
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/BlockContactDialog.java b/src/main/java/eu/siacs/conversations/ui/BlockContactDialog.java
new file mode 100644
index 00000000..9cf7e9f8
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/BlockContactDialog.java
@@ -0,0 +1,41 @@
+package eu.siacs.conversations.ui;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Blockable;
+import eu.siacs.conversations.services.XmppConnectionService;
+
+public final class BlockContactDialog {
+ public static void show(final Context context,
+ final XmppConnectionService xmppConnectionService,
+ final Blockable blockable) {
+ final AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ final boolean isBlocked = blockable.isBlocked();
+ builder.setNegativeButton(R.string.cancel, null);
+
+ if (blockable.getJid().isDomainJid() || blockable.getAccount().isBlocked(blockable.getJid().toDomainJid())) {
+ builder.setTitle(isBlocked ? R.string.action_unblock_domain : R.string.action_block_domain);
+ builder.setMessage(context.getResources().getString(isBlocked ? R.string.unblock_domain_text : R.string.block_domain_text,
+ blockable.getJid().toDomainJid()));
+ } else {
+ builder.setTitle(isBlocked ? R.string.action_unblock_contact : R.string.action_block_contact);
+ builder.setMessage(context.getResources().getString(isBlocked ? R.string.unblock_contact_text : R.string.block_contact_text,
+ blockable.getJid().toBareJid()));
+ }
+ builder.setPositiveButton(isBlocked ? R.string.unblock : R.string.block, new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(final DialogInterface dialog, final int which) {
+ if (isBlocked) {
+ xmppConnectionService.sendUnblockRequest(blockable);
+ } else {
+ xmppConnectionService.sendBlockRequest(blockable);
+ }
+ }
+ });
+ builder.create().show();
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/BlocklistActivity.java b/src/main/java/eu/siacs/conversations/ui/BlocklistActivity.java
new file mode 100644
index 00000000..5a85c17b
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/BlocklistActivity.java
@@ -0,0 +1,74 @@
+package eu.siacs.conversations.ui;
+
+import android.os.Bundle;
+import android.text.Editable;
+import android.view.View;
+import android.widget.AdapterView;
+
+import java.util.Collections;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+public class BlocklistActivity extends AbstractSearchableListItemActivity implements OnUpdateBlocklist {
+
+ private Account account = null;
+
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ getListView().setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
+
+ @Override
+ public boolean onItemLongClick(final AdapterView<?> parent,
+ final View view,
+ final int position,
+ final long id) {
+ BlockContactDialog.show(parent.getContext(), xmppConnectionService,(Contact) getListItems().get(position));
+ return true;
+ }
+ });
+ }
+
+ @Override
+ public void onBackendConnected() {
+ for (final Account account : xmppConnectionService.getAccounts()) {
+ if (account.getJid().toString().equals(getIntent().getStringExtra(EXTRA_ACCOUNT))) {
+ this.account = account;
+ break;
+ }
+ }
+ filterContacts();
+ }
+
+ @Override
+ protected void filterContacts(final String needle) {
+ getListItems().clear();
+ if (account != null) {
+ for (final Jid jid : account.getBlocklist()) {
+ final Contact contact = account.getRoster().getContact(jid);
+ if (contact.match(needle) && contact.isBlocked()) {
+ getListItems().add(contact);
+ }
+ }
+ Collections.sort(getListItems());
+ }
+ getListItemAdapter().notifyDataSetChanged();
+ }
+
+ protected void refreshUiReal() {
+ final Editable editable = getSearchEditText().getText();
+ if (editable != null) {
+ filterContacts(editable.toString());
+ } else {
+ filterContacts();
+ }
+ }
+
+ @Override
+ public void OnUpdateBlocklist(final OnUpdateBlocklist.Status status) {
+ refreshUi();
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/ChangePasswordActivity.java b/src/main/java/eu/siacs/conversations/ui/ChangePasswordActivity.java
new file mode 100644
index 00000000..9f4a4bc3
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/ChangePasswordActivity.java
@@ -0,0 +1,103 @@
+package eu.siacs.conversations.ui;
+
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.Toast;
+
+import de.thedevstack.conversationsplus.ConversationsPlusColors;
+import de.thedevstack.conversationsplus.utils.ui.TextViewUtil;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.services.XmppConnectionService;
+
+public class ChangePasswordActivity extends XmppActivity implements XmppConnectionService.OnAccountPasswordChanged {
+
+ private Button mChangePasswordButton;
+ private View.OnClickListener mOnChangePasswordButtonClicked = new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if (mAccount != null) {
+ final String currentPassword = mCurrentPassword.getText().toString();
+ final String newPassword = mNewPassword.getText().toString();
+ final String newPasswordConfirm = mNewPasswordConfirm.getText().toString();
+ if (!currentPassword.equals(mAccount.getPassword())) {
+ mCurrentPassword.requestFocus();
+ mCurrentPassword.setError(getString(R.string.account_status_unauthorized));
+ } else if (!newPassword.equals(newPasswordConfirm)) {
+ mNewPasswordConfirm.requestFocus();
+ mNewPasswordConfirm.setError(getString(R.string.passwords_do_not_match));
+ } else if (newPassword.isEmpty()) {
+ mNewPassword.requestFocus();
+ mNewPassword.setError(getString(R.string.password_should_not_be_empty));
+ } else if (newPassword.trim().isEmpty()) {
+ mNewPassword.requestFocus();
+ mNewPassword.setError(getString(R.string.password_should_not_contain_only_spaces));
+ } else {
+ mCurrentPassword.setError(null);
+ mNewPassword.setError(null);
+ mNewPasswordConfirm.setError(null);
+ xmppConnectionService.updateAccountPasswordOnServer(mAccount, newPassword, ChangePasswordActivity.this);
+ TextViewUtil.disable(mChangePasswordButton, ConversationsPlusColors.secondaryText(), R.string.updating);
+ }
+ }
+ }
+ };
+ private EditText mCurrentPassword;
+ private EditText mNewPassword;
+ private EditText mNewPasswordConfirm;
+ private Account mAccount;
+
+ @Override
+ void onBackendConnected() {
+ this.mAccount = extractAccount(getIntent());
+
+ }
+
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_change_password);
+ Button mCancelButton = (Button) findViewById(R.id.left_button);
+ mCancelButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ finish();
+ }
+ });
+ this.mChangePasswordButton = (Button) findViewById(R.id.right_button);
+ this.mChangePasswordButton.setOnClickListener(this.mOnChangePasswordButtonClicked);
+ this.mCurrentPassword = (EditText) findViewById(R.id.current_password);
+ this.mNewPassword = (EditText) findViewById(R.id.new_password);
+ this.mNewPasswordConfirm = (EditText) findViewById(R.id.new_password_confirm);
+ }
+
+ @Override
+ public void onPasswordChangeSucceeded() {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(ChangePasswordActivity.this,R.string.password_changed,Toast.LENGTH_LONG).show();
+ finish();
+ }
+ });
+ }
+
+ @Override
+ public void onPasswordChangeFailed() {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mNewPassword.setError(getString(R.string.could_not_change_password));
+ TextViewUtil.enable(mChangePasswordButton, ConversationsPlusColors.primaryText(), R.string.change_password);
+ }
+ });
+
+ }
+
+ public void refreshUiReal() {
+
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java b/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java
new file mode 100644
index 00000000..c5357a5e
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java
@@ -0,0 +1,223 @@
+package eu.siacs.conversations.ui;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.ActionMode;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.AbsListView.MultiChoiceModeListener;
+import android.widget.AdapterView;
+import android.widget.ListView;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.ListItem;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+public class ChooseContactActivity extends AbstractSearchableListItemActivity {
+ private List<String> mActivatedAccounts = new ArrayList<String>();
+ private List<String> mKnownHosts;
+
+ private Set<Contact> selected;
+ private Set<String> filterContacts;
+
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ filterContacts = new HashSet<>();
+ String[] contacts = getIntent().getStringArrayExtra("filter_contacts");
+ if (contacts != null) {
+ Collections.addAll(filterContacts, contacts);
+ }
+
+ if (getIntent().getBooleanExtra("multiple", false)) {
+ getListView().setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
+ getListView().setMultiChoiceModeListener(new MultiChoiceModeListener() {
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ return false;
+ }
+
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.hideSoftInputFromWindow(getSearchEditText().getWindowToken(),
+ InputMethodManager.HIDE_IMPLICIT_ONLY);
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.select_multiple, menu);
+ selected = new HashSet<Contact>();
+ return true;
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ switch(item.getItemId()) {
+ case R.id.selection_submit:
+ final Intent request = getIntent();
+ final Intent data = new Intent();
+ data.putExtra("conversation",
+ request.getStringExtra("conversation"));
+ String[] selection = getSelectedContactJids();
+ data.putExtra("contacts", selection);
+ data.putExtra("multiple", true);
+ setResult(RESULT_OK, data);
+ finish();
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
+ Contact item = (Contact) getListItems().get(position);
+ if (checked) {
+ selected.add(item);
+ } else {
+ selected.remove(item);
+ }
+ int numSelected = selected.size();
+ MenuItem selectButton = mode.getMenu().findItem(R.id.selection_submit);
+ String buttonText = getResources().getQuantityString(R.plurals.select_contact,
+ numSelected, numSelected);
+ selectButton.setTitle(buttonText);
+ }
+ });
+ }
+
+ getListView().setOnItemClickListener(new AdapterView.OnItemClickListener() {
+
+ @Override
+ public void onItemClick(final AdapterView<?> parent, final View view,
+ final int position, final long id) {
+ final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.hideSoftInputFromWindow(getSearchEditText().getWindowToken(),
+ InputMethodManager.HIDE_IMPLICIT_ONLY);
+ final Intent request = getIntent();
+ final Intent data = new Intent();
+ final ListItem mListItem = getListItems().get(position);
+ data.putExtra("contact", mListItem.getJid().toString());
+ String account = request.getStringExtra(EXTRA_ACCOUNT);
+ if (account == null && mListItem instanceof Contact) {
+ account = ((Contact) mListItem).getAccount().getJid().toBareJid().toString();
+ }
+ data.putExtra(EXTRA_ACCOUNT, account);
+ data.putExtra("conversation",
+ request.getStringExtra("conversation"));
+ data.putExtra("multiple", false);
+ setResult(RESULT_OK, data);
+ finish();
+ }
+ });
+
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(final Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ final Intent i = getIntent();
+ boolean showEnterJid = i != null && i.getBooleanExtra("show_enter_jid", false);
+ menu.findItem(R.id.action_create_contact).setVisible(showEnterJid);
+ return true;
+ }
+
+ protected void filterContacts(final String needle) {
+ getListItems().clear();
+ for (final Account account : xmppConnectionService.getAccounts()) {
+ if (account.getStatus() != Account.State.DISABLED) {
+ for (final Contact contact : account.getRoster().getContacts()) {
+ if (contact.showInRoster() &&
+ !filterContacts.contains(contact.getJid().toBareJid().toString())
+ && contact.match(needle)) {
+ getListItems().add(contact);
+ }
+ }
+ }
+ }
+ Collections.sort(getListItems());
+ getListItemAdapter().notifyDataSetChanged();
+ }
+
+ private String[] getSelectedContactJids() {
+ List<String> result = new ArrayList<>();
+ for (Contact contact : selected) {
+ result.add(contact.getJid().toString());
+ }
+ return result.toArray(new String[result.size()]);
+ }
+
+
+ public void refreshUiReal() {
+ //nothing to do. This Activity doesn't implement any listeners
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.action_create_contact:
+ showEnterJidDialog();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ protected void showEnterJidDialog() {
+ EnterJidDialog dialog = new EnterJidDialog(
+ this, mKnownHosts, mActivatedAccounts,
+ getString(R.string.enter_contact), getString(R.string.select),
+ null, getIntent().getStringExtra(EXTRA_ACCOUNT), true
+ );
+
+ dialog.setOnEnterJidDialogPositiveListener(new EnterJidDialog.OnEnterJidDialogPositiveListener() {
+ @Override
+ public boolean onEnterJidDialogPositive(Jid accountJid, Jid contactJid) throws EnterJidDialog.JidError {
+ final Intent request = getIntent();
+ final Intent data = new Intent();
+ data.putExtra("contact", contactJid.toString());
+ data.putExtra(EXTRA_ACCOUNT, accountJid.toString());
+ data.putExtra("conversation",
+ request.getStringExtra("conversation"));
+ data.putExtra("multiple", false);
+ setResult(RESULT_OK, data);
+ finish();
+
+ return true;
+ }
+ });
+
+ dialog.show();
+ }
+
+ @Override
+ void onBackendConnected() {
+ filterContacts();
+
+ this.mActivatedAccounts.clear();
+ for (Account account : xmppConnectionService.getAccounts()) {
+ if (account.getStatus() != Account.State.DISABLED) {
+ if (Config.DOMAIN_LOCK != null) {
+ this.mActivatedAccounts.add(account.getJid().getLocalpart());
+ } else {
+ this.mActivatedAccounts.add(account.getJid().toBareJid().toString());
+ }
+ }
+ }
+ this.mKnownHosts = xmppConnectionService.getKnownHosts();
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java
new file mode 100644
index 00000000..eac2c1b8
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java
@@ -0,0 +1,714 @@
+package eu.siacs.conversations.ui;
+
+import android.annotation.TargetApi;
+import android.app.AlertDialog;
+import android.app.PendingIntent;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.IntentSender.SendIntentException;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TableLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import org.openintents.openpgp.util.OpenPgpUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import de.thedevstack.conversationsplus.ConversationsPlusPreferences;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.crypto.PgpEngine;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Bookmark;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.MucOptions;
+import eu.siacs.conversations.entities.MucOptions.User;
+import eu.siacs.conversations.services.AvatarService;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.services.XmppConnectionService.OnConversationUpdate;
+import eu.siacs.conversations.services.XmppConnectionService.OnMucRosterUpdate;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+public class ConferenceDetailsActivity extends XmppActivity implements OnConversationUpdate, OnMucRosterUpdate, XmppConnectionService.OnAffiliationChanged, XmppConnectionService.OnRoleChanged, XmppConnectionService.OnConferenceOptionsPushed {
+ public static final String ACTION_VIEW_MUC = "view_muc";
+ private Conversation mConversation;
+ private OnClickListener inviteListener = new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ inviteToConversation(mConversation);
+ }
+ };
+ private TextView mYourNick;
+ private ImageView mYourPhoto;
+ private ImageButton mEditNickButton;
+ private TextView mRoleAffiliaton;
+ private TextView mFullJid;
+ private TextView mAccountJid;
+ private LinearLayout membersView;
+ private LinearLayout mMoreDetails;
+ private TextView mConferenceType;
+ private TableLayout mConferenceInfoTable;
+ private TextView mConferenceInfoMam;
+ private TextView mNotifyStatusText;
+ private ImageButton mChangeConferenceSettingsButton;
+ private ImageButton mNotifyStatusButton;
+ private Button mInviteButton;
+ private String uuid = null;
+ private User mSelectedUser = null;
+
+ private boolean mAdvancedMode = false;
+
+ private UiCallback<Conversation> renameCallback = new UiCallback<Conversation>() {
+ @Override
+ public void success(Conversation object) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(ConferenceDetailsActivity.this,getString(R.string.your_nick_has_been_changed),Toast.LENGTH_SHORT).show();
+ updateView();
+ }
+ });
+
+ }
+
+ @Override
+ public void error(final int errorCode, Conversation object) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(ConferenceDetailsActivity.this,getString(errorCode),Toast.LENGTH_SHORT).show();
+ }
+ });
+ }
+
+ @Override
+ public void userInputRequried(PendingIntent pi, Conversation object) {
+
+ }
+ };
+
+ private OnClickListener mNotifyStatusClickListener = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(ConferenceDetailsActivity.this);
+ builder.setTitle(R.string.pref_notification_settings);
+ String[] choices = {
+ getString(R.string.notify_on_all_messages),
+ getString(R.string.notify_only_when_highlighted),
+ getString(R.string.notify_never)
+ };
+ final AtomicInteger choice;
+ if (mConversation.getLongAttribute(Conversation.ATTRIBUTE_MUTED_TILL,0) == Long.MAX_VALUE) {
+ choice = new AtomicInteger(2);
+ } else {
+ choice = new AtomicInteger(mConversation.alwaysNotify() ? 0 : 1);
+ }
+ builder.setSingleChoiceItems(choices, choice.get(), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ choice.set(which);
+ }
+ });
+ builder.setNegativeButton(R.string.cancel, null);
+ builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (choice.get() == 2) {
+ mConversation.setMutedTill(Long.MAX_VALUE);
+ } else {
+ mConversation.setMutedTill(0);
+ mConversation.setAttribute(Conversation.ATTRIBUTE_ALWAYS_NOTIFY,String.valueOf(choice.get() == 0));
+ }
+ xmppConnectionService.updateConversation(mConversation);
+ updateView();
+ }
+ });
+ builder.create().show();
+ }
+ };
+
+ private OnClickListener mChangeConferenceSettings = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ final MucOptions mucOptions = mConversation.getMucOptions();
+ AlertDialog.Builder builder = new AlertDialog.Builder(ConferenceDetailsActivity.this);
+ builder.setTitle(R.string.conference_options);
+ final String[] options;
+ final boolean[] values;
+ if (mAdvancedMode) {
+ options = new String[]{
+ getString(R.string.members_only),
+ getString(R.string.moderated),
+ getString(R.string.non_anonymous)
+ };
+ values = new boolean[]{
+ mucOptions.membersOnly(),
+ mucOptions.moderated(),
+ mucOptions.nonanonymous()
+ };
+ } else {
+ options = new String[]{
+ getString(R.string.members_only),
+ getString(R.string.non_anonymous)
+ };
+ values = new boolean[]{
+ mucOptions.membersOnly(),
+ mucOptions.nonanonymous()
+ };
+ }
+ builder.setMultiChoiceItems(options,values,new DialogInterface.OnMultiChoiceClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which, boolean isChecked) {
+ values[which] = isChecked;
+ }
+ });
+ builder.setNegativeButton(R.string.cancel, null);
+ builder.setPositiveButton(R.string.confirm,new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (!mucOptions.membersOnly() && values[0]) {
+ xmppConnectionService.changeAffiliationsInConference(mConversation,
+ MucOptions.Affiliation.NONE,
+ MucOptions.Affiliation.MEMBER);
+ }
+ Bundle options = new Bundle();
+ options.putString("muc#roomconfig_membersonly", values[0] ? "1" : "0");
+ if (values.length == 2) {
+ options.putString("muc#roomconfig_whois", values[1] ? "anyone" : "moderators");
+ } else if (values.length == 3) {
+ options.putString("muc#roomconfig_moderatedroom", values[1] ? "1" : "0");
+ options.putString("muc#roomconfig_whois", values[2] ? "anyone" : "moderators");
+ }
+ options.putString("muc#roomconfig_persistentroom", "1");
+ xmppConnectionService.pushConferenceConfiguration(mConversation,
+ options,
+ ConferenceDetailsActivity.this);
+ }
+ });
+ builder.create().show();
+ }
+ };
+ private OnValueEdited onSubjectEdited = new OnValueEdited() {
+
+ @Override
+ public void onValueEdited(String value) {
+ xmppConnectionService.pushSubjectToConference(mConversation,value);
+ }
+ };
+
+ @Override
+ public void onConversationUpdate() {
+ refreshUi();
+ }
+
+ @Override
+ public void onMucRosterUpdate() {
+ refreshUi();
+ }
+
+ @Override
+ protected void refreshUiReal() {
+ updateView();
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_muc_details);
+ mYourNick = (TextView) findViewById(R.id.muc_your_nick);
+ mYourPhoto = (ImageView) findViewById(R.id.your_photo);
+ mEditNickButton = (ImageButton) findViewById(R.id.edit_nick_button);
+ mFullJid = (TextView) findViewById(R.id.muc_jabberid);
+ membersView = (LinearLayout) findViewById(R.id.muc_members);
+ mAccountJid = (TextView) findViewById(R.id.details_account);
+ mMoreDetails = (LinearLayout) findViewById(R.id.muc_more_details);
+ mMoreDetails.setVisibility(View.GONE);
+ mChangeConferenceSettingsButton = (ImageButton) findViewById(R.id.change_conference_button);
+ mChangeConferenceSettingsButton.setOnClickListener(this.mChangeConferenceSettings);
+ mInviteButton = (Button) findViewById(R.id.invite);
+ mInviteButton.setOnClickListener(inviteListener);
+ mConferenceType = (TextView) findViewById(R.id.muc_conference_type);
+ if (getActionBar() != null) {
+ getActionBar().setHomeButtonEnabled(true);
+ getActionBar().setDisplayHomeAsUpEnabled(true);
+ }
+ mEditNickButton.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ quickEdit(mConversation.getMucOptions().getActualNick(),
+ new OnValueEdited() {
+
+ @Override
+ public void onValueEdited(String value) {
+ xmppConnectionService.renameInMuc(mConversation,value,renameCallback);
+ }
+ });
+ }
+ });
+ this.mAdvancedMode = ConversationsPlusPreferences.advancedMucMode();
+ this.mConferenceInfoTable = (TableLayout) findViewById(R.id.muc_info_more);
+ mConferenceInfoTable.setVisibility(this.mAdvancedMode ? View.VISIBLE : View.GONE);
+ this.mConferenceInfoMam = (TextView) findViewById(R.id.muc_info_mam);
+ this.mNotifyStatusButton = (ImageButton) findViewById(R.id.notification_status_button);
+ this.mNotifyStatusButton.setOnClickListener(this.mNotifyStatusClickListener);
+ this.mNotifyStatusText = (TextView) findViewById(R.id.notification_status_text);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem menuItem) {
+ switch (menuItem.getItemId()) {
+ case android.R.id.home:
+ finish();
+ break;
+ case R.id.action_edit_subject:
+ if (mConversation != null) {
+ quickEdit(mConversation.getName(),this.onSubjectEdited);
+ }
+ break;
+ case R.id.action_share:
+ share();
+ break;
+ case R.id.action_save_as_bookmark:
+ saveAsBookmark();
+ break;
+ case R.id.action_delete_bookmark:
+ deleteBookmark();
+ break;
+ case R.id.action_advanced_mode:
+ this.mAdvancedMode = !menuItem.isChecked();
+ menuItem.setChecked(this.mAdvancedMode);
+ ConversationsPlusPreferences.commitAdvancedMucMode(mAdvancedMode);
+ mConferenceInfoTable.setVisibility(this.mAdvancedMode ? View.VISIBLE : View.GONE);
+ invalidateOptionsMenu();
+ updateView();
+ break;
+ }
+ return super.onOptionsItemSelected(menuItem);
+ }
+
+ @Override
+ protected String getShareableUri() {
+ if (mConversation != null) {
+ return "xmpp:" + mConversation.getJid().toBareJid().toString() + "?join";
+ } else {
+ return "";
+ }
+ }
+
+ private void share() {
+ Intent shareIntent = new Intent();
+ shareIntent.setAction(Intent.ACTION_SEND);
+ shareIntent.putExtra(Intent.EXTRA_TEXT, getShareableUri());
+ 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();
+ }
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ MenuItem menuItemSaveBookmark = menu.findItem(R.id.action_save_as_bookmark);
+ MenuItem menuItemDeleteBookmark = menu.findItem(R.id.action_delete_bookmark);
+ MenuItem menuItemAdvancedMode = menu.findItem(R.id.action_advanced_mode);
+ MenuItem menuItemChangeSubject = menu.findItem(R.id.action_edit_subject);
+ menuItemAdvancedMode.setChecked(mAdvancedMode);
+ if (mConversation == null) {
+ return true;
+ }
+ Account account = mConversation.getAccount();
+ if (account.hasBookmarkFor(mConversation.getJid().toBareJid())) {
+ menuItemSaveBookmark.setVisible(false);
+ menuItemDeleteBookmark.setVisible(true);
+ } else {
+ menuItemDeleteBookmark.setVisible(false);
+ menuItemSaveBookmark.setVisible(true);
+ }
+ menuItemChangeSubject.setVisible(mConversation.getMucOptions().canChangeSubject());
+ return true;
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.muc_details, menu);
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
+ Object tag = v.getTag();
+ if (tag instanceof User) {
+ getMenuInflater().inflate(R.menu.muc_details_context,menu);
+ final User user = (User) tag;
+ final User self = mConversation.getMucOptions().getSelf();
+ this.mSelectedUser = user;
+ String name;
+ final Contact contact = user.getContact();
+ if (contact != null) {
+ name = contact.getDisplayName();
+ } else if (user.getJid() != null){
+ name = user.getJid().toBareJid().toString();
+ } else {
+ name = user.getName();
+ }
+ menu.setHeaderTitle(name);
+ if (user.getJid() != null) {
+ MenuItem showContactDetails = menu.findItem(R.id.action_contact_details);
+ MenuItem startConversation = menu.findItem(R.id.start_conversation);
+ MenuItem giveMembership = menu.findItem(R.id.give_membership);
+ MenuItem removeMembership = menu.findItem(R.id.remove_membership);
+ MenuItem giveAdminPrivileges = menu.findItem(R.id.give_admin_privileges);
+ MenuItem removeAdminPrivileges = menu.findItem(R.id.remove_admin_privileges);
+ MenuItem removeFromRoom = menu.findItem(R.id.remove_from_room);
+ MenuItem banFromConference = menu.findItem(R.id.ban_from_conference);
+ startConversation.setVisible(true);
+ if (contact != null) {
+ showContactDetails.setVisible(true);
+ }
+ if (self.getAffiliation().ranks(MucOptions.Affiliation.ADMIN) &&
+ self.getAffiliation().outranks(user.getAffiliation())) {
+ if (mAdvancedMode) {
+ if (user.getAffiliation() == MucOptions.Affiliation.NONE) {
+ giveMembership.setVisible(true);
+ } else {
+ removeMembership.setVisible(true);
+ }
+ banFromConference.setVisible(true);
+ } else {
+ removeFromRoom.setVisible(true);
+ }
+ if (user.getAffiliation() != MucOptions.Affiliation.ADMIN) {
+ giveAdminPrivileges.setVisible(true);
+ } else {
+ removeAdminPrivileges.setVisible(true);
+ }
+ }
+ } else {
+ MenuItem sendPrivateMessage = menu.findItem(R.id.send_private_message);
+ sendPrivateMessage.setVisible(true);
+ }
+
+ }
+ super.onCreateContextMenu(menu, v, menuInfo);
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.action_contact_details:
+ Contact contact = mSelectedUser.getContact();
+ if (contact != null) {
+ switchToContactDetails(contact);
+ }
+ return true;
+ case R.id.start_conversation:
+ startConversation(mSelectedUser);
+ return true;
+ case R.id.give_admin_privileges:
+ xmppConnectionService.changeAffiliationInConference(mConversation,mSelectedUser.getJid(), MucOptions.Affiliation.ADMIN,this);
+ return true;
+ case R.id.give_membership:
+ xmppConnectionService.changeAffiliationInConference(mConversation,mSelectedUser.getJid(), MucOptions.Affiliation.MEMBER,this);
+ return true;
+ case R.id.remove_membership:
+ xmppConnectionService.changeAffiliationInConference(mConversation,mSelectedUser.getJid(), MucOptions.Affiliation.NONE,this);
+ return true;
+ case R.id.remove_admin_privileges:
+ xmppConnectionService.changeAffiliationInConference(mConversation,mSelectedUser.getJid(), MucOptions.Affiliation.MEMBER,this);
+ return true;
+ case R.id.remove_from_room:
+ removeFromRoom(mSelectedUser);
+ return true;
+ case R.id.ban_from_conference:
+ xmppConnectionService.changeAffiliationInConference(mConversation,mSelectedUser.getJid(), MucOptions.Affiliation.OUTCAST,this);
+ xmppConnectionService.changeRoleInConference(mConversation,mSelectedUser.getName(), MucOptions.Role.NONE,this);
+ return true;
+ case R.id.send_private_message:
+ privateMsgInMuc(mConversation,mSelectedUser.getName());
+ return true;
+ default:
+ return super.onContextItemSelected(item);
+ }
+ }
+
+ private void removeFromRoom(final User user) {
+ if (mConversation.getMucOptions().membersOnly()) {
+ xmppConnectionService.changeAffiliationInConference(mConversation,user.getJid(), MucOptions.Affiliation.NONE,this);
+ xmppConnectionService.changeRoleInConference(mConversation,mSelectedUser.getName(), MucOptions.Role.NONE,ConferenceDetailsActivity.this);
+ } else {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(R.string.ban_from_conference);
+ builder.setMessage(getString(R.string.removing_from_public_conference,user.getName()));
+ builder.setNegativeButton(R.string.cancel,null);
+ builder.setPositiveButton(R.string.ban_now,new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ xmppConnectionService.changeAffiliationInConference(mConversation,user.getJid(), MucOptions.Affiliation.OUTCAST,ConferenceDetailsActivity.this);
+ xmppConnectionService.changeRoleInConference(mConversation,mSelectedUser.getName(), MucOptions.Role.NONE,ConferenceDetailsActivity.this);
+ }
+ });
+ builder.create().show();
+ }
+ }
+
+ protected void startConversation(User user) {
+ if (user.getJid() != null) {
+ Conversation conversation = xmppConnectionService.findOrCreateConversation(this.mConversation.getAccount(),user.getJid().toBareJid(),false);
+ switchToConversation(conversation);
+ }
+ }
+
+ protected void saveAsBookmark() {
+ Account account = mConversation.getAccount();
+ Bookmark bookmark = new Bookmark(account, mConversation.getJid().toBareJid());
+ if (!mConversation.getJid().isBareJid()) {
+ bookmark.setNick(mConversation.getJid().getResourcepart());
+ }
+ bookmark.setBookmarkName(mConversation.getMucOptions().getSubject());
+ bookmark.setAutojoin(ConversationsPlusPreferences.autojoin());
+ account.getBookmarks().add(bookmark);
+ xmppConnectionService.pushBookmarks(account);
+ mConversation.setBookmark(bookmark);
+ }
+
+ protected void deleteBookmark() {
+ Account account = mConversation.getAccount();
+ Bookmark bookmark = mConversation.getBookmark();
+ bookmark.unregisterConversation();
+ account.getBookmarks().remove(bookmark);
+ xmppConnectionService.pushBookmarks(account);
+ }
+
+ @Override
+ void onBackendConnected() {
+ if (mPendingConferenceInvite != null) {
+ mPendingConferenceInvite.execute(this);
+ mPendingConferenceInvite = null;
+ }
+ if (getIntent().getAction().equals(ACTION_VIEW_MUC)) {
+ this.uuid = getIntent().getExtras().getString("uuid");
+ }
+ if (uuid != null) {
+ this.mConversation = xmppConnectionService
+ .findConversationByUuid(uuid);
+ if (this.mConversation != null) {
+ updateView();
+ }
+ }
+ }
+
+ private void updateView() {
+ final MucOptions mucOptions = mConversation.getMucOptions();
+ final User self = mucOptions.getSelf();
+ String account;
+ if (Config.DOMAIN_LOCK != null) {
+ account = mConversation.getAccount().getJid().getLocalpart();
+ } else {
+ account = mConversation.getAccount().getJid().toBareJid().toString();
+ }
+ mAccountJid.setText(getString(R.string.using_account, account));
+ mYourPhoto.setImageBitmap(AvatarService.getInstance().get(mConversation.getAccount(), getPixel(48)));
+ setTitle(mConversation.getName());
+ if (Config.LOCK_DOMAINS_IN_CONVERSATIONS && mConversation.getJid().getDomainpart().equals(Config.CONFERENCE_DOMAIN_LOCK)) {
+ mFullJid.setText(mConversation.getJid().getLocalpart());
+ } else {
+ mFullJid.setText(mConversation.getJid().toBareJid().toString());
+ }
+ mYourNick.setText(mucOptions.getActualNick());
+ mRoleAffiliaton = (TextView) findViewById(R.id.muc_role);
+ if (mucOptions.online()) {
+ mMoreDetails.setVisibility(View.VISIBLE);
+ final String status = getStatus(self);
+ if (status != null) {
+ mRoleAffiliaton.setVisibility(View.VISIBLE);
+ mRoleAffiliaton.setText(status);
+ } else {
+ mRoleAffiliaton.setVisibility(View.GONE);
+ }
+ if (mucOptions.membersOnly()) {
+ mConferenceType.setText(R.string.private_conference);
+ } else {
+ mConferenceType.setText(R.string.public_conference);
+ }
+ if (mucOptions.mamSupport()) {
+ mConferenceInfoMam.setText(R.string.server_info_available);
+ } else {
+ mConferenceInfoMam.setText(R.string.server_info_unavailable);
+ }
+ if (self.getAffiliation().ranks(MucOptions.Affiliation.OWNER)) {
+ mChangeConferenceSettingsButton.setVisibility(View.VISIBLE);
+ } else {
+ mChangeConferenceSettingsButton.setVisibility(View.GONE);
+ }
+ }
+
+ long mutedTill = mConversation.getLongAttribute(Conversation.ATTRIBUTE_MUTED_TILL,0);
+ if (mutedTill == Long.MAX_VALUE) {
+ mNotifyStatusText.setText(R.string.notify_never);
+ mNotifyStatusButton.setImageResource(R.drawable.ic_notifications_off_grey600_24dp);
+ } else if (System.currentTimeMillis() < mutedTill) {
+ mNotifyStatusText.setText(R.string.notify_paused);
+ mNotifyStatusButton.setImageResource(R.drawable.ic_notifications_paused_grey600_24dp);
+ } else if (mConversation.alwaysNotify()) {
+ mNotifyStatusButton.setImageResource(R.drawable.ic_notifications_grey600_24dp);
+ mNotifyStatusText.setText(R.string.notify_on_all_messages);
+ } else {
+ mNotifyStatusButton.setImageResource(R.drawable.ic_notifications_none_grey600_24dp);
+ mNotifyStatusText.setText(R.string.notify_only_when_highlighted);
+ }
+
+ LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ membersView.removeAllViews();
+ final ArrayList<User> users = mucOptions.getUsers();
+ Collections.sort(users,new Comparator<User>() {
+ @Override
+ public int compare(User lhs, User rhs) {
+ return lhs.getName().compareToIgnoreCase(rhs.getName());
+ }
+ });
+ for (final User user : users) {
+ View view = inflater.inflate(R.layout.contact, membersView,false);
+ this.setListItemBackgroundOnView(view);
+ view.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ highlightInMuc(mConversation, user.getName());
+ }
+ });
+ registerForContextMenu(view);
+ view.setTag(user);
+ TextView tvDisplayName = (TextView) view.findViewById(R.id.contact_display_name);
+ TextView tvKey = (TextView) view.findViewById(R.id.key);
+ TextView tvStatus = (TextView) view.findViewById(R.id.contact_jid);
+ if (mAdvancedMode && user.getPgpKeyId() != 0) {
+ tvKey.setVisibility(View.VISIBLE);
+ tvKey.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ viewPgpKey(user);
+ }
+ });
+ tvKey.setText(OpenPgpUtils.convertKeyIdToHex(user.getPgpKeyId()));
+ }
+ Contact contact = user.getContact();
+ if (contact != null) {
+ tvDisplayName.setText(contact.getDisplayName());
+ tvStatus.setText(user.getName() + " \u2022 " + getStatus(user));
+ } else {
+ tvDisplayName.setText(user.getName());
+ tvStatus.setText(getStatus(user));
+
+ }
+ ImageView iv = (ImageView) view.findViewById(R.id.contact_photo);
+ iv.setImageBitmap(AvatarService.getInstance().get(user, getPixel(48), false));
+ membersView.addView(view);
+ if (mConversation.getMucOptions().canInvite()) {
+ mInviteButton.setVisibility(View.VISIBLE);
+ } else {
+ mInviteButton.setVisibility(View.GONE);
+ }
+ }
+ }
+
+ private String getStatus(User user) {
+ if (mAdvancedMode) {
+ StringBuilder builder = new StringBuilder();
+ builder.append(getString(user.getAffiliation().getResId()));
+ builder.append(" (");
+ builder.append(getString(user.getRole().getResId()));
+ builder.append(')');
+ return builder.toString();
+ } else {
+ return getString(user.getAffiliation().getResId());
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ private void setListItemBackgroundOnView(View view) {
+ int sdk = android.os.Build.VERSION.SDK_INT;
+ if (sdk < android.os.Build.VERSION_CODES.JELLY_BEAN) {
+ view.setBackgroundDrawable(getResources().getDrawable(R.drawable.greybackground));
+ } else {
+ view.setBackground(getResources().getDrawable(R.drawable.greybackground));
+ }
+ }
+
+ private void viewPgpKey(User user) {
+ PgpEngine pgp = xmppConnectionService.getPgpEngine();
+ if (pgp != null) {
+ PendingIntent intent = pgp.getIntentForKey(
+ mConversation.getAccount(), user.getPgpKeyId());
+ if (intent != null) {
+ try {
+ startIntentSenderForResult(intent.getIntentSender(), 0,
+ null, 0, 0, 0);
+ } catch (SendIntentException ignored) {
+
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onAffiliationChangedSuccessful(Jid jid) {
+
+ }
+
+ @Override
+ public void onAffiliationChangeFailed(Jid jid, int resId) {
+ displayToast(getString(resId,jid.toBareJid().toString()));
+ }
+
+ @Override
+ public void onRoleChangedSuccessful(String nick) {
+
+ }
+
+ @Override
+ public void onRoleChangeFailed(String nick, int resId) {
+ displayToast(getString(resId,nick));
+ }
+
+ @Override
+ public void onPushSucceeded() {
+ displayToast(getString(R.string.modified_conference_options));
+ }
+
+ @Override
+ public void onPushFailed() {
+ displayToast(getString(R.string.could_not_modify_conference_options));
+ }
+
+ private void displayToast(final String msg) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(ConferenceDetailsActivity.this,msg,Toast.LENGTH_SHORT).show();
+ }
+ });
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java
new file mode 100644
index 00000000..b11564a9
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java
@@ -0,0 +1,529 @@
+package eu.siacs.conversations.ui;
+
+import android.app.AlertDialog;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.IntentSender.SendIntentException;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Intents;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.QuickContactBadge;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import org.openintents.openpgp.util.OpenPgpUtils;
+
+import java.security.cert.X509Certificate;
+import java.util.List;
+
+import de.thedevstack.conversationsplus.ConversationsPlusPreferences;
+import de.thedevstack.conversationsplus.ui.listeners.ShowResourcesListDialogListener;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.crypto.PgpEngine;
+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.Contact;
+import eu.siacs.conversations.entities.ListItem;
+import eu.siacs.conversations.services.AvatarService;
+import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
+import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate;
+import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.utils.UIHelper;
+import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
+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.Jid;
+
+public class ContactDetailsActivity extends XmppActivity implements OnAccountUpdate, OnRosterUpdate, OnUpdateBlocklist, OnKeyStatusUpdated {
+ public static final String ACTION_VIEW_CONTACT = "view_contact";
+
+ private Contact contact;
+ private DialogInterface.OnClickListener removeFromRoster = new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ xmppConnectionService.deleteContactOnServer(contact);
+ }
+ };
+ private OnCheckedChangeListener mOnSendCheckedChange = new OnCheckedChangeListener() {
+
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView,
+ boolean isChecked) {
+ if (isChecked) {
+ if (contact
+ .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
+ xmppConnectionService.sendPresencePacket(contact
+ .getAccount(),
+ xmppConnectionService.getPresenceGenerator()
+ .sendPresenceUpdatesTo(contact));
+ } else {
+ contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
+ }
+ } else {
+ contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
+ xmppConnectionService.sendPresencePacket(contact.getAccount(),
+ xmppConnectionService.getPresenceGenerator()
+ .stopPresenceUpdatesTo(contact));
+ }
+ }
+ };
+ private OnCheckedChangeListener mOnReceiveCheckedChange = new OnCheckedChangeListener() {
+
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView,
+ boolean isChecked) {
+ if (isChecked) {
+ xmppConnectionService.sendPresencePacket(contact.getAccount(),
+ xmppConnectionService.getPresenceGenerator()
+ .requestPresenceUpdatesFrom(contact));
+ } else {
+ xmppConnectionService.sendPresencePacket(contact.getAccount(),
+ xmppConnectionService.getPresenceGenerator()
+ .stopPresenceUpdatesFrom(contact));
+ }
+ }
+ };
+ private Jid accountJid;
+ private Jid contactJid;
+ private TextView contactJidTv;
+ private TextView accountJidTv;
+ private TextView lastseen;
+ private CheckBox send;
+ private CheckBox receive;
+ private Button addContactButton;
+ private QuickContactBadge badge;
+ private LinearLayout keys;
+ private LinearLayout tags;
+ private boolean showDynamicTags;
+ private String messageFingerprint;
+
+ private DialogInterface.OnClickListener addToPhonebook = new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
+ intent.setType(Contacts.CONTENT_ITEM_TYPE);
+ intent.putExtra(Intents.Insert.IM_HANDLE, contact.getJid().toString());
+ intent.putExtra(Intents.Insert.IM_PROTOCOL,
+ CommonDataKinds.Im.PROTOCOL_JABBER);
+ intent.putExtra("finishActivityOnSaveCompleted", true);
+ ContactDetailsActivity.this.startActivityForResult(intent, 0);
+ }
+ };
+
+ private OnClickListener onBadgeClick = new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (contact.getSystemAccount() == null) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(
+ ContactDetailsActivity.this);
+ builder.setTitle(getString(R.string.action_add_phone_book));
+ builder.setMessage(getString(R.string.add_phone_book_text,
+ contact.getDisplayJid()));
+ builder.setNegativeButton(getString(R.string.cancel), null);
+ builder.setPositiveButton(getString(R.string.add), addToPhonebook);
+ builder.create().show();
+ } else {
+ String[] systemAccount = contact.getSystemAccount().split("#");
+ long id = Long.parseLong(systemAccount[0]);
+ Uri uri = ContactsContract.Contacts.getLookupUri(id, systemAccount[1]);
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(uri);
+ startActivity(intent);
+ }
+ }
+ };
+
+ @Override
+ public void onRosterUpdate() {
+ refreshUi();
+ }
+
+ @Override
+ public void onAccountUpdate() {
+ refreshUi();
+ }
+
+ @Override
+ public void OnUpdateBlocklist(final Status status) {
+ refreshUi();
+ }
+
+ @Override
+ protected void refreshUiReal() {
+ invalidateOptionsMenu();
+ populateView();
+ }
+
+ @Override
+ protected String getShareableUri() {
+ if (contact != null) {
+ return contact.getShareableUri();
+ } else {
+ return "";
+ }
+ }
+
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (getIntent().getAction().equals(ACTION_VIEW_CONTACT)) {
+ try {
+ this.accountJid = Jid.fromString(getIntent().getExtras().getString(EXTRA_ACCOUNT));
+ } catch (final InvalidJidException ignored) {
+ }
+ try {
+ this.contactJid = Jid.fromString(getIntent().getExtras().getString("contact"));
+ } catch (final InvalidJidException ignored) {
+ }
+ }
+ this.messageFingerprint = getIntent().getStringExtra("fingerprint");
+ setContentView(R.layout.activity_contact_details);
+
+ contactJidTv = (TextView) findViewById(R.id.details_contactjid);
+ accountJidTv = (TextView) findViewById(R.id.details_account);
+ lastseen = (TextView) findViewById(R.id.details_lastseen);
+ send = (CheckBox) findViewById(R.id.details_send_presence);
+ receive = (CheckBox) findViewById(R.id.details_receive_presence);
+ badge = (QuickContactBadge) findViewById(R.id.details_contact_badge);
+ addContactButton = (Button) findViewById(R.id.add_contact_button);
+ addContactButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ showAddToRosterDialog(contact);
+ }
+ });
+ keys = (LinearLayout) findViewById(R.id.details_contact_keys);
+ tags = (LinearLayout) findViewById(R.id.tags);
+ if (getActionBar() != null) {
+ getActionBar().setHomeButtonEnabled(true);
+ getActionBar().setDisplayHomeAsUpEnabled(true);
+ }
+
+ this.showDynamicTags = ConversationsPlusPreferences.showDynamicTags();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem menuItem) {
+ final AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setNegativeButton(getString(R.string.cancel), null);
+ switch (menuItem.getItemId()) {
+ case android.R.id.home:
+ finish();
+ break;
+ case R.id.action_delete_contact:
+ builder.setTitle(getString(R.string.action_delete_contact))
+ .setMessage(
+ getString(R.string.remove_contact_text,
+ contact.getDisplayJid()))
+ .setPositiveButton(getString(R.string.delete),
+ removeFromRoster).create().show();
+ break;
+ case R.id.action_edit_contact:
+ if (contact.getSystemAccount() == null) {
+ quickEdit(contact.getDisplayName(), new OnValueEdited() {
+
+ @Override
+ public void onValueEdited(String value) {
+ contact.setServerName(value);
+ ContactDetailsActivity.this.xmppConnectionService
+ .pushContactToServer(contact);
+ populateView();
+ }
+ });
+ } else {
+ Intent intent = new Intent(Intent.ACTION_EDIT);
+ String[] systemAccount = contact.getSystemAccount().split("#");
+ long id = Long.parseLong(systemAccount[0]);
+ Uri uri = Contacts.getLookupUri(id, systemAccount[1]);
+ intent.setDataAndType(uri, Contacts.CONTENT_ITEM_TYPE);
+ intent.putExtra("finishActivityOnSaveCompleted", true);
+ startActivity(intent);
+ }
+ break;
+ case R.id.action_block:
+ BlockContactDialog.show(this, xmppConnectionService, contact);
+ break;
+ case R.id.action_unblock:
+ BlockContactDialog.show(this, xmppConnectionService, contact);
+ break;
+ }
+ return super.onOptionsItemSelected(menuItem);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(final Menu menu) {
+ getMenuInflater().inflate(R.menu.contact_details, menu);
+ MenuItem block = menu.findItem(R.id.action_block);
+ MenuItem unblock = menu.findItem(R.id.action_unblock);
+ MenuItem edit = menu.findItem(R.id.action_edit_contact);
+ MenuItem delete = menu.findItem(R.id.action_delete_contact);
+ if (contact == null) {
+ return true;
+ }
+ final XmppConnection connection = contact.getAccount().getXmppConnection();
+ if (connection != null && connection.getFeatures().blocking()) {
+ if (this.contact.isBlocked()) {
+ block.setVisible(false);
+ } else {
+ unblock.setVisible(false);
+ }
+ } else {
+ unblock.setVisible(false);
+ block.setVisible(false);
+ }
+ if (!contact.showInRoster()) {
+ edit.setVisible(false);
+ delete.setVisible(false);
+ }
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ private void populateView() {
+ invalidateOptionsMenu();
+ setTitle(contact.getDisplayName());
+ if (contact.showInRoster()) {
+ send.setVisibility(View.VISIBLE);
+ receive.setVisibility(View.VISIBLE);
+ addContactButton.setVisibility(View.GONE);
+ send.setOnCheckedChangeListener(null);
+ receive.setOnCheckedChangeListener(null);
+
+ if (contact.getOption(Contact.Options.FROM)) {
+ send.setText(R.string.send_presence_updates);
+ send.setChecked(true);
+ } else if (contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
+ send.setChecked(false);
+ send.setText(R.string.send_presence_updates);
+ } else {
+ send.setText(R.string.preemptively_grant);
+ if (contact.getOption(Contact.Options.PREEMPTIVE_GRANT)) {
+ send.setChecked(true);
+ } else {
+ send.setChecked(false);
+ }
+ }
+ if (contact.getOption(Contact.Options.TO)) {
+ receive.setText(R.string.receive_presence_updates);
+ receive.setChecked(true);
+ } else {
+ receive.setText(R.string.ask_for_presence_updates);
+ if (contact.getOption(Contact.Options.ASKING)) {
+ receive.setChecked(true);
+ } else {
+ receive.setChecked(false);
+ }
+ }
+ if (contact.getAccount().isOnlineAndConnected()) {
+ receive.setEnabled(true);
+ send.setEnabled(true);
+ } else {
+ receive.setEnabled(false);
+ send.setEnabled(false);
+ }
+
+ send.setOnCheckedChangeListener(this.mOnSendCheckedChange);
+ receive.setOnCheckedChangeListener(this.mOnReceiveCheckedChange);
+ } else {
+ addContactButton.setVisibility(View.VISIBLE);
+ send.setVisibility(View.GONE);
+ receive.setVisibility(View.GONE);
+ }
+
+ if (contact.isBlocked() && !this.showDynamicTags) {
+ lastseen.setText(R.string.contact_blocked);
+ } else {
+ lastseen.setText(UIHelper.lastseen(getApplicationContext(), contact.lastseen.time));
+ }
+
+ if (contact.getPresences().size() > 1) {
+ contactJidTv.setText(contact.getDisplayJid() + " ("
+ + contact.getPresences().size() + ")");
+ } else {
+ contactJidTv.setText(contact.getDisplayJid());
+ }
+ String account;
+ if (Config.DOMAIN_LOCK != null) {
+ account = contact.getAccount().getJid().getLocalpart();
+ } else {
+ account = contact.getAccount().getJid().toBareJid().toString();
+ }
+ contactJidTv.setOnClickListener(new ShowResourcesListDialogListener(ContactDetailsActivity.this, contact));
+ accountJidTv.setText(getString(R.string.using_account, account));
+ badge.setImageBitmap(AvatarService.getInstance().get(contact, getPixel(72)));
+ badge.setOnClickListener(this.onBadgeClick);
+
+ keys.removeAllViews();
+ boolean hasKeys = false;
+ LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ for(final String otrFingerprint : contact.getOtrFingerprints()) {
+ hasKeys = true;
+ View view = inflater.inflate(R.layout.contact_key, keys, false);
+ TextView key = (TextView) view.findViewById(R.id.key);
+ TextView keyType = (TextView) view.findViewById(R.id.key_type);
+ ImageButton removeButton = (ImageButton) view
+ .findViewById(R.id.button_remove);
+ removeButton.setVisibility(View.VISIBLE);
+ keyType.setText("OTR Fingerprint");
+ key.setText(CryptoHelper.prettifyFingerprint(otrFingerprint));
+ keys.addView(view);
+ removeButton.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ confirmToDeleteFingerprint(otrFingerprint);
+ }
+ });
+ }
+ for (final String fingerprint : contact.getAccount().getAxolotlService().getFingerprintsForContact(contact)) {
+ boolean highlight = fingerprint.equals(messageFingerprint);
+ hasKeys |= addFingerprintRow(keys, contact.getAccount(), fingerprint, highlight, new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ onOmemoKeyClicked(contact.getAccount(), fingerprint);
+ }
+ });
+ }
+ if (contact.getPgpKeyId() != 0) {
+ hasKeys = true;
+ View view = inflater.inflate(R.layout.contact_key, keys, false);
+ TextView key = (TextView) view.findViewById(R.id.key);
+ TextView keyType = (TextView) view.findViewById(R.id.key_type);
+ keyType.setText("PGP Key ID");
+ key.setText(OpenPgpUtils.convertKeyIdToHex(contact.getPgpKeyId()));
+ view.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ PgpEngine pgp = ContactDetailsActivity.this.xmppConnectionService
+ .getPgpEngine();
+ if (pgp != null) {
+ PendingIntent intent = pgp.getIntentForKey(contact);
+ if (intent != null) {
+ try {
+ startIntentSenderForResult(
+ intent.getIntentSender(), 0, null, 0,
+ 0, 0);
+ } catch (SendIntentException e) {
+
+ }
+ }
+ }
+ }
+ });
+ keys.addView(view);
+ }
+ if (hasKeys) {
+ keys.setVisibility(View.VISIBLE);
+ } else {
+ keys.setVisibility(View.GONE);
+ }
+
+ List<ListItem.Tag> tagList = contact.getTags();
+ if (tagList.size() == 0 || !this.showDynamicTags) {
+ tags.setVisibility(View.GONE);
+ } else {
+ tags.setVisibility(View.VISIBLE);
+ tags.removeAllViewsInLayout();
+ for(final ListItem.Tag tag : tagList) {
+ final TextView tv = (TextView) inflater.inflate(R.layout.list_item_tag,tags,false);
+ tv.setText(tag.getName());
+ tv.setBackgroundColor(tag.getColor());
+ tags.addView(tv);
+ }
+ }
+ }
+
+ 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) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(R.string.delete_fingerprint);
+ builder.setMessage(R.string.sure_delete_fingerprint);
+ builder.setNegativeButton(R.string.cancel, null);
+ builder.setPositiveButton(R.string.delete,
+ new android.content.DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (contact.deleteOtrFingerprint(fingerprint)) {
+ populateView();
+ xmppConnectionService.syncRosterToDisk(contact.getAccount());
+ }
+ }
+
+ });
+ builder.create().show();
+ }
+
+ @Override
+ public void onBackendConnected() {
+ if ((accountJid != null) && (contactJid != null)) {
+ Account account = xmppConnectionService
+ .findAccountByJid(accountJid);
+ if (account == null) {
+ return;
+ }
+ this.contact = account.getRoster().getContact(contactJid);
+ populateView();
+ }
+ }
+
+ @Override
+ public void onKeyStatusUpdated(AxolotlService.FetchStatus report) {
+ refreshUi();
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java
new file mode 100644
index 00000000..4466dd22
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java
@@ -0,0 +1,1619 @@
+package eu.siacs.conversations.ui;
+
+import android.annotation.SuppressLint;
+import android.app.ActionBar;
+import android.app.AlertDialog;
+import android.app.FragmentTransaction;
+import android.app.PendingIntent;
+import android.content.ClipData;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.content.IntentSender.SendIntentException;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.provider.MediaStore;
+import android.provider.Settings;
+import android.support.v4.widget.SlidingPaneLayout;
+import android.support.v4.widget.SlidingPaneLayout.PanelSlideListener;
+import android.util.Log;
+import android.util.Pair;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.Surface;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.CheckBox;
+import android.widget.PopupMenu;
+import android.widget.PopupMenu.OnMenuItemClickListener;
+import android.widget.Toast;
+
+import net.java.otr4j.session.SessionStatus;
+
+import org.openintents.openpgp.util.OpenPgpApi;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import de.thedevstack.android.logcat.Logging;
+import de.thedevstack.conversationsplus.ConversationsPlusPreferences;
+import de.thedevstack.conversationsplus.ui.dialogs.UserDecisionDialog;
+import de.thedevstack.conversationsplus.ui.listeners.ResizePictureUserDecisionListener;
+import de.thedevstack.conversationsplus.utils.ConversationUtil;
+import de.timroes.android.listview.EnhancedListView;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.crypto.axolotl.AxolotlService;
+import eu.siacs.conversations.crypto.axolotl.AxolotlServiceImpl;
+import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Blockable;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.Transferable;
+import eu.siacs.conversations.http.HttpConnectionManager;
+import eu.siacs.conversations.http.HttpDownloadConnection;
+import eu.siacs.conversations.persistance.FileBackend;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
+import eu.siacs.conversations.services.XmppConnectionService.OnConversationUpdate;
+import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate;
+import eu.siacs.conversations.ui.adapter.ConversationAdapter;
+import eu.siacs.conversations.utils.ExceptionHelper;
+import eu.siacs.conversations.utils.FileUtils;
+import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
+import eu.siacs.conversations.xmpp.jid.InvalidJidException;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+public class ConversationActivity extends XmppActivity
+ implements OnAccountUpdate, OnConversationUpdate, OnRosterUpdate, OnUpdateBlocklist, XmppConnectionService.OnShowErrorToast {
+
+ public static final String ACTION_DOWNLOAD = "eu.siacs.conversations.action.DOWNLOAD";
+
+ public static final String VIEW_CONVERSATION = "viewConversation";
+ public static final String CONVERSATION = "conversationUuid";
+ public static final String MESSAGE = "messageUuid";
+ public static final String TEXT = "text";
+ public static final String NICK = "nick";
+ public static final String PRIVATE_MESSAGE = "pm";
+
+ public static final int REQUEST_SEND_MESSAGE = 0x0201;
+ public static final int REQUEST_DECRYPT_PGP = 0x0202;
+ public static final int REQUEST_ENCRYPT_MESSAGE = 0x0207;
+ public static final int REQUEST_TRUST_KEYS_TEXT = 0x0208;
+ public static final int REQUEST_TRUST_KEYS_MENU = 0x0209;
+ public static final int REQUEST_START_DOWNLOAD = 0x0210;
+ public static final int ATTACHMENT_CHOICE_CHOOSE_IMAGE = 0x0301;
+ public static final int ATTACHMENT_CHOICE_TAKE_PHOTO = 0x0302;
+ public static final int ATTACHMENT_CHOICE_CHOOSE_FILE = 0x0303;
+ public static final int ATTACHMENT_CHOICE_RECORD_VOICE = 0x0304;
+ public static final int ATTACHMENT_CHOICE_LOCATION = 0x0305;
+ public static final int ATTACHMENT_CHOICE_INVALID = 0x0306;
+ private static final String STATE_OPEN_CONVERSATION = "state_open_conversation";
+ private static final String STATE_PANEL_OPEN = "state_panel_open";
+ private static final String STATE_PENDING_URI = "state_pending_uri";
+
+ private String mOpenConverstaion = null;
+ private boolean mPanelOpen = true;
+ final private List<Uri> mPendingImageUris = new ArrayList<>();
+ final private List<Uri> mPendingFileUris = new ArrayList<>();
+ private Uri mPendingGeoUri = null;
+ private boolean forbidProcessingPendings = false;
+ private Message mPendingDownloadableMessage = null;
+
+ private boolean conversationWasSelectedByKeyboard = false;
+ protected boolean mUsingEnterKey = false;
+
+ private View mContentView;
+
+ private List<Conversation> conversationList = new ArrayList<>();
+ private Conversation swipedConversation = null;
+ private Conversation mSelectedConversation = null;
+ private EnhancedListView listView;
+ private ConversationFragment mConversationFragment;
+
+ private ArrayAdapter<Conversation> listAdapter;
+
+ private boolean mActivityPaused = false;
+ private AtomicBoolean mRedirected = new AtomicBoolean(false);
+ private Pair<Integer, Intent> mPostponedActivityResult;
+
+ public Conversation getSelectedConversation() {
+ return this.mSelectedConversation;
+ }
+
+ public void setSelectedConversation(Conversation conversation) {
+ this.mSelectedConversation = conversation;
+ }
+
+ public void showConversationsOverview() {
+ if (mContentView instanceof SlidingPaneLayout) {
+ SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView;
+ mSlidingPaneLayout.openPane();
+ }
+ }
+
+ @Override
+ protected String getShareableUri() {
+ Conversation conversation = getSelectedConversation();
+ if (conversation != null) {
+ return conversation.getAccount().getShareableUri();
+ } else {
+ return "";
+ }
+ }
+
+ public void hideConversationsOverview() {
+ if (mContentView instanceof SlidingPaneLayout) {
+ SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView;
+ mSlidingPaneLayout.closePane();
+ }
+ }
+
+ public boolean isConversationsOverviewHideable() {
+ if (mContentView instanceof SlidingPaneLayout) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public boolean isConversationsOverviewVisable() {
+ if (mContentView instanceof SlidingPaneLayout) {
+ SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView;
+ return mSlidingPaneLayout.isOpen();
+ } else {
+ return true;
+ }
+ }
+
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (savedInstanceState != null) {
+ mOpenConverstaion = savedInstanceState.getString(STATE_OPEN_CONVERSATION, null);
+ mPanelOpen = savedInstanceState.getBoolean(STATE_PANEL_OPEN, true);
+ String pending = savedInstanceState.getString(STATE_PENDING_URI, null);
+ if (pending != null) {
+ mPendingImageUris.clear();
+ mPendingImageUris.add(Uri.parse(pending));
+ }
+ }
+ this.mUsingEnterKey = ConversationsPlusPreferences.displayEnterKey();
+
+ setContentView(R.layout.fragment_conversations_overview);
+
+ this.mConversationFragment = new ConversationFragment();
+ FragmentTransaction transaction = getFragmentManager().beginTransaction();
+ transaction.replace(R.id.selected_conversation, this.mConversationFragment, "conversation");
+ transaction.commit();
+
+ listView = (EnhancedListView) findViewById(R.id.list);
+ this.listAdapter = new ConversationAdapter(this, conversationList);
+ listView.setAdapter(this.listAdapter);
+
+ if (getActionBar() != null) {
+ getActionBar().setDisplayHomeAsUpEnabled(false);
+ getActionBar().setHomeButtonEnabled(false);
+ }
+
+ listView.setOnItemClickListener(new OnItemClickListener() {
+
+ @Override
+ public void onItemClick(AdapterView<?> arg0, View clickedView,
+ int position, long arg3) {
+ if (getSelectedConversation() != conversationList.get(position)) {
+ setSelectedConversation(conversationList.get(position));
+ ConversationActivity.this.mConversationFragment.reInit(getSelectedConversation());
+ conversationWasSelectedByKeyboard = false;
+ }
+ hideConversationsOverview();
+ openConversation();
+ }
+ });
+
+ listView.setDismissCallback(new EnhancedListView.OnDismissCallback() {
+
+ @Override
+ public EnhancedListView.Undoable onDismiss(final EnhancedListView enhancedListView, final int position) {
+
+ final int index = listView.getFirstVisiblePosition();
+ View v = listView.getChildAt(0);
+ final int top = (v == null) ? 0 : (v.getTop() - listView.getPaddingTop());
+
+ try {
+ swipedConversation = listAdapter.getItem(position);
+ } catch (IndexOutOfBoundsException e) {
+ return null;
+ }
+ listAdapter.remove(swipedConversation);
+ Logging.d("markRead", "EnhancedListView.OnDismissCallback.onDismiss (" + swipedConversation.getName() + ")");
+ xmppConnectionService.markRead(swipedConversation);
+
+ final boolean formerlySelected = (getSelectedConversation() == swipedConversation);
+ if (position == 0 && listAdapter.getCount() == 0) {
+ endConversation(swipedConversation, false, true);
+ return null;
+ } else if (formerlySelected) {
+ setSelectedConversation(listAdapter.getItem(0));
+ ConversationActivity.this.mConversationFragment
+ .reInit(getSelectedConversation());
+ }
+
+ return new EnhancedListView.Undoable() {
+
+ @Override
+ public void undo() {
+ listAdapter.insert(swipedConversation, position);
+ if (formerlySelected) {
+ setSelectedConversation(swipedConversation);
+ ConversationActivity.this.mConversationFragment
+ .reInit(getSelectedConversation());
+ }
+ swipedConversation = null;
+ listView.setSelectionFromTop(index + (listView.getChildCount() < position ? 1 : 0), top);
+ }
+
+ @Override
+ public void discard() {
+ if (!swipedConversation.isRead()
+ && swipedConversation.getMode() == Conversation.MODE_SINGLE) {
+ swipedConversation = null;
+ return;
+ }
+ endConversation(swipedConversation, false, false);
+ swipedConversation = null;
+ }
+
+ @Override
+ public String getTitle() {
+ if (swipedConversation.getMode() == Conversation.MODE_MULTI) {
+ return getResources().getString(R.string.title_undo_swipe_out_muc);
+ } else {
+ return getResources().getString(R.string.title_undo_swipe_out_conversation);
+ }
+ }
+ };
+ }
+ });
+ listView.enableSwipeToDismiss();
+ listView.setSwipeDirection(EnhancedListView.SwipeDirection.START);
+ listView.setSwipingLayout(R.id.swipeable_item);
+ listView.setUndoStyle(EnhancedListView.UndoStyle.SINGLE_POPUP);
+ listView.setUndoHideDelay(5000);
+ listView.setRequireTouchBeforeDismiss(false);
+
+ mContentView = findViewById(R.id.content_view_spl);
+ if (mContentView == null) {
+ mContentView = findViewById(R.id.content_view_ll);
+ }
+ if (mContentView instanceof SlidingPaneLayout) {
+ SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView;
+ // Move the conversation list when sliding the selected conversation
+ mSlidingPaneLayout.setParallaxDistance(150);
+ // The shadow between conversation list and selected conversation
+ mSlidingPaneLayout.setShadowResourceLeft(R.drawable.es_slidingpane_shadow);
+ mSlidingPaneLayout.setSliderFadeColor(0);
+ mSlidingPaneLayout.setPanelSlideListener(new PanelSlideListener() {
+
+ @Override
+ public void onPanelOpened(View arg0) {
+ updateActionBarTitle();
+ invalidateOptionsMenu();
+ hideKeyboard();
+ if (xmppConnectionServiceBound) {
+ xmppConnectionService.getNotificationService()
+ .setOpenConversation(null);
+ }
+ closeContextMenu();
+ }
+
+ @Override
+ public void onPanelClosed(View arg0) {
+ listView.discardUndo();
+ openConversation();
+ }
+
+ @Override
+ public void onPanelSlide(View arg0, float arg1) {
+ // TODO Auto-generated method stub
+
+ }
+ });
+ }
+ }
+
+ @Override
+ public void switchToConversation(Conversation conversation) {
+ setSelectedConversation(conversation);
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ ConversationActivity.this.mConversationFragment.reInit(getSelectedConversation());
+ openConversation();
+ }
+ });
+ }
+
+ private void updateActionBarTitle() {
+ updateActionBarTitle(isConversationsOverviewHideable() && !isConversationsOverviewVisable());
+ }
+
+ private void updateActionBarTitle(boolean titleShouldBeName) {
+ final ActionBar ab = getActionBar();
+ final Conversation conversation = getSelectedConversation();
+ if (ab != null) {
+ if (titleShouldBeName && conversation != null) {
+ ab.setDisplayHomeAsUpEnabled(true);
+ ab.setHomeButtonEnabled(true);
+ if (conversation.getMode() == Conversation.MODE_SINGLE || ConversationsPlusPreferences.useSubject()) {
+ ab.setTitle(conversation.getName());
+ } else {
+ ab.setTitle(conversation.getJid().toBareJid().toString());
+ }
+ } else {
+ ab.setDisplayHomeAsUpEnabled(false);
+ ab.setHomeButtonEnabled(false);
+ ab.setTitle(R.string.app_name);
+ }
+ }
+ }
+
+ private void openConversation() {
+ this.updateActionBarTitle();
+ this.invalidateOptionsMenu();
+ if (xmppConnectionServiceBound) {
+ final Conversation conversation = getSelectedConversation();
+ xmppConnectionService.getNotificationService().setOpenConversation(conversation);
+ sendReadMarkerIfNecessary(conversation);
+ }
+ listAdapter.notifyDataSetChanged();
+ }
+
+ public void sendReadMarkerIfNecessary(final Conversation conversation) {
+ if (!mActivityPaused && conversation != null) {
+ xmppConnectionService.sendReadMarker(conversation);
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.conversations, menu);
+ final MenuItem menuSecure = menu.findItem(R.id.action_security);
+ final MenuItem menuArchive = menu.findItem(R.id.action_archive);
+ final MenuItem menuMucDetails = menu.findItem(R.id.action_muc_details);
+ final MenuItem menuContactDetails = menu.findItem(R.id.action_contact_details);
+ final MenuItem menuAttach = menu.findItem(R.id.action_attach_file);
+ final MenuItem menuClearHistory = menu.findItem(R.id.action_clear_history);
+ final MenuItem menuAdd = menu.findItem(R.id.action_add);
+ final MenuItem menuInviteContact = menu.findItem(R.id.action_invite);
+ final MenuItem menuMute = menu.findItem(R.id.action_mute);
+ final MenuItem menuUnmute = menu.findItem(R.id.action_unmute);
+
+ if (isConversationsOverviewVisable() && isConversationsOverviewHideable()) {
+ menuArchive.setVisible(false);
+ menuMucDetails.setVisible(false);
+ menuContactDetails.setVisible(false);
+ menuSecure.setVisible(false);
+ menuInviteContact.setVisible(false);
+ menuAttach.setVisible(false);
+ menuClearHistory.setVisible(false);
+ menuMute.setVisible(false);
+ menuUnmute.setVisible(false);
+ } else {
+ menuAdd.setVisible(!isConversationsOverviewHideable());
+ if (this.getSelectedConversation() != null) {
+ if (this.getSelectedConversation().getNextEncryption() != Message.ENCRYPTION_NONE) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ menuSecure.setIcon(R.drawable.ic_lock_white_24dp);
+ } else {
+ menuSecure.setIcon(R.drawable.ic_action_secure);
+ }
+ }
+ if (this.getSelectedConversation().getMode() == Conversation.MODE_MULTI) {
+ menuContactDetails.setVisible(false);
+ menuAttach.setVisible(getSelectedConversation().getAccount().httpUploadAvailable() && getSelectedConversation().getMucOptions().participating());
+ menuInviteContact.setVisible(getSelectedConversation().getMucOptions().canInvite());
+ menuSecure.setVisible((Config.supportOpenPgp() || Config.supportOmemo()) && Config.multipleEncryptionChoices()); //only if pgp is supported we have a choice
+ } else {
+ menuMucDetails.setVisible(false);
+ menuSecure.setVisible(Config.multipleEncryptionChoices());
+ }
+ if (this.getSelectedConversation().isMuted()) {
+ menuMute.setVisible(false);
+ } else {
+ menuUnmute.setVisible(false);
+ }
+ }
+ }
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ protected void selectPresenceToAttachFile(final int attachmentChoice, final int encryption) {
+ final Conversation conversation = getSelectedConversation();
+ final Account account = conversation.getAccount();
+ final OnPresenceSelected callback = new OnPresenceSelected() {
+
+ @Override
+ public void onPresenceSelected() {
+ Intent intent = new Intent();
+ boolean chooser = false;
+ String fallbackPackageId = null;
+ switch (attachmentChoice) {
+ case ATTACHMENT_CHOICE_CHOOSE_IMAGE:
+ intent.setAction(Intent.ACTION_GET_CONTENT);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+ intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
+ }
+ intent.setType("image/*");
+ chooser = true;
+ break;
+ case ATTACHMENT_CHOICE_TAKE_PHOTO:
+ Uri uri = FileBackend.getTakePhotoUri();
+ intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);
+ intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
+ mPendingImageUris.clear();
+ mPendingImageUris.add(uri);
+ break;
+ case ATTACHMENT_CHOICE_CHOOSE_FILE:
+ chooser = true;
+ intent.setType("*/*");
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+ intent.setAction(Intent.ACTION_GET_CONTENT);
+ break;
+ case ATTACHMENT_CHOICE_RECORD_VOICE:
+ intent.setAction(MediaStore.Audio.Media.RECORD_SOUND_ACTION);
+ fallbackPackageId = "eu.siacs.conversations.voicerecorder";
+ break;
+ case ATTACHMENT_CHOICE_LOCATION:
+ intent.setAction("eu.siacs.conversations.location.request");
+ fallbackPackageId = "eu.siacs.conversations.sharelocation";
+ break;
+ }
+ if (intent.resolveActivity(getPackageManager()) != null) {
+ if (chooser) {
+ startActivityForResult(
+ Intent.createChooser(intent, getString(R.string.perform_action_with)),
+ attachmentChoice);
+ } else {
+ startActivityForResult(intent, attachmentChoice);
+ }
+ } else if (fallbackPackageId != null) {
+ startActivity(getInstallApkIntent(fallbackPackageId));
+ }
+ }
+ };
+ if ((account.httpUploadAvailable() || attachmentChoice == ATTACHMENT_CHOICE_LOCATION) && encryption != Message.ENCRYPTION_OTR) {
+ conversation.setNextCounterpart(null);
+ callback.onPresenceSelected();
+ } else {
+ selectPresence(conversation, callback);
+ }
+ }
+
+ private Intent getInstallApkIntent(final String packageId) {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(Uri.parse("market://details?id=" + packageId));
+ if (intent.resolveActivity(getPackageManager()) != null) {
+ return intent;
+ } else {
+ intent.setData(Uri.parse("http://play.google.com/store/apps/details?id=" + packageId));
+ return intent;
+ }
+ }
+
+ public void attachFile(final int attachmentChoice) {
+ if (attachmentChoice != ATTACHMENT_CHOICE_LOCATION) {
+ if (!hasStoragePermission(attachmentChoice)) {
+ return;
+ }
+ }
+ switch (attachmentChoice) {
+ case ATTACHMENT_CHOICE_LOCATION:
+ ConversationsPlusPreferences.applyRecentlyUsedQuickAction("location");
+ break;
+ case ATTACHMENT_CHOICE_RECORD_VOICE:
+ ConversationsPlusPreferences.applyRecentlyUsedQuickAction("voice");
+ break;
+ case ATTACHMENT_CHOICE_TAKE_PHOTO:
+ ConversationsPlusPreferences.applyRecentlyUsedQuickAction("photo");
+ break;
+ case ATTACHMENT_CHOICE_CHOOSE_IMAGE:
+ ConversationsPlusPreferences.applyRecentlyUsedQuickAction("picture");
+ break;
+ }
+ final Conversation conversation = getSelectedConversation();
+ final int encryption = conversation.getNextEncryption();
+ final int mode = conversation.getMode();
+ if (encryption == Message.ENCRYPTION_PGP) {
+ if (hasPgp()) {
+ if (mode == Conversation.MODE_SINGLE && conversation.getContact().getPgpKeyId() != 0) {
+ xmppConnectionService.getPgpEngine().hasKey(
+ conversation.getContact(),
+ new UiCallback<Contact>() {
+
+ @Override
+ public void userInputRequried(PendingIntent pi, Contact contact) {
+ ConversationActivity.this.runIntent(pi, attachmentChoice);
+ }
+
+ @Override
+ public void success(Contact contact) {
+ selectPresenceToAttachFile(attachmentChoice, encryption);
+ }
+
+ @Override
+ public void error(int error, Contact contact) {
+ displayErrorDialog(error);
+ }
+ });
+ } else if (mode == Conversation.MODE_MULTI && conversation.getMucOptions().pgpKeysInUse()) {
+ if (!conversation.getMucOptions().everybodyHasKeys()) {
+ Toast warning = Toast
+ .makeText(this,
+ R.string.missing_public_keys,
+ Toast.LENGTH_LONG);
+ warning.setGravity(Gravity.CENTER_VERTICAL, 0, 0);
+ warning.show();
+ }
+ selectPresenceToAttachFile(attachmentChoice, encryption);
+ } else {
+ final ConversationFragment fragment = (ConversationFragment) getFragmentManager()
+ .findFragmentByTag("conversation");
+ if (fragment != null) {
+ fragment.showNoPGPKeyDialog(false,
+ new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog,
+ int which) {
+ conversation
+ .setNextEncryption(Message.ENCRYPTION_NONE);
+ xmppConnectionService.databaseBackend
+ .updateConversation(conversation);
+ selectPresenceToAttachFile(attachmentChoice, Message.ENCRYPTION_NONE);
+ }
+ });
+ }
+ }
+ } else {
+ showInstallPgpDialog();
+ }
+ } else {
+ if (encryption != Message.ENCRYPTION_AXOLOTL || !trustKeysIfNeeded(REQUEST_TRUST_KEYS_MENU, attachmentChoice)) {
+ selectPresenceToAttachFile(attachmentChoice, encryption);
+ }
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
+ if (grantResults.length > 0)
+ if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ if (requestCode == REQUEST_START_DOWNLOAD) {
+ if (this.mPendingDownloadableMessage != null) {
+ startDownloadable(this.mPendingDownloadableMessage);
+ }
+ } else {
+ attachFile(requestCode);
+ }
+ } else {
+ Toast.makeText(this, R.string.no_storage_permission, Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ public void startDownloadable(Message message) {
+ if (!hasStoragePermission(ConversationActivity.REQUEST_START_DOWNLOAD)) {
+ this.mPendingDownloadableMessage = message;
+ return;
+ }
+ Transferable transferable = message.getTransferable();
+ if (transferable != null) {
+ if (!transferable.start()) {
+ Toast.makeText(this, R.string.not_connected_try_again, Toast.LENGTH_SHORT).show();
+ }
+ } else if (message.treatAsDownloadable() != Message.Decision.NEVER) {
+ HttpDownloadConnection downloadConnection = HttpConnectionManager.createNewDownloadConnection(message, true);
+ if (null == downloadConnection) {
+ Toast.makeText(this, R.string.file_not_on_remote_host, Toast.LENGTH_LONG).show();
+ }
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ showConversationsOverview();
+ return true;
+ } else if (item.getItemId() == R.id.action_add) {
+ startActivity(new Intent(this, StartConversationActivity.class));
+ return true;
+ } else if (getSelectedConversation() != null) {
+ switch (item.getItemId()) {
+ case R.id.action_attach_file:
+ attachFileDialog();
+ break;
+ case R.id.action_archive:
+ this.endConversation(getSelectedConversation());
+ break;
+ case R.id.action_contact_details:
+ switchToContactDetails(getSelectedConversation().getContact());
+ break;
+ case R.id.action_muc_details:
+ Intent intent = new Intent(this,
+ ConferenceDetailsActivity.class);
+ intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC);
+ intent.putExtra("uuid", getSelectedConversation().getUuid());
+ startActivity(intent);
+ break;
+ case R.id.action_invite:
+ inviteToConversation(getSelectedConversation());
+ break;
+ case R.id.action_security:
+ selectEncryptionDialog(getSelectedConversation());
+ break;
+ case R.id.action_clear_history:
+ clearHistoryDialog(getSelectedConversation());
+ break;
+ case R.id.action_mute:
+ muteConversationDialog(getSelectedConversation());
+ break;
+ case R.id.action_unmute:
+ unmuteConversation(getSelectedConversation());
+ break;
+ case R.id.action_block:
+ BlockContactDialog.show(this, xmppConnectionService, getSelectedConversation());
+ break;
+ case R.id.action_unblock:
+ BlockContactDialog.show(this, xmppConnectionService, getSelectedConversation());
+ break;
+ default:
+ break;
+ }
+ return super.onOptionsItemSelected(item);
+ } else {
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ public void endConversation(Conversation conversation) {
+ endConversation(conversation, true, true);
+ }
+
+ public void endConversation(Conversation conversation, boolean showOverview, boolean reinit) {
+ if (showOverview) {
+ showConversationsOverview();
+ }
+ xmppConnectionService.archiveConversation(conversation);
+ if (reinit) {
+ if (conversationList.size() > 0) {
+ setSelectedConversation(conversationList.get(0));
+ this.mConversationFragment.reInit(getSelectedConversation());
+ } else {
+ setSelectedConversation(null);
+ if (mRedirected.compareAndSet(false, true)) {
+ Intent intent = new Intent(this, StartConversationActivity.class);
+ intent.putExtra("init", true);
+ startActivity(intent);
+ finish();
+ }
+ }
+ }
+ }
+
+ @SuppressLint("InflateParams")
+ protected void clearHistoryDialog(final Conversation conversation) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(getString(R.string.clear_conversation_history));
+ View dialogView = getLayoutInflater().inflate(
+ R.layout.dialog_clear_history, null);
+ final CheckBox endConversationCheckBox = (CheckBox) dialogView
+ .findViewById(R.id.end_conversation_checkbox);
+ builder.setView(dialogView);
+ builder.setNegativeButton(getString(R.string.cancel), null);
+ builder.setPositiveButton(getString(R.string.delete_messages),
+ new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ ConversationActivity.this.xmppConnectionService.clearConversationHistory(conversation);
+ if (endConversationCheckBox.isChecked()) {
+ endConversation(conversation);
+ } else {
+ updateConversationList();
+ ConversationActivity.this.mConversationFragment.updateMessages();
+ }
+ }
+ });
+ builder.create().show();
+ }
+
+ protected void attachFileDialog() {
+ View menuAttachFile = findViewById(R.id.action_attach_file);
+ if (menuAttachFile == null) {
+ return;
+ }
+ PopupMenu attachFilePopup = new PopupMenu(this, menuAttachFile);
+ attachFilePopup.inflate(R.menu.attachment_choices);
+ if (new Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION).resolveActivity(getPackageManager()) == null) {
+ attachFilePopup.getMenu().findItem(R.id.attach_record_voice).setVisible(false);
+ }
+ if (new Intent("eu.siacs.conversations.location.request").resolveActivity(getPackageManager()) == null) {
+ attachFilePopup.getMenu().findItem(R.id.attach_location).setVisible(false);
+ }
+ attachFilePopup.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.attach_choose_picture:
+ attachFile(ATTACHMENT_CHOICE_CHOOSE_IMAGE);
+ break;
+ case R.id.attach_take_picture:
+ attachFile(ATTACHMENT_CHOICE_TAKE_PHOTO);
+ break;
+ case R.id.attach_choose_file:
+ attachFile(ATTACHMENT_CHOICE_CHOOSE_FILE);
+ break;
+ case R.id.attach_record_voice:
+ attachFile(ATTACHMENT_CHOICE_RECORD_VOICE);
+ break;
+ case R.id.attach_location:
+ attachFile(ATTACHMENT_CHOICE_LOCATION);
+ break;
+ }
+ return false;
+ }
+ });
+ attachFilePopup.show();
+ }
+
+ public void verifyOtrSessionDialog(final Conversation conversation, View view) {
+ if (!conversation.hasValidOtrSession() || conversation.getOtrSession().getSessionStatus() != SessionStatus.ENCRYPTED) {
+ Toast.makeText(this, R.string.otr_session_not_started, Toast.LENGTH_LONG).show();
+ return;
+ }
+ if (view == null) {
+ return;
+ }
+ PopupMenu popup = new PopupMenu(this, view);
+ popup.inflate(R.menu.verification_choices);
+ popup.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ @Override
+ public boolean onMenuItemClick(MenuItem menuItem) {
+ Intent intent = new Intent(ConversationActivity.this, VerifyOTRActivity.class);
+ intent.setAction(VerifyOTRActivity.ACTION_VERIFY_CONTACT);
+ intent.putExtra("contact", conversation.getContact().getJid().toBareJid().toString());
+ intent.putExtra(EXTRA_ACCOUNT, conversation.getAccount().getJid().toBareJid().toString());
+ switch (menuItem.getItemId()) {
+ case R.id.scan_fingerprint:
+ intent.putExtra("mode", VerifyOTRActivity.MODE_SCAN_FINGERPRINT);
+ break;
+ case R.id.ask_question:
+ intent.putExtra("mode", VerifyOTRActivity.MODE_ASK_QUESTION);
+ break;
+ case R.id.manual_verification:
+ intent.putExtra("mode", VerifyOTRActivity.MODE_MANUAL_VERIFICATION);
+ break;
+ }
+ startActivity(intent);
+ return true;
+ }
+ });
+ popup.show();
+ }
+
+ protected void selectEncryptionDialog(final Conversation conversation) {
+ View menuItemView = findViewById(R.id.action_security);
+ if (menuItemView == null) {
+ return;
+ }
+ PopupMenu popup = new PopupMenu(this, menuItemView);
+ final ConversationFragment fragment = (ConversationFragment) getFragmentManager()
+ .findFragmentByTag("conversation");
+ if (fragment != null) {
+ popup.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.encryption_choice_none:
+ conversation.setNextEncryption(Message.ENCRYPTION_NONE);
+ item.setChecked(true);
+ break;
+ case R.id.encryption_choice_otr:
+ conversation.setNextEncryption(Message.ENCRYPTION_OTR);
+ item.setChecked(true);
+ break;
+ case R.id.encryption_choice_pgp:
+ if (hasPgp()) {
+ if (conversation.getAccount().getPgpSignature() != null) {
+ conversation.setNextEncryption(Message.ENCRYPTION_PGP);
+ item.setChecked(true);
+ } else {
+ announcePgp(conversation.getAccount(), conversation);
+ }
+ } else {
+ showInstallPgpDialog();
+ }
+ break;
+ case R.id.encryption_choice_axolotl:
+ Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(conversation.getAccount())
+ + "Enabled axolotl for Contact " + conversation.getContact().getJid());
+ conversation.setNextEncryption(Message.ENCRYPTION_AXOLOTL);
+ item.setChecked(true);
+ break;
+ default:
+ conversation.setNextEncryption(Message.ENCRYPTION_NONE);
+ break;
+ }
+ xmppConnectionService.databaseBackend.updateConversation(conversation);
+ fragment.updateChatMsgHint();
+ invalidateOptionsMenu();
+ refreshUi();
+ return true;
+ }
+ });
+ popup.inflate(R.menu.encryption_choices);
+ MenuItem otr = popup.getMenu().findItem(R.id.encryption_choice_otr);
+ MenuItem none = popup.getMenu().findItem(R.id.encryption_choice_none);
+ MenuItem pgp = popup.getMenu().findItem(R.id.encryption_choice_pgp);
+ MenuItem axolotl = popup.getMenu().findItem(R.id.encryption_choice_axolotl);
+ pgp.setVisible(Config.supportOpenPgp());
+ none.setVisible(Config.supportUnencrypted() || conversation.getMode() == Conversation.MODE_MULTI);
+ otr.setVisible(Config.supportOtr());
+ axolotl.setVisible(Config.supportOmemo());
+ if (conversation.getMode() == Conversation.MODE_MULTI) {
+ otr.setVisible(false);
+ }
+ if (!conversation.getAccount().getAxolotlService().isConversationAxolotlCapable(conversation)) {
+ axolotl.setEnabled(false);
+ }
+ switch (conversation.getNextEncryption()) {
+ case Message.ENCRYPTION_NONE:
+ none.setChecked(true);
+ break;
+ case Message.ENCRYPTION_OTR:
+ otr.setChecked(true);
+ break;
+ case Message.ENCRYPTION_PGP:
+ pgp.setChecked(true);
+ break;
+ case Message.ENCRYPTION_AXOLOTL:
+ axolotl.setChecked(true);
+ break;
+ default:
+ none.setChecked(true);
+ break;
+ }
+ popup.show();
+ }
+ }
+
+ protected void muteConversationDialog(final Conversation conversation) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(R.string.disable_notifications);
+ final int[] durations = getResources().getIntArray(R.array.mute_options_durations);
+ builder.setItems(R.array.mute_options_descriptions,
+ new OnClickListener() {
+
+ @Override
+ public void onClick(final DialogInterface dialog, final int which) {
+ final long till;
+ if (durations[which] == -1) {
+ till = Long.MAX_VALUE;
+ } else {
+ till = System.currentTimeMillis() + (durations[which] * 1000);
+ }
+ conversation.setMutedTill(till);
+ ConversationActivity.this.xmppConnectionService.databaseBackend
+ .updateConversation(conversation);
+ updateConversationList();
+ ConversationActivity.this.mConversationFragment.updateMessages();
+ invalidateOptionsMenu();
+ }
+ });
+ builder.create().show();
+ }
+
+ public void unmuteConversation(final Conversation conversation) {
+ conversation.setMutedTill(0);
+ this.xmppConnectionService.databaseBackend.updateConversation(conversation);
+ updateConversationList();
+ ConversationActivity.this.mConversationFragment.updateMessages();
+ invalidateOptionsMenu();
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (!isConversationsOverviewVisable()) {
+ showConversationsOverview();
+ } else {
+ moveTaskToBack(true);
+ }
+ }
+
+ @Override
+ public boolean onKeyUp(int key, KeyEvent event) {
+ int rotation = getWindowManager().getDefaultDisplay().getRotation();
+ final int upKey;
+ final int downKey;
+ switch (rotation) {
+ case Surface.ROTATION_90:
+ upKey = KeyEvent.KEYCODE_DPAD_LEFT;
+ downKey = KeyEvent.KEYCODE_DPAD_RIGHT;
+ break;
+ case Surface.ROTATION_180:
+ upKey = KeyEvent.KEYCODE_DPAD_DOWN;
+ downKey = KeyEvent.KEYCODE_DPAD_UP;
+ break;
+ case Surface.ROTATION_270:
+ upKey = KeyEvent.KEYCODE_DPAD_RIGHT;
+ downKey = KeyEvent.KEYCODE_DPAD_LEFT;
+ break;
+ default:
+ upKey = KeyEvent.KEYCODE_DPAD_UP;
+ downKey = KeyEvent.KEYCODE_DPAD_DOWN;
+ }
+ final boolean modifier = event.isCtrlPressed() || event.isAltPressed();
+ if (modifier && key == KeyEvent.KEYCODE_TAB && isConversationsOverviewHideable()) {
+ toggleConversationsOverview();
+ return true;
+ } else if (modifier && key == downKey) {
+ if (isConversationsOverviewHideable() && !isConversationsOverviewVisable()) {
+ showConversationsOverview();
+ ;
+ }
+ return selectDownConversation();
+ } else if (modifier && key == upKey) {
+ if (isConversationsOverviewHideable() && !isConversationsOverviewVisable()) {
+ showConversationsOverview();
+ }
+ return selectUpConversation();
+ } else if (modifier && key == KeyEvent.KEYCODE_1) {
+ return openConversationByIndex(0);
+ } else if (modifier && key == KeyEvent.KEYCODE_2) {
+ return openConversationByIndex(1);
+ } else if (modifier && key == KeyEvent.KEYCODE_3) {
+ return openConversationByIndex(2);
+ } else if (modifier && key == KeyEvent.KEYCODE_4) {
+ return openConversationByIndex(3);
+ } else if (modifier && key == KeyEvent.KEYCODE_5) {
+ return openConversationByIndex(4);
+ } else if (modifier && key == KeyEvent.KEYCODE_6) {
+ return openConversationByIndex(5);
+ } else if (modifier && key == KeyEvent.KEYCODE_7) {
+ return openConversationByIndex(6);
+ } else if (modifier && key == KeyEvent.KEYCODE_8) {
+ return openConversationByIndex(7);
+ } else if (modifier && key == KeyEvent.KEYCODE_9) {
+ return openConversationByIndex(8);
+ } else if (modifier && key == KeyEvent.KEYCODE_0) {
+ return openConversationByIndex(9);
+ } else {
+ return super.onKeyUp(key, event);
+ }
+ }
+
+ private void toggleConversationsOverview() {
+ if (isConversationsOverviewVisable()) {
+ hideConversationsOverview();
+ if (mConversationFragment != null) {
+ mConversationFragment.setFocusOnInputField();
+ }
+ } else {
+ showConversationsOverview();
+ }
+ }
+
+ private boolean selectUpConversation() {
+ if (this.mSelectedConversation != null) {
+ int index = this.conversationList.indexOf(this.mSelectedConversation);
+ if (index > 0) {
+ return openConversationByIndex(index - 1);
+ }
+ }
+ return false;
+ }
+
+ private boolean selectDownConversation() {
+ if (this.mSelectedConversation != null) {
+ int index = this.conversationList.indexOf(this.mSelectedConversation);
+ if (index != -1 && index < this.conversationList.size() - 1) {
+ return openConversationByIndex(index + 1);
+ }
+ }
+ return false;
+ }
+
+ private boolean openConversationByIndex(int index) {
+ try {
+ this.conversationWasSelectedByKeyboard = true;
+ setSelectedConversation(this.conversationList.get(index));
+ this.mConversationFragment.reInit(getSelectedConversation());
+ if (index > listView.getLastVisiblePosition() - 1 || index < listView.getFirstVisiblePosition() + 1) {
+ this.listView.setSelection(index);
+ }
+ openConversation();
+ return true;
+ } catch (IndexOutOfBoundsException e) {
+ return false;
+ }
+ }
+
+ @Override
+ protected void onNewIntent(final Intent intent) {
+ if (xmppConnectionServiceBound) {
+ if (intent != null && VIEW_CONVERSATION.equals(intent.getType())) {
+ handleViewConversationIntent(intent);
+ setIntent(new Intent());
+ }
+ } else {
+ setIntent(intent);
+ }
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ this.mRedirected.set(false);
+ if (this.xmppConnectionServiceBound) {
+ this.onBackendConnected();
+ }
+ if (conversationList.size() >= 1) {
+ this.onConversationUpdate();
+ }
+ }
+
+ @Override
+ public void onPause() {
+ listView.discardUndo();
+ super.onPause();
+ this.mActivityPaused = true;
+ if (this.xmppConnectionServiceBound) {
+ this.xmppConnectionService.getNotificationService().setIsInForeground(false);
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ final int theme = findTheme();
+ final boolean usingEnterKey = ConversationsPlusPreferences.displayEnterKey();
+ if (this.mTheme != theme || usingEnterKey != mUsingEnterKey) {
+ recreate();
+ }
+ this.mActivityPaused = false;
+ if (this.xmppConnectionServiceBound) {
+ this.xmppConnectionService.getNotificationService().setIsInForeground(true);
+ }
+
+ if (!isConversationsOverviewVisable() || !isConversationsOverviewHideable()) {
+ sendReadMarkerIfNecessary(getSelectedConversation());
+ }
+
+ }
+
+ @Override
+ public void onSaveInstanceState(final Bundle savedInstanceState) {
+ Conversation conversation = getSelectedConversation();
+ if (conversation != null) {
+ savedInstanceState.putString(STATE_OPEN_CONVERSATION, conversation.getUuid());
+ } else {
+ savedInstanceState.remove(STATE_OPEN_CONVERSATION);
+ }
+ savedInstanceState.putBoolean(STATE_PANEL_OPEN, isConversationsOverviewVisable());
+ if (this.mPendingImageUris.size() >= 1) {
+ savedInstanceState.putString(STATE_PENDING_URI, this.mPendingImageUris.get(0).toString());
+ } else {
+ savedInstanceState.remove(STATE_PENDING_URI);
+ }
+ super.onSaveInstanceState(savedInstanceState);
+ }
+
+ private void clearPending() {
+ mPendingImageUris.clear();
+ mPendingFileUris.clear();
+ mPendingGeoUri = null;
+ mPostponedActivityResult = null;
+ }
+
+ @Override
+ void onBackendConnected() {
+ this.xmppConnectionService.getNotificationService().setIsInForeground(true);
+ updateConversationList();
+
+ if (mPendingConferenceInvite != null) {
+ mPendingConferenceInvite.execute(this);
+ mPendingConferenceInvite = null;
+ }
+
+ if (xmppConnectionService.getAccounts().size() == 0) {
+ if (mRedirected.compareAndSet(false, true)) {
+ if (Config.X509_VERIFICATION) {
+ startActivity(new Intent(this, ManageAccountActivity.class));
+ } else {
+ startActivity(new Intent(this, EditAccountActivity.class));
+ }
+ finish();
+ }
+ } else if (conversationList.size() <= 0) {
+ if (mRedirected.compareAndSet(false, true)) {
+ Intent intent = new Intent(this, StartConversationActivity.class);
+ intent.putExtra("init", true);
+ startActivity(intent);
+ finish();
+ }
+ } else if (getIntent() != null && VIEW_CONVERSATION.equals(getIntent().getType())) {
+ clearPending();
+ handleViewConversationIntent(getIntent());
+ } else if (selectConversationByUuid(mOpenConverstaion)) {
+ if (mPanelOpen) {
+ showConversationsOverview();
+ } else {
+ if (isConversationsOverviewHideable()) {
+ openConversation();
+ updateActionBarTitle(true);
+ }
+ }
+ this.mConversationFragment.reInit(getSelectedConversation());
+ mOpenConverstaion = null;
+ } else if (getSelectedConversation() == null) {
+ showConversationsOverview();
+ clearPending();
+ setSelectedConversation(conversationList.get(0));
+ this.mConversationFragment.reInit(getSelectedConversation());
+ } else {
+ this.mConversationFragment.messagesView.invalidateViews();
+ this.mConversationFragment.setupIme();
+ }
+
+ if (this.mPostponedActivityResult != null) {
+ this.onActivityResult(mPostponedActivityResult.first, RESULT_OK, mPostponedActivityResult.second);
+ }
+
+ if (!forbidProcessingPendings) {
+ for (Iterator<Uri> i = mPendingImageUris.iterator(); i.hasNext(); i.remove()) {
+ Uri foo = i.next();
+ attachImageToConversation(getSelectedConversation(), foo);
+ }
+
+ for (Iterator<Uri> i = mPendingFileUris.iterator(); i.hasNext(); i.remove()) {
+ attachFileToConversation(getSelectedConversation(), i.next());
+ }
+
+ if (mPendingGeoUri != null) {
+ attachLocationToConversation(getSelectedConversation(), mPendingGeoUri);
+ mPendingGeoUri = null;
+ }
+ }
+ forbidProcessingPendings = false;
+
+ if (!ExceptionHelper.checkForCrash(this, this.xmppConnectionService)) {
+ openBatteryOptimizationDialogIfNeeded();
+ }
+ setIntent(new Intent());
+ }
+
+ private void handleViewConversationIntent(final Intent intent) {
+ final String uuid = intent.getStringExtra(CONVERSATION);
+ final String downloadUuid = intent.getStringExtra(MESSAGE);
+ final String text = intent.getStringExtra(TEXT);
+ final String nick = intent.getStringExtra(NICK);
+ final boolean pm = intent.getBooleanExtra(PRIVATE_MESSAGE, false);
+ if (selectConversationByUuid(uuid)) {
+ this.mConversationFragment.reInit(getSelectedConversation());
+ if (nick != null) {
+ if (pm) {
+ Jid jid = getSelectedConversation().getJid();
+ try {
+ Jid next = Jid.fromParts(jid.getLocalpart(), jid.getDomainpart(), nick);
+ this.mConversationFragment.privateMessageWith(next);
+ } catch (final InvalidJidException ignored) {
+ //do nothing
+ }
+ } else {
+ this.mConversationFragment.highlightInConference(nick);
+ }
+ } else {
+ this.mConversationFragment.appendText(text);
+ }
+ hideConversationsOverview();
+ openConversation();
+ if (mContentView instanceof SlidingPaneLayout) {
+ updateActionBarTitle(true); //fixes bug where slp isn't properly closed yet
+ }
+ if (downloadUuid != null) {
+ final Message message = mSelectedConversation.findMessageWithFileAndUuid(downloadUuid);
+ if (message != null) {
+ startDownloadable(message);
+ }
+ }
+ }
+ }
+
+ private boolean selectConversationByUuid(String uuid) {
+ if (uuid == null) {
+ return false;
+ }
+ for (Conversation aConversationList : conversationList) {
+ if (aConversationList.getUuid().equals(uuid)) {
+ setSelectedConversation(aConversationList);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ protected void unregisterListeners() {
+ super.unregisterListeners();
+ xmppConnectionService.getNotificationService().setOpenConversation(null);
+ }
+
+ @SuppressLint("NewApi")
+ private static List<Uri> extractUriFromIntent(final Intent intent) {
+ List<Uri> uris = new ArrayList<>();
+ if (intent == null) {
+ return uris;
+ }
+ Uri uri = intent.getData();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && uri == null) {
+ ClipData clipData = intent.getClipData();
+ for (int i = 0; i < clipData.getItemCount(); ++i) {
+ uris.add(clipData.getItemAt(i).getUri());
+ }
+ } else {
+ uris.add(uri);
+ }
+ return uris;
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, final Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (resultCode == RESULT_OK) {
+ if (requestCode == REQUEST_DECRYPT_PGP) {
+ mConversationFragment.onActivityResult(requestCode, resultCode, data);
+ } else if (requestCode == REQUEST_CHOOSE_PGP_ID) {
+ // the user chose OpenPGP for encryption and selected his key in the PGP provider
+ if (xmppConnectionServiceBound) {
+ if (data.getExtras().containsKey(OpenPgpApi.EXTRA_SIGN_KEY_ID)) {
+ // associate selected PGP keyId with the account
+ mSelectedConversation.getAccount().setPgpSignId(data.getExtras().getLong(OpenPgpApi.EXTRA_SIGN_KEY_ID));
+ // we need to announce the key as described in XEP-027
+ announcePgp(mSelectedConversation.getAccount(), null);
+ } else {
+ choosePgpSignId(mSelectedConversation.getAccount());
+ }
+ this.mPostponedActivityResult = null;
+ } else {
+ this.mPostponedActivityResult = new Pair<>(requestCode, data);
+ }
+ } else if (requestCode == REQUEST_ANNOUNCE_PGP) {
+ if (xmppConnectionServiceBound) {
+ announcePgp(mSelectedConversation.getAccount(), mSelectedConversation);
+ this.mPostponedActivityResult = null;
+ } else {
+ this.mPostponedActivityResult = new Pair<>(requestCode, data);
+ }
+ } else if (requestCode == ATTACHMENT_CHOICE_CHOOSE_IMAGE) {
+ mPendingImageUris.clear();
+ mPendingImageUris.addAll(extractUriFromIntent(data));
+ if (xmppConnectionServiceBound) {
+ for (Iterator<Uri> i = mPendingImageUris.iterator(); i.hasNext(); i.remove()) {
+ attachImageToConversation(getSelectedConversation(), i.next());
+ }
+ }
+ } else if (requestCode == ATTACHMENT_CHOICE_CHOOSE_FILE || requestCode == ATTACHMENT_CHOICE_RECORD_VOICE) {
+ final List<Uri> uris = extractUriFromIntent(data);
+ final Conversation c = getSelectedConversation();
+ final OnPresenceSelected callback = new OnPresenceSelected() {
+ @Override
+ public void onPresenceSelected() {
+ mPendingFileUris.clear();
+ mPendingFileUris.addAll(uris);
+ if (xmppConnectionServiceBound) {
+ for (Iterator<Uri> i = mPendingFileUris.iterator(); i.hasNext(); i.remove()) {
+ attachFileToConversation(c, i.next());
+ }
+ }
+ }
+ };
+ if (c.getMode() == Conversation.MODE_MULTI
+ || FileUtils.allFilesUnderSize(this, uris, getMaxHttpUploadSize(c))
+ || c.getNextEncryption() == Message.ENCRYPTION_OTR) {
+ callback.onPresenceSelected();
+ } else {
+ selectPresence(c, callback);
+ }
+ } else if (requestCode == ATTACHMENT_CHOICE_TAKE_PHOTO) {
+ if (mPendingImageUris.size() == 1) {
+ Uri uri = mPendingImageUris.get(0);
+ if (xmppConnectionServiceBound) {
+ attachImageToConversation(getSelectedConversation(), uri);
+ mPendingImageUris.clear();
+ }
+ Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
+ intent.setData(uri);
+ sendBroadcast(intent);
+ } else {
+ mPendingImageUris.clear();
+ }
+ } else if (requestCode == ATTACHMENT_CHOICE_LOCATION) {
+ double latitude = data.getDoubleExtra("latitude", 0);
+ double longitude = data.getDoubleExtra("longitude", 0);
+ this.mPendingGeoUri = Uri.parse("geo:" + String.valueOf(latitude) + "," + String.valueOf(longitude));
+ if (xmppConnectionServiceBound) {
+ attachLocationToConversation(getSelectedConversation(), mPendingGeoUri);
+ this.mPendingGeoUri = null;
+ }
+ } else if (requestCode == REQUEST_TRUST_KEYS_TEXT || requestCode == REQUEST_TRUST_KEYS_MENU) {
+ this.forbidProcessingPendings = !xmppConnectionServiceBound;
+ if (xmppConnectionServiceBound) {
+ mConversationFragment.onActivityResult(requestCode, resultCode, data);
+ this.mPostponedActivityResult = null;
+ } else {
+ this.mPostponedActivityResult = new Pair<>(requestCode, data);
+ }
+
+ }
+ } else {
+ mPendingImageUris.clear();
+ mPendingFileUris.clear();
+ if (requestCode == ConversationActivity.REQUEST_DECRYPT_PGP) {
+ mConversationFragment.onActivityResult(requestCode, resultCode, data);
+ }
+ if (requestCode == REQUEST_BATTERY_OP) {
+ // Never Ask For Battery Optimizations Again
+ ConversationsPlusPreferences.commitShowBatteryOptimization(false);
+ }
+ }
+ }
+
+ private long getMaxHttpUploadSize(Conversation conversation) {
+ return conversation.getAccount().getXmppConnection().getFeatures().getMaxHttpUploadSize();
+ }
+
+ private void openBatteryOptimizationDialogIfNeeded() {
+ if (hasAccountWithoutPush()
+ && isOptimizingBattery()
+ && ConversationsPlusPreferences.showBatteryOptimization()) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(R.string.battery_optimizations_enabled);
+ builder.setMessage(R.string.battery_optimizations_enabled_dialog);
+ builder.setPositiveButton(R.string.next, new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
+ Uri uri = Uri.parse("package:" + getPackageName());
+ intent.setData(uri);
+ startActivityForResult(intent, REQUEST_BATTERY_OP);
+ }
+ });
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+ builder.setOnDismissListener(new DialogInterface.OnDismissListener() {
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ ConversationsPlusPreferences.commitShowBatteryOptimization(false);
+ }
+ });
+ }
+ builder.create().show();
+ }
+ }
+
+ private boolean hasAccountWithoutPush() {
+ for(Account account : xmppConnectionService.getAccounts()) {
+ if (account.getStatus() != Account.State.DISABLED
+ && !xmppConnectionService.getPushManagementService().available(account)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void attachLocationToConversation(Conversation conversation, Uri uri) {
+ if (conversation == null) {
+ return;
+ }
+ ConversationUtil.attachLocationToConversation(conversation,uri, new UiCallback<Message>() {
+
+ @Override
+ public void success(Message message) {
+ xmppConnectionService.sendMessage(message);
+ }
+
+ @Override
+ public void error(int errorCode, Message object) {
+
+ }
+
+ @Override
+ public void userInputRequried(PendingIntent pi, Message object) {
+
+ }
+ });
+ }
+
+ private void attachFileToConversation(Conversation conversation, Uri uri) {
+ if (conversation == null) {
+ return;
+ }
+ final Toast prepareFileToast = Toast.makeText(getApplicationContext(),getText(R.string.preparing_file), Toast.LENGTH_LONG);
+ prepareFileToast.show();
+ ConversationUtil.attachFileToConversation(conversation, uri, new UiCallback<Message>() {
+ @Override
+ public void success(Message message) {
+ hidePrepareFileToast(prepareFileToast);
+ xmppConnectionService.sendMessage(message);
+ }
+
+ @Override
+ public void error(int errorCode, Message message) {
+ hidePrepareFileToast(prepareFileToast);
+ displayErrorDialog(errorCode);
+ }
+
+ @Override
+ public void userInputRequried(PendingIntent pi, Message message) {
+
+ }
+ });
+ }
+
+ private void attachImageToConversation(Conversation conversation, Uri uri) {
+ if (conversation == null) {
+ return;
+ }
+ ResizePictureUserDecisionListener userDecisionListener = new ResizePictureUserDecisionListener(this, conversation, uri, xmppConnectionService);
+ UserDecisionDialog userDecisionDialog = new UserDecisionDialog(this, R.string.userdecision_question_resize_picture, userDecisionListener);
+ userDecisionDialog.decide(ConversationsPlusPreferences.resizePicture());
+ }
+
+ private void hidePrepareFileToast(final Toast prepareFileToast) {
+ if (prepareFileToast != null) {
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ prepareFileToast.cancel();
+ }
+ });
+ }
+ }
+
+ public void updateConversationList() {
+ xmppConnectionService
+ .populateWithOrderedConversations(conversationList);
+ if (swipedConversation != null) {
+ if (swipedConversation.isRead()) {
+ conversationList.remove(swipedConversation);
+ } else {
+ listView.discardUndo();
+ }
+ }
+ listAdapter.notifyDataSetChanged();
+ }
+
+ public void runIntent(PendingIntent pi, int requestCode) {
+ try {
+ this.startIntentSenderForResult(pi.getIntentSender(), requestCode,
+ null, 0, 0, 0);
+ } catch (final SendIntentException ignored) {
+ }
+ }
+
+ public void encryptTextMessage(Message message) {
+ xmppConnectionService.getPgpEngine().encrypt(message,
+ new UiCallback<Message>() {
+
+ @Override
+ public void userInputRequried(PendingIntent pi,
+ Message message) {
+ ConversationActivity.this.runIntent(pi,
+ ConversationActivity.REQUEST_SEND_MESSAGE);
+ }
+
+ @Override
+ public void success(Message message) {
+ message.setEncryption(Message.ENCRYPTION_DECRYPTED);
+ xmppConnectionService.sendMessage(message);
+ }
+
+ @Override
+ public void error(int error, Message message) {
+
+ }
+ });
+ }
+
+ protected boolean trustKeysIfNeeded(int requestCode) {
+ return trustKeysIfNeeded(requestCode, ATTACHMENT_CHOICE_INVALID);
+ }
+
+ protected boolean trustKeysIfNeeded(int requestCode, int attachmentChoice) {
+ AxolotlService axolotlService = mSelectedConversation.getAccount().getAxolotlService();
+ final List<Jid> targets = axolotlService.getCryptoTargets(mSelectedConversation);
+ boolean hasUnaccepted = !mSelectedConversation.getAcceptedCryptoTargets().containsAll(targets);
+ boolean hasUndecidedOwn = !axolotlService.getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED).isEmpty();
+ boolean hasUndecidedContacts = !axolotlService.getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED, targets).isEmpty();
+ boolean hasPendingKeys = !axolotlService.findDevicesWithoutSession(mSelectedConversation).isEmpty();
+ boolean hasNoTrustedKeys = axolotlService.anyTargetHasNoTrustedKeys(targets);
+ if(hasUndecidedOwn || hasUndecidedContacts || hasPendingKeys || hasNoTrustedKeys || hasUnaccepted) {
+ axolotlService.createSessionsIfNeeded(mSelectedConversation);
+ Intent intent = new Intent(getApplicationContext(), TrustKeysActivity.class);
+ String[] contacts = new String[targets.size()];
+ for(int i = 0; i < contacts.length; ++i) {
+ contacts[i] = targets.get(i).toString();
+ }
+ intent.putExtra("contacts", contacts);
+ intent.putExtra(EXTRA_ACCOUNT, mSelectedConversation.getAccount().getJid().toBareJid().toString());
+ intent.putExtra("choice", attachmentChoice);
+ intent.putExtra("conversation",mSelectedConversation.getUuid());
+ startActivityForResult(intent, requestCode);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ protected void refreshUiReal() {
+ updateConversationList();
+ if (conversationList.size() > 0) {
+ if (!this.mConversationFragment.isAdded()) {
+ Log.d(Config.LOGTAG,"fragment NOT added to activity. detached="+Boolean.toString(mConversationFragment.isDetached()));
+ }
+ ConversationActivity.this.mConversationFragment.updateMessages();
+ updateActionBarTitle();
+ invalidateOptionsMenu();
+ } else {
+ Log.d(Config.LOGTAG,"not updating conversations fragment because conversations list size was 0");
+ }
+ }
+
+ @Override
+ public void onAccountUpdate() {
+ this.refreshUi();
+ }
+
+ @Override
+ public void onConversationUpdate() {
+ this.refreshUi();
+ }
+
+ @Override
+ public void onRosterUpdate() {
+ this.refreshUi();
+ }
+
+ @Override
+ public void OnUpdateBlocklist(Status status) {
+ this.refreshUi();
+ }
+
+ public void unblockConversation(final Blockable conversation) {
+ xmppConnectionService.sendUnblockRequest(conversation);
+ }
+
+ @Override
+ public void onShowErrorToast(final int resId) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(ConversationActivity.this,resId,Toast.LENGTH_SHORT).show();
+ }
+ });
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
new file mode 100644
index 00000000..08f49551
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
@@ -0,0 +1,1389 @@
+package eu.siacs.conversations.ui;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Fragment;
+import android.app.PendingIntent;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.IntentSender.SendIntentException;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.text.InputType;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.PopupWindow;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+import android.widget.TextView.OnEditorActionListener;
+import android.widget.Toast;
+
+import com.orangegangsters.github.swipyrefreshlayout.library.SwipyRefreshLayout;
+
+import net.java.otr4j.session.SessionStatus;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.UUID;
+
+import de.thedevstack.conversationsplus.ConversationsPlusPreferences;
+import de.thedevstack.conversationsplus.services.filetransfer.http.delete.DeleteRemoteFileService;
+import de.thedevstack.conversationsplus.ui.dialogs.SimpleConfirmDialog;
+import de.thedevstack.conversationsplus.ui.dialogs.MessageDetailsDialog;
+import de.thedevstack.conversationsplus.ui.listeners.DeleteFileCallback;
+import de.thedevstack.conversationsplus.ui.listeners.SimpleUserDecisionCallback;
+import de.thedevstack.conversationsplus.ui.listeners.UserDecisionListener;
+import de.thedevstack.conversationsplus.utils.MessageUtil;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.crypto.axolotl.AxolotlService;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.MucOptions;
+import eu.siacs.conversations.entities.Presence;
+import eu.siacs.conversations.entities.Transferable;
+import eu.siacs.conversations.entities.TransferablePlaceholder;
+import eu.siacs.conversations.http.HttpConnectionManager;
+import eu.siacs.conversations.http.HttpDownloadConnection;
+import eu.siacs.conversations.persistance.FileBackend;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.ui.XmppActivity.OnPresenceSelected;
+import eu.siacs.conversations.ui.XmppActivity.OnValueEdited;
+import eu.siacs.conversations.ui.adapter.MessageAdapter;
+import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureClicked;
+import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureLongClicked;
+import eu.siacs.conversations.ui.listeners.ConversationSwipeRefreshListener;
+import eu.siacs.conversations.utils.GeoHelper;
+import eu.siacs.conversations.utils.UIHelper;
+import eu.siacs.conversations.xmpp.chatstate.ChatState;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+import github.ankushsachdeva.emojicon.EmojiconGridView;
+import github.ankushsachdeva.emojicon.EmojiconsPopup;
+import github.ankushsachdeva.emojicon.emoji.Emojicon;
+
+public class ConversationFragment extends Fragment implements EditMessage.KeyboardListener {
+
+ protected Conversation conversation;
+ private OnClickListener leaveMuc = new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ activity.endConversation(conversation);
+ }
+ };
+ private OnClickListener joinMuc = new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ activity.xmppConnectionService.joinMuc(conversation);
+ }
+ };
+ private OnClickListener enterPassword = new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ MucOptions muc = conversation.getMucOptions();
+ String password = muc.getPassword();
+ if (password == null) {
+ password = "";
+ }
+ activity.quickPasswordEdit(password, new OnValueEdited() {
+
+ @Override
+ public void onValueEdited(String value) {
+ activity.xmppConnectionService.providePasswordForMuc(
+ conversation, value);
+ }
+ });
+ }
+ };
+ protected ListView messagesView;
+ protected SwipyRefreshLayout swipeLayout;
+ final protected List<Message> messageList = new ArrayList<>();
+ protected MessageAdapter messageListAdapter;
+ private EditMessage mEditMessage;
+ private ImageButton mSendButton;
+ private ImageView mEmojButton;
+ private View mRootView;
+ private EmojiconsPopup mEmojPopup;
+ private RelativeLayout snackbar;
+ private TextView snackbarMessage;
+ private TextView snackbarAction;
+ private boolean messagesLoaded = true;
+ private Toast messageLoaderToast;
+ private final int KEYCHAIN_UNLOCK_NOT_REQUIRED = 0;
+ private final int KEYCHAIN_UNLOCK_REQUIRED = 1;
+ private final int KEYCHAIN_UNLOCK_PENDING = 2;
+ private int keychainUnlock = KEYCHAIN_UNLOCK_NOT_REQUIRED;
+ protected OnClickListener clickToDecryptListener = new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (keychainUnlock == KEYCHAIN_UNLOCK_REQUIRED
+ && activity.hasPgp() && !conversation.getAccount().getPgpDecryptionService().isRunning()) {
+ keychainUnlock = KEYCHAIN_UNLOCK_PENDING;
+ updateSnackBar(conversation);
+ Message message = getLastPgpDecryptableMessage();
+ if (message != null) {
+ activity.xmppConnectionService.getPgpEngine().decrypt(message, new UiCallback<Message>() {
+ @Override
+ public void success(Message object) {
+ conversation.getAccount().getPgpDecryptionService().onKeychainUnlocked();
+ keychainUnlock = KEYCHAIN_UNLOCK_NOT_REQUIRED;
+ }
+
+ @Override
+ public void error(int errorCode, Message object) {
+ keychainUnlock = KEYCHAIN_UNLOCK_NOT_REQUIRED;
+ }
+
+ @Override
+ public void userInputRequried(PendingIntent pi, Message object) {
+ try {
+ activity.startIntentSenderForResult(pi.getIntentSender(),
+ ConversationActivity.REQUEST_DECRYPT_PGP, null, 0, 0, 0);
+ } catch (SendIntentException e) {
+ keychainUnlock = KEYCHAIN_UNLOCK_NOT_REQUIRED;
+ updatePgpMessages();
+ }
+ }
+ });
+ }
+ } else {
+ keychainUnlock = KEYCHAIN_UNLOCK_NOT_REQUIRED;
+ updatePgpMessages();
+ }
+ }
+ };
+ protected OnClickListener clickToVerify = new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ activity.verifyOtrSessionDialog(conversation, v);
+ }
+ };
+ private OnEditorActionListener mEditorActionListener = new OnEditorActionListener() {
+
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (actionId == EditorInfo.IME_ACTION_SEND) {
+ InputMethodManager imm = (InputMethodManager) v.getContext()
+ .getSystemService(Context.INPUT_METHOD_SERVICE);
+ if (imm.isFullscreenMode()) {
+ imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
+ }
+ sendMessage();
+ return true;
+ } else {
+ return false;
+ }
+ }
+ };
+ private OnClickListener mSendButtonListener = new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ Object tag = v.getTag();
+ if (tag instanceof SendButtonAction) {
+ SendButtonAction action = (SendButtonAction) tag;
+ switch (action) {
+ case TAKE_PHOTO:
+ activity.attachFile(ConversationActivity.ATTACHMENT_CHOICE_TAKE_PHOTO);
+ break;
+ case SEND_LOCATION:
+ activity.attachFile(ConversationActivity.ATTACHMENT_CHOICE_LOCATION);
+ break;
+ case RECORD_VOICE:
+ activity.attachFile(ConversationActivity.ATTACHMENT_CHOICE_RECORD_VOICE);
+ break;
+ case CHOOSE_PICTURE:
+ activity.attachFile(ConversationActivity.ATTACHMENT_CHOICE_CHOOSE_IMAGE);
+ break;
+ case CANCEL:
+ if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) {
+ conversation.setNextCounterpart(null);
+ updateChatMsgHint();
+ updateSendButton();
+ }
+ break;
+ default:
+ sendMessage();
+ }
+ } else {
+ sendMessage();
+ }
+ }
+ };
+ private OnClickListener clickToMuc = new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent(getActivity(), ConferenceDetailsActivity.class);
+ intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC);
+ intent.putExtra("uuid", conversation.getUuid());
+ startActivity(intent);
+ }
+ };
+ private ConversationActivity activity;
+ private Message selectedMessage;
+
+ public void setMessagesLoaded() {
+ this.messagesLoaded = true;
+ }
+
+ private void sendMessage() {
+ final String body = mEditMessage.getText().toString();
+ if (body.length() == 0 || this.conversation == null) {
+ return;
+ }
+ Message message = new Message(conversation, body, conversation.getNextEncryption());
+ if (conversation.getMode() == Conversation.MODE_MULTI) {
+ if (conversation.getNextCounterpart() != null) {
+ message.setCounterpart(conversation.getNextCounterpart());
+ message.setType(Message.TYPE_PRIVATE);
+ }
+ }
+ switch (conversation.getNextEncryption()) {
+ case Message.ENCRYPTION_OTR:
+ sendOtrMessage(message);
+ break;
+ case Message.ENCRYPTION_PGP:
+ sendPgpMessage(message);
+ break;
+ case Message.ENCRYPTION_AXOLOTL:
+ if(!activity.trustKeysIfNeeded(ConversationActivity.REQUEST_TRUST_KEYS_TEXT)) {
+ sendAxolotlMessage(message);
+ }
+ break;
+ default:
+ sendPlainTextMessage(message);
+ }
+ }
+
+ public void updateChatMsgHint() {
+ final boolean multi = conversation.getMode() == Conversation.MODE_MULTI;
+ if (multi && conversation.getNextCounterpart() != null) {
+ this.mEditMessage.setHint(getString(
+ R.string.send_private_message_to,
+ conversation.getNextCounterpart().getResourcepart()));
+ } else if (multi && !conversation.getMucOptions().participating()) {
+ this.mEditMessage.setHint(R.string.you_are_not_participating);
+ } else {
+ switch (conversation.getNextEncryption()) {
+ case Message.ENCRYPTION_NONE:
+ mEditMessage
+ .setHint(getString(R.string.send_unencrypted_message));
+ break;
+ case Message.ENCRYPTION_OTR:
+ mEditMessage.setHint(getString(R.string.send_otr_message));
+ break;
+ case Message.ENCRYPTION_AXOLOTL:
+ AxolotlService axolotlService = conversation.getAccount().getAxolotlService();
+ if (axolotlService != null && axolotlService.trustedSessionVerified(conversation)) {
+ mEditMessage.setHint(getString(R.string.send_omemo_x509_message));
+ } else {
+ mEditMessage.setHint(getString(R.string.send_omemo_message));
+ }
+ break;
+ case Message.ENCRYPTION_PGP:
+ mEditMessage.setHint(getString(R.string.send_pgp_message));
+ break;
+ default:
+ break;
+ }
+ getActivity().invalidateOptionsMenu();
+ }
+ }
+
+ public void setupIme() {
+ if (activity == null) {
+ return;
+ } else if (ConversationsPlusPreferences.displayEnterKey() && ConversationsPlusPreferences.enterIsSend()) {
+ mEditMessage.setInputType(mEditMessage.getInputType() & (~InputType.TYPE_TEXT_FLAG_MULTI_LINE));
+ mEditMessage.setInputType(mEditMessage.getInputType() & (~InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE));
+ } else if (ConversationsPlusPreferences.displayEnterKey()) {
+ mEditMessage.setInputType(mEditMessage.getInputType() | InputType.TYPE_TEXT_FLAG_MULTI_LINE);
+ mEditMessage.setInputType(mEditMessage.getInputType() & (~InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE));
+ } else {
+ mEditMessage.setInputType(mEditMessage.getInputType() | InputType.TYPE_TEXT_FLAG_MULTI_LINE);
+ mEditMessage.setInputType(mEditMessage.getInputType() | InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE);
+ }
+ }
+
+ @Override
+ public View onCreateView(final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ final View view = inflater.inflate(R.layout.fragment_conversation, container, false);
+ view.setOnClickListener(null);
+ mEditMessage = (EditMessage) view.findViewById(R.id.textinput);
+ mEditMessage.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (activity != null) {
+ activity.hideConversationsOverview();
+ }
+ }
+ });
+ mEditMessage.setOnEditorActionListener(mEditorActionListener);
+
+ // Start of emojicon
+ mEmojButton = (ImageView) view.findViewById(R.id.emoji_btn);
+ mRootView = view.findViewById(R.id.textsend);
+
+ // Give the topmost view of your activity layout hierarchy. This will be used to measure soft keyboard height
+ mEmojPopup = new EmojiconsPopup(mRootView, this.getActivity());
+
+ //Will automatically set size according to the soft keyboard size
+ mEmojPopup.setSizeForSoftKeyboard();
+
+ //If the emoji popup is dismissed, change emojiButton to smiley icon
+ mEmojPopup.setOnDismissListener(new PopupWindow.OnDismissListener() {
+
+ @Override
+ public void onDismiss() {
+ changeEmojiKeyboardIcon(mEmojButton, R.drawable.smiley);
+ }
+ });
+
+ //If the text keyboard closes, also dismiss the emoji popup
+ mEmojPopup.setOnSoftKeyboardOpenCloseListener(new EmojiconsPopup.OnSoftKeyboardOpenCloseListener() {
+
+ @Override
+ public void onKeyboardOpen(int keyBoardHeight) {
+
+ }
+
+ @Override
+ public void onKeyboardClose() {
+ if (mEmojPopup.isShowing())
+ mEmojPopup.dismiss();
+ }
+ });
+
+ //On emoji clicked, add it to edittext
+ mEmojPopup.setOnEmojiconClickedListener(new EmojiconGridView.OnEmojiconClickedListener() {
+
+ @Override
+ public void onEmojiconClicked(Emojicon emojicon) {
+ if (mEditMessage == null || emojicon == null) {
+ return;
+ }
+
+ int start = mEditMessage.getSelectionStart();
+ int end = mEditMessage.getSelectionEnd();
+ if (start < 0) {
+ mEditMessage.append(emojicon.getEmoji());
+ } else {
+ mEditMessage.getText().replace(Math.min(start, end),
+ Math.max(start, end), emojicon.getEmoji(), 0,
+ emojicon.getEmoji().length());
+ }
+ }
+ });
+
+ //On backspace clicked, emulate the KEYCODE_DEL key event
+ mEmojPopup.setOnEmojiconBackspaceClickedListener(new EmojiconsPopup.OnEmojiconBackspaceClickedListener() {
+
+ @Override
+ public void onEmojiconBackspaceClicked(View v) {
+ KeyEvent event = new KeyEvent(
+ 0, 0, 0, KeyEvent.KEYCODE_DEL, 0, 0, 0, 0, KeyEvent.KEYCODE_ENDCALL);
+ mEditMessage.dispatchKeyEvent(event);
+ }
+ });
+
+ // To toggle between text keyboard and emoji keyboard keyboard(Popup)
+ mEmojButton.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+
+ //If popup is not showing => emoji keyboard is not visible, we need to show it
+ if(!mEmojPopup.isShowing()){
+
+ //If keyboard is visible, simply show the emoji popup
+ if(mEmojPopup.isKeyBoardOpen()){
+ mEmojPopup.showAtBottom();
+ changeEmojiKeyboardIcon(mEmojButton, R.drawable.ic_action_keyboard);
+ }
+
+ //else, open the text keyboard first and immediately after that show the emoji popup
+ else{
+ mEditMessage.setFocusableInTouchMode(true);
+ mEditMessage.requestFocus();
+ mEmojPopup.showAtBottomPending();
+ final InputMethodManager inputMethodManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
+ inputMethodManager.showSoftInput(mEditMessage, InputMethodManager.SHOW_IMPLICIT);
+ changeEmojiKeyboardIcon(mEmojButton, R.drawable.ic_action_keyboard);
+ }
+ }
+
+ //If popup is showing, simply dismiss it to show the undelying text keyboard
+ else{
+ mEmojPopup.dismiss();
+ }
+ }
+ });
+
+ // End of emojicon
+
+ mSendButton = (ImageButton) view.findViewById(R.id.textSendButton);
+ mSendButton.setOnClickListener(this.mSendButtonListener);
+
+ snackbar = (RelativeLayout) view.findViewById(R.id.snackbar);
+ snackbarMessage = (TextView) view.findViewById(R.id.snackbar_message);
+ snackbarAction = (TextView) view.findViewById(R.id.snackbar_action);
+
+ messagesView = (ListView) view.findViewById(R.id.messages_view);
+ messagesView.setTranscriptMode(ListView.TRANSCRIPT_MODE_NORMAL);
+ messageListAdapter = new MessageAdapter((ConversationActivity) getActivity(), this.messageList);
+ messageListAdapter.setOnContactPictureClicked(new OnContactPictureClicked() {
+
+ @Override
+ public void onContactPictureClicked(Message message) {
+ if (message.getStatus() <= Message.STATUS_RECEIVED) {
+ if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
+ if (message.getCounterpart() != null) {
+ String user = message.getCounterpart().isBareJid() ? message.getCounterpart().toString() : message.getCounterpart().getResourcepart();
+ if (!message.getConversation().getMucOptions().isUserInRoom(user)) {
+ Toast.makeText(activity,activity.getString(R.string.user_has_left_conference,user),Toast.LENGTH_SHORT).show();
+ }
+ highlightInConference(user);
+ }
+ } else {
+ activity.switchToContactDetails(message.getContact(), message.getFingerprint());
+ }
+ } else {
+ Account account = message.getConversation().getAccount();
+ Intent intent = new Intent(activity, EditAccountActivity.class);
+ intent.putExtra("jid", account.getJid().toBareJid().toString());
+ intent.putExtra("fingerprint", message.getFingerprint());
+ startActivity(intent);
+ }
+ }
+ });
+ messageListAdapter
+ .setOnContactPictureLongClicked(new OnContactPictureLongClicked() {
+
+ @Override
+ public void onContactPictureLongClicked(Message message) {
+ if (message.getStatus() <= Message.STATUS_RECEIVED) {
+ if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
+ if (message.getCounterpart() != null) {
+ String user = message.getCounterpart().getResourcepart();
+ if (user != null) {
+ if (message.getConversation().getMucOptions().isUserInRoom(user)) {
+ privateMessageWith(message.getCounterpart());
+ } else {
+ Toast.makeText(activity, activity.getString(R.string.user_has_left_conference, user), Toast.LENGTH_SHORT).show();
+ }
+ }
+ }
+ }
+ } else {
+ activity.showQrCode();
+ }
+ }
+ });
+ messagesView.setAdapter(messageListAdapter);
+
+ registerForContextMenu(messagesView);
+
+ // Start of swipe refresh
+ // New Swipe refresh
+ swipeLayout = (SwipyRefreshLayout) view.findViewById(R.id.swipe_refresh_container);
+ swipeLayout.setOnRefreshListener(new ConversationSwipeRefreshListener(messageList, swipeLayout, this, messagesView, messageListAdapter));
+ // End of swipe refresh
+
+ return view;
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+ synchronized (this.messageList) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo;
+ this.selectedMessage = this.messageList.get(acmi.position);
+ populateContextMenu(menu);
+ }
+ }
+
+ private void populateContextMenu(ContextMenu menu) {
+ final Message m = this.selectedMessage;
+ final Transferable t = m.getTransferable();
+ if (m.getType() != Message.TYPE_STATUS) {
+ final boolean treatAsFile = m.getType() != Message.TYPE_TEXT
+ && m.getType() != Message.TYPE_PRIVATE
+ && t == null;
+ activity.getMenuInflater().inflate(R.menu.message_context, menu);
+ menu.setHeaderTitle(R.string.message_options);
+ MenuItem copyText = menu.findItem(R.id.copy_text);
+ MenuItem retryDecryption = menu.findItem(R.id.retry_decryption);
+ MenuItem shareWith = menu.findItem(R.id.share_with);
+ MenuItem sendAgain = menu.findItem(R.id.send_again);
+ MenuItem copyUrl = menu.findItem(R.id.copy_url);
+ MenuItem downloadFile = menu.findItem(R.id.download_file);
+ MenuItem cancelTransmission = menu.findItem(R.id.cancel_transmission);
+ MenuItem deleteFile = menu.findItem(R.id.delete_file);
+ if (!treatAsFile
+ && !GeoHelper.isGeoUri(m.getBody())
+ && m.treatAsDownloadable() != Message.Decision.MUST) {
+ copyText.setVisible(true);
+ }
+ if (m.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) {
+ retryDecryption.setVisible(true);
+ }
+ if (treatAsFile || (GeoHelper.isGeoUri(m.getBody()))) {
+ shareWith.setVisible(true);
+ }
+ if (m.getStatus() == Message.STATUS_SEND_FAILED) {
+ sendAgain.setVisible(true);
+ }
+ if (m.hasFileOnRemoteHost()
+ || GeoHelper.isGeoUri(m.getBody())
+ || m.treatAsDownloadable() == Message.Decision.MUST
+ || (t != null && t instanceof HttpDownloadConnection)) {
+ copyUrl.setVisible(true);
+ }
+ if ((m.getType() == Message.TYPE_TEXT && t == null && m.treatAsDownloadable() != Message.Decision.NEVER)
+ || (t instanceof TransferablePlaceholder && m.hasFileOnRemoteHost())){
+ downloadFile.setVisible(true);
+ downloadFile.setTitle(activity.getString(R.string.download_x_file,UIHelper.getFileDescriptionString(activity, m)));
+ }
+ if ((t != null && !(t instanceof TransferablePlaceholder))
+ || (m.isFileOrImage() && (m.getStatus() == Message.STATUS_WAITING
+ || m.getStatus() == Message.STATUS_OFFERED))) {
+ cancelTransmission.setVisible(true);
+ }
+ if (treatAsFile) {
+ deleteFile.setVisible(true);
+ deleteFile.setTitle(activity.getString(R.string.delete_x_file,UIHelper.getFileDescriptionString(activity, m)));
+ }
+ if (m.isHttpUploaded() && MessageUtil.isMessageSent(m)) {
+ MenuItem deleteRemoteFile = menu.findItem(R.id.msg_ctx_menu_delete_remote_file);
+ deleteRemoteFile.setVisible(true);
+ }
+ }
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.msg_ctx_menu_delete_remote_file:
+ new SimpleConfirmDialog(getActivity(), R.string.cplus_are_you_sure, new DeleteRemoteFileService(selectedMessage)).show();
+ return true;
+ case R.id.msg_ctx_mnu_details:
+ new MessageDetailsDialog(getActivity(), selectedMessage).show();
+ return true;
+ case R.id.share_with:
+ shareWith(selectedMessage);
+ return true;
+ case R.id.copy_text:
+ copyText(selectedMessage);
+ return true;
+ case R.id.send_again:
+ resendMessage(selectedMessage);
+ return true;
+ case R.id.copy_url:
+ copyUrl(selectedMessage);
+ return true;
+ case R.id.download_file:
+ downloadFile(selectedMessage);
+ return true;
+ case R.id.cancel_transmission:
+ cancelTransmission(selectedMessage);
+ return true;
+ case R.id.retry_decryption:
+ retryDecryption(selectedMessage);
+ return true;
+ case R.id.delete_file:
+ new SimpleConfirmDialog(getActivity(), R.string.cplus_are_you_sure, new DeleteFileCallback(selectedMessage)).show();
+ return true;
+ default:
+ return super.onContextItemSelected(item);
+ }
+ }
+
+ private void shareWith(Message message) {
+ Intent shareIntent = new Intent();
+ shareIntent.setAction(Intent.ACTION_SEND);
+ if (GeoHelper.isGeoUri(message.getBody())) {
+ shareIntent.putExtra(Intent.EXTRA_TEXT, message.getBody());
+ shareIntent.setType("text/plain");
+ } else {
+ shareIntent.putExtra(Intent.EXTRA_STREAM,
+ FileBackend.getJingleFileUri(message));
+ shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ String mime = message.getMimeType();
+ if (mime == null) {
+ mime = "*/*";
+ }
+ shareIntent.setType(mime);
+ }
+ try {
+ activity.startActivity(Intent.createChooser(shareIntent, getText(R.string.share_with)));
+ } catch (ActivityNotFoundException e) {
+ //This should happen only on faulty androids because normally chooser is always available
+ Toast.makeText(activity,R.string.no_application_found_to_open_file,Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ private void copyText(Message message) {
+ if (activity.copyTextToClipboard(message.getBody(),
+ R.string.message_text)) {
+ Toast.makeText(activity, R.string.message_copied_to_clipboard,
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ private void resendMessage(Message message) {
+ if (message.getType() == Message.TYPE_FILE || message.getType() == Message.TYPE_IMAGE) {
+ DownloadableFile file = FileBackend.getFile(message);
+ if (!file.exists()) {
+ Toast.makeText(activity, R.string.file_deleted, Toast.LENGTH_SHORT).show();
+ message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED));
+ activity.xmppConnectionService.updateConversationUi();
+ return;
+ }
+ }
+ activity.xmppConnectionService.resendFailedMessages(message);
+ }
+
+ private void copyUrl(Message message) {
+ final String url;
+ final int resId;
+ if (GeoHelper.isGeoUri(message.getBody())) {
+ resId = R.string.location;
+ url = message.getBody();
+ } else if (message.hasFileOnRemoteHost()) {
+ resId = R.string.file_url;
+ url = message.getFileParams().getUrl();
+ } else {
+ url = message.getBody();
+ resId = R.string.file_url;
+ }
+ if (activity.copyTextToClipboard(url, resId)) {
+ Toast.makeText(activity, R.string.url_copied_to_clipboard,
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ private void downloadFile(Message message) {
+ HttpDownloadConnection downloadConnection = HttpConnectionManager.createNewDownloadConnection(message, true);
+ if (null == downloadConnection) {
+ Toast.makeText(activity, R.string.file_not_on_remote_host, Toast.LENGTH_LONG).show();
+ }
+ }
+
+ private void cancelTransmission(Message message) {
+ Transferable transferable = message.getTransferable();
+ if (transferable != null) {
+ transferable.cancel();
+ } else {
+ MessageUtil.markMessage(message, Message.STATUS_SEND_FAILED);
+ }
+ }
+
+ private void retryDecryption(Message message) {
+ message.setEncryption(Message.ENCRYPTION_PGP);
+ activity.xmppConnectionService.updateConversationUi();
+ conversation.getAccount().getPgpDecryptionService().add(message);
+ }
+
+ protected void privateMessageWith(final Jid counterpart) {
+ this.mEditMessage.setText("");
+ this.conversation.setNextCounterpart(counterpart);
+ updateChatMsgHint();
+ updateSendButton();
+ }
+
+ protected void highlightInConference(String nick) {
+ String oldString = mEditMessage.getText().toString();
+ if (oldString.isEmpty() || mEditMessage.getSelectionStart() == 0) {
+ mEditMessage.getText().insert(0, nick + ": ");
+ } else {
+ if (mEditMessage.getText().charAt(
+ mEditMessage.getSelectionStart() - 1) != ' ') {
+ nick = " " + nick;
+ }
+ mEditMessage.getText().insert(mEditMessage.getSelectionStart(),
+ nick + " ");
+ }
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ if (this.conversation != null) {
+ final String msg = mEditMessage.getText().toString();
+ this.conversation.setNextMessage(msg);
+ updateChatState(this.conversation, msg);
+ }
+ }
+
+ private void updateChatState(final Conversation conversation, final String msg) {
+ ChatState state = msg.length() == 0 ? Config.DEFAULT_CHATSTATE : ChatState.PAUSED;
+ Account.State status = conversation.getAccount().getStatus();
+ if (status == Account.State.ONLINE && conversation.setOutgoingChatState(state)) {
+ activity.xmppConnectionService.sendChatState(conversation);
+ }
+ }
+
+ public void reInit(Conversation conversation) {
+ if (conversation == null) {
+ return;
+ }
+ this.activity = (ConversationActivity) getActivity();
+ setupIme();
+ if (this.conversation != null) {
+ final String msg = mEditMessage.getText().toString();
+ this.conversation.setNextMessage(msg);
+ if (this.conversation != conversation) {
+ updateChatState(this.conversation, msg);
+ }
+ this.conversation.trim();
+ }
+
+ this.keychainUnlock = KEYCHAIN_UNLOCK_NOT_REQUIRED;
+ this.conversation = conversation;
+ boolean canWrite = this.conversation.getMode() == Conversation.MODE_SINGLE || this.conversation.getMucOptions().participating();
+ this.mEditMessage.setEnabled(canWrite);
+ this.mSendButton.setEnabled(canWrite);
+ this.mEditMessage.setKeyboardListener(null);
+ this.mEditMessage.setText("");
+ this.mEditMessage.append(this.conversation.getNextMessage());
+ this.mEditMessage.setKeyboardListener(this);
+ this.messagesView.setAdapter(messageListAdapter);
+ updateMessages();
+ this.messagesLoaded = true;
+ int size = this.messageList.size();
+ if (size > 0) {
+ messagesView.setSelection(size - 1);
+ }
+ swipeLayout.setRefreshing(false);
+ }
+
+ private OnClickListener mEnableAccountListener = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ final Account account = conversation == null ? null : conversation.getAccount();
+ if (account != null) {
+ account.setOption(Account.OPTION_DISABLED, false);
+ activity.xmppConnectionService.updateAccount(account);
+ }
+ }
+ };
+
+ private OnClickListener mUnblockClickListener = new OnClickListener() {
+ @Override
+ public void onClick(final View v) {
+ v.post(new Runnable() {
+ @Override
+ public void run() {
+ v.setVisibility(View.INVISIBLE);
+ }
+ });
+ if (conversation.isDomainBlocked()) {
+ BlockContactDialog.show(activity, activity.xmppConnectionService, conversation);
+ } else {
+ activity.unblockConversation(conversation);
+ }
+ }
+ };
+
+ private OnClickListener mAddBackClickListener = new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ final Contact contact = conversation == null ? null : conversation.getContact();
+ if (contact != null) {
+ activity.xmppConnectionService.createContact(contact);
+ activity.switchToContactDetails(contact);
+ }
+ }
+ };
+
+ private OnClickListener mAnswerSmpClickListener = new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Intent intent = new Intent(activity, VerifyOTRActivity.class);
+ intent.setAction(VerifyOTRActivity.ACTION_VERIFY_CONTACT);
+ intent.putExtra("contact", conversation.getContact().getJid().toBareJid().toString());
+ intent.putExtra(VerifyOTRActivity.EXTRA_ACCOUNT, conversation.getAccount().getJid().toBareJid().toString());
+ intent.putExtra("mode", VerifyOTRActivity.MODE_ANSWER_QUESTION);
+ startActivity(intent);
+ }
+ };
+
+ private void updateSnackBar(final Conversation conversation) {
+ final Account account = conversation.getAccount();
+ final Contact contact = conversation.getContact();
+ final int mode = conversation.getMode();
+ if (account.getStatus() == Account.State.DISABLED) {
+ showSnackbar(R.string.this_account_is_disabled, R.string.enable, this.mEnableAccountListener);
+ } else if (conversation.isBlocked()) {
+ showSnackbar(R.string.contact_blocked, R.string.unblock, this.mUnblockClickListener);
+ } else if (!contact.showInRoster() && contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
+ showSnackbar(R.string.contact_added_you, R.string.add_back, this.mAddBackClickListener);
+ } else if (mode == Conversation.MODE_MULTI
+ && !conversation.getMucOptions().online()
+ && account.getStatus() == Account.State.ONLINE) {
+ switch (conversation.getMucOptions().getError()) {
+ case NICK_IN_USE:
+ showSnackbar(R.string.nick_in_use, R.string.edit, clickToMuc);
+ break;
+ case NO_RESPONSE:
+ showSnackbar(R.string.joining_conference, 0, null);
+ break;
+ case PASSWORD_REQUIRED:
+ showSnackbar(R.string.conference_requires_password, R.string.enter_password, enterPassword);
+ break;
+ case BANNED:
+ showSnackbar(R.string.conference_banned, R.string.leave, leaveMuc);
+ break;
+ case MEMBERS_ONLY:
+ showSnackbar(R.string.conference_members_only, R.string.leave, leaveMuc);
+ break;
+ case KICKED:
+ showSnackbar(R.string.conference_kicked, R.string.join, joinMuc);
+ break;
+ case UNKNOWN:
+ showSnackbar(R.string.conference_unknown_error, R.string.join, joinMuc);
+ break;
+ case SHUTDOWN:
+ showSnackbar(R.string.conference_shutdown, R.string.join, joinMuc);
+ break;
+ default:
+ break;
+ }
+ } else if (keychainUnlock == KEYCHAIN_UNLOCK_REQUIRED) {
+ showSnackbar(R.string.openpgp_messages_found, R.string.decrypt, clickToDecryptListener);
+ } else if (mode == Conversation.MODE_SINGLE
+ && conversation.smpRequested()) {
+ showSnackbar(R.string.smp_requested, R.string.verify, this.mAnswerSmpClickListener);
+ } else if (mode == Conversation.MODE_SINGLE
+ && conversation.hasValidOtrSession()
+ && (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED)
+ && (!conversation.isOtrFingerprintVerified())) {
+ showSnackbar(R.string.unknown_otr_fingerprint, R.string.verify, clickToVerify);
+ } else {
+ hideSnackbar();
+ }
+ }
+
+ public void updateMessages() {
+ synchronized (this.messageList) {
+ if (getView() == null) {
+ return;
+ }
+ final ConversationActivity activity = (ConversationActivity) getActivity();
+ if (this.conversation != null) {
+ conversation.populateWithMessages(ConversationFragment.this.messageList);
+ updatePgpMessages();
+ updateSnackBar(conversation);
+ updateStatusMessages();
+ this.messageListAdapter.notifyDataSetChanged();
+ updateChatMsgHint();
+ if (!activity.isConversationsOverviewVisable() || !activity.isConversationsOverviewHideable()) {
+ activity.sendReadMarkerIfNecessary(conversation);
+ }
+ this.updateSendButton();
+ }
+ }
+ }
+
+ public void updatePgpMessages() {
+ if (keychainUnlock != KEYCHAIN_UNLOCK_PENDING) {
+ if (getLastPgpDecryptableMessage() != null
+ && !conversation.getAccount().getPgpDecryptionService().isRunning()) {
+ keychainUnlock = KEYCHAIN_UNLOCK_REQUIRED;
+ } else {
+ keychainUnlock = KEYCHAIN_UNLOCK_NOT_REQUIRED;
+ }
+ }
+ }
+
+ @Nullable
+ private Message getLastPgpDecryptableMessage() {
+ for (final Message message : this.messageList) {
+ if (message.getEncryption() == Message.ENCRYPTION_PGP
+ && (message.getStatus() == Message.STATUS_RECEIVED || message.getStatus() >= Message.STATUS_SEND)
+ && message.getTransferable() == null) {
+ return message;
+ }
+ }
+ return null;
+ }
+
+ private void messageSent() {
+ int size = this.messageList.size();
+ messagesView.setSelection(size - 1);
+ mEditMessage.setText("");
+ updateChatMsgHint();
+ }
+
+ public void setFocusOnInputField() {
+ mEditMessage.requestFocus();
+ }
+
+ enum SendButtonAction {TEXT, TAKE_PHOTO, SEND_LOCATION, RECORD_VOICE, CANCEL, CHOOSE_PICTURE}
+
+ private int getSendButtonImageResource(SendButtonAction action, Presence.Status status) {
+ switch (action) {
+ case TEXT:
+ switch (status) {
+ case CHAT:
+ case ONLINE:
+ return R.drawable.ic_send_text_online;
+ case AWAY:
+ return R.drawable.ic_send_text_away;
+ case XA:
+ case DND:
+ return R.drawable.ic_send_text_dnd;
+ default:
+ return R.drawable.ic_send_text_offline;
+ }
+ case TAKE_PHOTO:
+ switch (status) {
+ case CHAT:
+ case ONLINE:
+ return R.drawable.ic_send_photo_online;
+ case AWAY:
+ return R.drawable.ic_send_photo_away;
+ case XA:
+ case DND:
+ return R.drawable.ic_send_photo_dnd;
+ default:
+ return R.drawable.ic_send_photo_offline;
+ }
+ case RECORD_VOICE:
+ switch (status) {
+ case CHAT:
+ case ONLINE:
+ return R.drawable.ic_send_voice_online;
+ case AWAY:
+ return R.drawable.ic_send_voice_away;
+ case XA:
+ case DND:
+ return R.drawable.ic_send_voice_dnd;
+ default:
+ return R.drawable.ic_send_voice_offline;
+ }
+ case SEND_LOCATION:
+ switch (status) {
+ case CHAT:
+ case ONLINE:
+ return R.drawable.ic_send_location_online;
+ case AWAY:
+ return R.drawable.ic_send_location_away;
+ case XA:
+ case DND:
+ return R.drawable.ic_send_location_dnd;
+ default:
+ return R.drawable.ic_send_location_offline;
+ }
+ case CANCEL:
+ switch (status) {
+ case CHAT:
+ case ONLINE:
+ return R.drawable.ic_send_cancel_online;
+ case AWAY:
+ return R.drawable.ic_send_cancel_away;
+ case XA:
+ case DND:
+ return R.drawable.ic_send_cancel_dnd;
+ default:
+ return R.drawable.ic_send_cancel_offline;
+ }
+ case CHOOSE_PICTURE:
+ switch (status) {
+ case CHAT:
+ case ONLINE:
+ return R.drawable.ic_send_picture_online;
+ case AWAY:
+ return R.drawable.ic_send_picture_away;
+ case XA:
+ case DND:
+ return R.drawable.ic_send_picture_dnd;
+ default:
+ return R.drawable.ic_send_picture_offline;
+ }
+ }
+ return R.drawable.ic_send_text_offline;
+ }
+
+ public void updateSendButton() {
+ final Conversation c = this.conversation;
+ final SendButtonAction action;
+ final Presence.Status status;
+ final String text = this.mEditMessage == null ? "" : this.mEditMessage.getText().toString();
+ final boolean empty = text.length() == 0;
+ final boolean conference = c.getMode() == Conversation.MODE_MULTI;
+ if (conference && !c.getAccount().httpUploadAvailable()) {
+ if (empty && c.getNextCounterpart() != null) {
+ action = SendButtonAction.CANCEL;
+ } else {
+ action = SendButtonAction.TEXT;
+ }
+ } else {
+ if (empty) {
+ if (conference && c.getNextCounterpart() != null) {
+ action = SendButtonAction.CANCEL;
+ } else {
+ String setting = ConversationsPlusPreferences.quickAction();
+ if (!setting.equals("none") && UIHelper.receivedLocationQuestion(conversation.getLatestMessage())) {
+ setting = "location";
+ } else if (setting.equals("recent")) {
+ setting = ConversationsPlusPreferences.recentlyUsedQuickAction();
+ }
+ switch (setting) {
+ case "photo":
+ action = SendButtonAction.TAKE_PHOTO;
+ break;
+ case "location":
+ action = SendButtonAction.SEND_LOCATION;
+ break;
+ case "voice":
+ action = SendButtonAction.RECORD_VOICE;
+ break;
+ case "picture":
+ action = SendButtonAction.CHOOSE_PICTURE;
+ break;
+ default:
+ action = SendButtonAction.TEXT;
+ break;
+ }
+ }
+ } else {
+ action = SendButtonAction.TEXT;
+ }
+ }
+ if (ConversationsPlusPreferences.sendButtonStatus() && c != null
+ && c.getAccount().getStatus() == Account.State.ONLINE) {
+ if (c.getMode() == Conversation.MODE_SINGLE) {
+ status = c.getContact().getMostAvailableStatus();
+ } else {
+ status = c.getMucOptions().online() ? Presence.Status.ONLINE : Presence.Status.OFFLINE;
+ }
+ } else {
+ status = Presence.Status.OFFLINE;
+ }
+ this.mSendButton.setTag(action);
+ this.mSendButton.setImageResource(getSendButtonImageResource(action, status));
+ }
+
+ public void updateStatusMessages() {
+ synchronized (this.messageList) {
+ if (conversation.getMode() == Conversation.MODE_SINGLE) {
+ ChatState state = conversation.getIncomingChatState();
+ if (state == ChatState.COMPOSING) {
+ this.messageList.add(Message.createStatusMessage(conversation, getString(R.string.contact_is_typing, conversation.getName())));
+ } else if (state == ChatState.PAUSED) {
+ this.messageList.add(Message.createStatusMessage(conversation, getString(R.string.contact_has_stopped_typing, conversation.getName())));
+ } else {
+ for (int i = this.messageList.size() - 1; i >= 0; --i) {
+ if (this.messageList.get(i).getStatus() == Message.STATUS_RECEIVED) {
+ return;
+ } else {
+ if (this.messageList.get(i).getStatus() == Message.STATUS_SEND_DISPLAYED) {
+ this.messageList.add(i + 1,
+ Message.createStatusMessage(conversation, getString(R.string.contact_has_read_up_to_this_point, conversation.getName())));
+ return;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ protected void showSnackbar(final int message, final int action, final OnClickListener clickListener) {
+ snackbar.setVisibility(View.VISIBLE);
+ snackbar.setOnClickListener(null);
+ snackbarMessage.setText(message);
+ snackbarMessage.setOnClickListener(null);
+ snackbarAction.setVisibility(clickListener == null ? View.GONE : View.VISIBLE);
+ if (action != 0) {
+ snackbarAction.setText(action);
+ }
+ snackbarAction.setOnClickListener(clickListener);
+ }
+
+ protected void hideSnackbar() {
+ snackbar.setVisibility(View.GONE);
+ }
+
+ protected void sendPlainTextMessage(Message message) {
+ ConversationActivity activity = (ConversationActivity) getActivity();
+ activity.xmppConnectionService.sendMessage(message);
+ messageSent();
+ }
+
+ protected void sendPgpMessage(final Message message) {
+ final ConversationActivity activity = (ConversationActivity) getActivity();
+ final XmppConnectionService xmppService = activity.xmppConnectionService;
+ final Contact contact = message.getConversation().getContact();
+ if (!activity.hasPgp()) {
+ activity.showInstallPgpDialog();
+ return;
+ }
+ if (conversation.getAccount().getPgpSignature() == null) {
+ activity.announcePgp(conversation.getAccount(), conversation);
+ return;
+ }
+ if (conversation.getMode() == Conversation.MODE_SINGLE) {
+ if (contact.getPgpKeyId() != 0) {
+ xmppService.getPgpEngine().hasKey(contact,
+ new UiCallback<Contact>() {
+
+ @Override
+ public void userInputRequried(PendingIntent pi,
+ Contact contact) {
+ activity.runIntent(
+ pi,
+ ConversationActivity.REQUEST_ENCRYPT_MESSAGE);
+ }
+
+ @Override
+ public void success(Contact contact) {
+ messageSent();
+ activity.encryptTextMessage(message);
+ }
+
+ @Override
+ public void error(int error, Contact contact) {
+ System.out.println();
+ }
+ });
+
+ } else {
+ showNoPGPKeyDialog(false,
+ new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog,
+ int which) {
+ conversation
+ .setNextEncryption(Message.ENCRYPTION_NONE);
+ xmppService.databaseBackend
+ .updateConversation(conversation);
+ message.setEncryption(Message.ENCRYPTION_NONE);
+ xmppService.sendMessage(message);
+ messageSent();
+ }
+ });
+ }
+ } else {
+ if (conversation.getMucOptions().pgpKeysInUse()) {
+ if (!conversation.getMucOptions().everybodyHasKeys()) {
+ Toast warning = Toast
+ .makeText(getActivity(),
+ R.string.missing_public_keys,
+ Toast.LENGTH_LONG);
+ warning.setGravity(Gravity.CENTER_VERTICAL, 0, 0);
+ warning.show();
+ }
+ activity.encryptTextMessage(message);
+ messageSent();
+ } else {
+ showNoPGPKeyDialog(true,
+ new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog,
+ int which) {
+ conversation
+ .setNextEncryption(Message.ENCRYPTION_NONE);
+ message.setEncryption(Message.ENCRYPTION_NONE);
+ xmppService.databaseBackend
+ .updateConversation(conversation);
+ xmppService.sendMessage(message);
+ messageSent();
+ }
+ });
+ }
+ }
+ }
+
+ public void showNoPGPKeyDialog(boolean plural,
+ DialogInterface.OnClickListener listener) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setIconAttribute(android.R.attr.alertDialogIcon);
+ if (plural) {
+ builder.setTitle(getString(R.string.no_pgp_keys));
+ builder.setMessage(getText(R.string.contacts_have_no_pgp_keys));
+ } else {
+ builder.setTitle(getString(R.string.no_pgp_key));
+ builder.setMessage(getText(R.string.contact_has_no_pgp_key));
+ }
+ builder.setNegativeButton(getString(R.string.cancel), null);
+ builder.setPositiveButton(getString(R.string.send_unencrypted),
+ listener);
+ builder.create().show();
+ }
+
+ protected void sendAxolotlMessage(final Message message) {
+ final ConversationActivity activity = (ConversationActivity) getActivity();
+ final XmppConnectionService xmppService = activity.xmppConnectionService;
+ xmppService.sendMessage(message);
+ messageSent();
+ }
+
+ protected void sendOtrMessage(final Message message) {
+ final ConversationActivity activity = (ConversationActivity) getActivity();
+ final XmppConnectionService xmppService = activity.xmppConnectionService;
+ activity.selectPresence(message.getConversation(),
+ new OnPresenceSelected() {
+
+ @Override
+ public void onPresenceSelected() {
+ message.setCounterpart(conversation.getNextCounterpart());
+ xmppService.sendMessage(message);
+ messageSent();
+ }
+ });
+ }
+
+ public void appendText(String text) {
+ if (text == null) {
+ return;
+ }
+ String previous = this.mEditMessage.getText().toString();
+ if (previous.length() != 0 && !previous.endsWith(" ")) {
+ text = " " + text;
+ }
+ this.mEditMessage.append(text);
+ }
+
+ @Override
+ public boolean onEnterPressed() {
+ if (ConversationsPlusPreferences.enterIsSend()) {
+ sendMessage();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public void onTypingStarted() {
+ Account.State status = conversation.getAccount().getStatus();
+ if (status == Account.State.ONLINE && conversation.setOutgoingChatState(ChatState.COMPOSING)) {
+ activity.xmppConnectionService.sendChatState(conversation);
+ }
+ activity.hideConversationsOverview();
+ updateSendButton();
+ }
+
+ @Override
+ public void onTypingStopped() {
+ Account.State status = conversation.getAccount().getStatus();
+ if (status == Account.State.ONLINE && conversation.setOutgoingChatState(ChatState.PAUSED)) {
+ activity.xmppConnectionService.sendChatState(conversation);
+ }
+ }
+
+ @Override
+ public void onTextDeleted() {
+ Account.State status = conversation.getAccount().getStatus();
+ if (status == Account.State.ONLINE && conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) {
+ activity.xmppConnectionService.sendChatState(conversation);
+ }
+ updateSendButton();
+ }
+
+ private int completionIndex = 0;
+ private int lastCompletionLength = 0;
+ private String incomplete;
+ private int lastCompletionCursor;
+ private boolean firstWord = false;
+
+ @Override
+ public boolean onTabPressed(boolean repeated) {
+ if (conversation == null || conversation.getMode() == Conversation.MODE_SINGLE) {
+ return false;
+ }
+ if (repeated) {
+ completionIndex++;
+ } else {
+ lastCompletionLength = 0;
+ completionIndex = 0;
+ final String content = mEditMessage.getText().toString();
+ lastCompletionCursor = mEditMessage.getSelectionEnd();
+ int start = lastCompletionCursor > 0 ? content.lastIndexOf(" ",lastCompletionCursor-1) + 1 : 0;
+ firstWord = start == 0;
+ incomplete = content.substring(start,lastCompletionCursor);
+ }
+ List<String> completions = new ArrayList<>();
+ for(MucOptions.User user : conversation.getMucOptions().getUsers()) {
+ if (user.getName().startsWith(incomplete)) {
+ completions.add(user.getName()+(firstWord ? ": " : " "));
+ }
+ }
+ Collections.sort(completions);
+ if (completions.size() > completionIndex) {
+ String completion = completions.get(completionIndex).substring(incomplete.length());
+ mEditMessage.getEditableText().delete(lastCompletionCursor,lastCompletionCursor + lastCompletionLength);
+ mEditMessage.getEditableText().insert(lastCompletionCursor, completion);
+ lastCompletionLength = completion.length();
+ } else {
+ completionIndex = -1;
+ mEditMessage.getEditableText().delete(lastCompletionCursor,lastCompletionCursor + lastCompletionLength);
+ lastCompletionLength = 0;
+ }
+ return true;
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode,
+ final Intent data) {
+ if (resultCode == Activity.RESULT_OK) {
+ if (requestCode == ConversationActivity.REQUEST_DECRYPT_PGP) {
+ activity.getSelectedConversation().getAccount().getPgpDecryptionService().onKeychainUnlocked();
+ keychainUnlock = KEYCHAIN_UNLOCK_NOT_REQUIRED;
+ updatePgpMessages();
+ } else if (requestCode == ConversationActivity.REQUEST_TRUST_KEYS_TEXT) {
+ final String body = mEditMessage.getText().toString();
+ Message message = new Message(conversation, body, conversation.getNextEncryption());
+ sendAxolotlMessage(message);
+ } else if (requestCode == ConversationActivity.REQUEST_TRUST_KEYS_MENU) {
+ int choice = data.getIntExtra("choice", ConversationActivity.ATTACHMENT_CHOICE_INVALID);
+ activity.selectPresenceToAttachFile(choice, conversation.getNextEncryption());
+ }
+ } else {
+ if (requestCode == ConversationActivity.REQUEST_DECRYPT_PGP) {
+ keychainUnlock = KEYCHAIN_UNLOCK_NOT_REQUIRED;
+ updatePgpMessages();
+ }
+ }
+ }
+
+ private void changeEmojiKeyboardIcon(ImageView iconToBeChanged, int drawableResourceId){
+ iconToBeChanged.setImageResource(drawableResourceId);
+ }
+
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java
new file mode 100644
index 00000000..d63516bc
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java
@@ -0,0 +1,970 @@
+package eu.siacs.conversations.ui;
+
+import android.app.AlertDialog;
+import android.app.AlertDialog.Builder;
+import android.app.PendingIntent;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.security.KeyChain;
+import android.security.KeyChainAliasCallback;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.AutoCompleteTextView;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
+import android.widget.TableLayout;
+import android.widget.TableRow;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import de.thedevstack.conversationsplus.ConversationsPlusColors;
+import de.thedevstack.conversationsplus.ConversationsPlusPreferences;
+import de.thedevstack.conversationsplus.ui.listeners.ShowResourcesListDialogListener;
+import de.thedevstack.conversationsplus.utils.ui.TextViewUtil;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.crypto.axolotl.AxolotlService;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.services.AvatarService;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
+import eu.siacs.conversations.services.XmppConnectionService.OnCaptchaRequested;
+import eu.siacs.conversations.ui.adapter.KnownHostsAdapter;
+import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.utils.UIHelper;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import eu.siacs.conversations.xmpp.XmppConnection.Features;
+import eu.siacs.conversations.xmpp.forms.Data;
+import eu.siacs.conversations.xmpp.jid.InvalidJidException;
+import eu.siacs.conversations.xmpp.jid.Jid;
+import eu.siacs.conversations.xmpp.pep.Avatar;
+
+public class EditAccountActivity extends XmppActivity implements OnAccountUpdate,
+ OnKeyStatusUpdated, OnCaptchaRequested, KeyChainAliasCallback, XmppConnectionService.OnShowErrorToast, XmppConnectionService.OnMamPreferencesFetched {
+
+ private AutoCompleteTextView mAccountJid;
+ private EditText mPassword;
+ private EditText mPasswordConfirm;
+ private CheckBox mRegisterNew;
+ private Button mCancelButton;
+ private Button mSaveButton;
+ private Button mDisableBatterOptimizations;
+ private TableLayout mMoreTable;
+
+ private LinearLayout mStats;
+ private RelativeLayout mBatteryOptimizations;
+ private TextView mServerInfoSm;
+ private TextView mServerInfoRosterVersion;
+ private TextView mServerInfoCarbons;
+ private TextView mServerInfoMam;
+ private TextView mServerInfoCSI;
+ private TextView mServerInfoBlocking;
+ private TextView mServerInfoPep;
+ private TextView mServerInfoHttpUpload;
+ private TextView mServerInfoPush;
+ private TextView mSessionEst;
+ private TextView mOtrFingerprint;
+ private TextView mAxolotlFingerprint;
+ private TextView mAccountJidLabel;
+ private ImageView mAvatar;
+ private RelativeLayout mOtrFingerprintBox;
+ private RelativeLayout mAxolotlFingerprintBox;
+ private ImageButton mOtrFingerprintToClipboardButton;
+ private ImageButton mAxolotlFingerprintToClipboardButton;
+ private ImageButton mRegenerateAxolotlKeyButton;
+ private LinearLayout keys;
+ private LinearLayout keysCard;
+ private LinearLayout mNamePort;
+ private EditText mHostname;
+ private EditText mPort;
+ private AlertDialog mCaptchaDialog = null;
+
+ private Jid jidToEdit;
+ private boolean mInitMode = false;
+ private boolean mShowOptions = false;
+ private Account mAccount;
+ private String messageFingerprint;
+
+ private boolean mFetchingAvatar = false;
+
+ private final OnClickListener mSaveButtonClickListener = new OnClickListener() {
+
+ @Override
+ public void onClick(final View v) {
+ if (mInitMode && mAccount != null) {
+ mAccount.setOption(Account.OPTION_DISABLED, false);
+ }
+ if (mAccount != null && mAccount.getStatus() == Account.State.DISABLED && !accountInfoEdited()) {
+ mAccount.setOption(Account.OPTION_DISABLED, false);
+ mAccount.setStatus(Account.State.CONNECTING);
+ xmppConnectionService.updateAccount(mAccount);
+ return;
+ }
+ final boolean registerNewAccount = mRegisterNew.isChecked() && !Config.DISALLOW_REGISTRATION_IN_UI;
+ if (Config.DOMAIN_LOCK != null && mAccountJid.getText().toString().contains("@")) {
+ mAccountJid.setError(getString(R.string.invalid_username));
+ mAccountJid.requestFocus();
+ return;
+ }
+ final Jid jid;
+ try {
+ if (Config.DOMAIN_LOCK != null) {
+ jid = Jid.fromParts(mAccountJid.getText().toString(), Config.DOMAIN_LOCK, null);
+ } else {
+ jid = Jid.fromString(mAccountJid.getText().toString());
+ }
+ } catch (final InvalidJidException e) {
+ if (Config.DOMAIN_LOCK != null) {
+ mAccountJid.setError(getString(R.string.invalid_username));
+ } else {
+ mAccountJid.setError(getString(R.string.invalid_jid));
+ }
+ mAccountJid.requestFocus();
+ return;
+ }
+ String hostname = null;
+ int numericPort = 5222;
+ if (mShowOptions) {
+ hostname = mHostname.getText().toString();
+ final String port = mPort.getText().toString();
+ if (hostname.contains(" ")) {
+ mHostname.setError(getString(R.string.not_valid_hostname));
+ mHostname.requestFocus();
+ return;
+ }
+ try {
+ numericPort = Integer.parseInt(port);
+ if (numericPort < 0 || numericPort > 65535) {
+ mPort.setError(getString(R.string.not_a_valid_port));
+ mPort.requestFocus();
+ return;
+ }
+
+ } catch (NumberFormatException e) {
+ mPort.setError(getString(R.string.not_a_valid_port));
+ mPort.requestFocus();
+ return;
+ }
+ }
+
+ if (jid.isDomainJid()) {
+ if (Config.DOMAIN_LOCK != null) {
+ mAccountJid.setError(getString(R.string.invalid_username));
+ } else {
+ mAccountJid.setError(getString(R.string.invalid_jid));
+ }
+ mAccountJid.requestFocus();
+ return;
+ }
+ final String password = mPassword.getText().toString();
+ final String passwordConfirm = mPasswordConfirm.getText().toString();
+ if (registerNewAccount) {
+ if (!password.equals(passwordConfirm)) {
+ mPasswordConfirm.setError(getString(R.string.passwords_do_not_match));
+ mPasswordConfirm.requestFocus();
+ return;
+ }
+ }
+ if (mAccount != null) {
+ mAccount.setJid(jid);
+ mAccount.setPort(numericPort);
+ mAccount.setHostname(hostname);
+ mAccountJid.setError(null);
+ mPasswordConfirm.setError(null);
+ mAccount.setPassword(password);
+ mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount);
+ xmppConnectionService.updateAccount(mAccount);
+ } else {
+ if (xmppConnectionService.findAccountByJid(jid) != null) {
+ mAccountJid.setError(getString(R.string.account_already_exists));
+ mAccountJid.requestFocus();
+ return;
+ }
+ mAccount = new Account(jid.toBareJid(), password);
+ mAccount.setPort(numericPort);
+ mAccount.setHostname(hostname);
+ mAccount.setOption(Account.OPTION_USETLS, true);
+ mAccount.setOption(Account.OPTION_USECOMPRESSION, true);
+ mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount);
+ xmppConnectionService.createAccount(mAccount);
+ }
+ mHostname.setError(null);
+ mPort.setError(null);
+ if (!mAccount.isOptionSet(Account.OPTION_DISABLED)
+ && !registerNewAccount
+ && !mInitMode) {
+ finish();
+ } else {
+ updateSaveButton();
+ updateAccountInformation(true);
+ }
+
+ }
+ };
+ private final OnClickListener mCancelButtonClickListener = new OnClickListener() {
+
+ @Override
+ public void onClick(final View v) {
+ finish();
+ }
+ };
+ private Toast mFetchingMamPrefsToast;
+ private TableRow mPushRow;
+
+ public void refreshUiReal() {
+ invalidateOptionsMenu();
+ if (mAccount != null
+ && mAccount.getStatus() != Account.State.ONLINE
+ && mFetchingAvatar) {
+ startActivity(new Intent(getApplicationContext(),
+ ManageAccountActivity.class));
+ finish();
+ } else if (mInitMode && mAccount != null && mAccount.getStatus() == Account.State.ONLINE) {
+ if (!mFetchingAvatar) {
+ mFetchingAvatar = true;
+ AvatarService.getInstance().checkForAvatar(mAccount, mAvatarFetchCallback);
+ }
+ }
+ if (mAccount != null) {
+ updateAccountInformation(false);
+ }
+ updateSaveButton();
+ }
+
+ @Override
+ public void onAccountUpdate() {
+ refreshUi();
+ }
+
+ private final UiCallback<Avatar> mAvatarFetchCallback = new UiCallback<Avatar>() {
+
+ @Override
+ public void userInputRequried(final PendingIntent pi, final Avatar avatar) {
+ finishInitialSetup(avatar);
+ }
+
+ @Override
+ public void success(final Avatar avatar) {
+ finishInitialSetup(avatar);
+ }
+
+ @Override
+ public void error(final int errorCode, final Avatar avatar) {
+ finishInitialSetup(avatar);
+ }
+ };
+ private final TextWatcher mTextWatcher = new TextWatcher() {
+
+ @Override
+ public void onTextChanged(final CharSequence s, final int start, final int before, final int count) {
+ updateSaveButton();
+ }
+
+ @Override
+ public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) {
+ }
+
+ @Override
+ public void afterTextChanged(final Editable s) {
+
+ }
+ };
+
+ private final OnClickListener mAvatarClickListener = new OnClickListener() {
+ @Override
+ public void onClick(final View view) {
+ if (mAccount != null) {
+ final Intent intent = new Intent(getApplicationContext(), PublishProfilePictureActivity.class);
+ intent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().toBareJid().toString());
+ startActivity(intent);
+ }
+ }
+ };
+
+ protected void finishInitialSetup(final Avatar avatar) {
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ final Intent intent;
+ final XmppConnection connection = mAccount.getXmppConnection();
+ if (avatar != null || (connection != null && !connection.getFeatures().pep())) {
+ intent = new Intent(getApplicationContext(), StartConversationActivity.class);
+ if (xmppConnectionService != null && xmppConnectionService.getAccounts().size() == 1) {
+ intent.putExtra("init", true);
+ }
+ } else {
+ intent = new Intent(getApplicationContext(), PublishProfilePictureActivity.class);
+ intent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().toBareJid().toString());
+ intent.putExtra("setup", true);
+ }
+ startActivity(intent);
+ finish();
+ }
+ });
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == REQUEST_BATTERY_OP) {
+ updateAccountInformation(mAccount == null);
+ }
+ }
+
+ protected void updateSaveButton() {
+ if (accountInfoEdited() && !mInitMode) {
+ TextViewUtil.enable(mSaveButton, ConversationsPlusColors.primaryText(), R.string.save);
+ } else if (mAccount != null && (mAccount.getStatus() == Account.State.CONNECTING || mFetchingAvatar)) {
+ TextViewUtil.disable(mSaveButton, ConversationsPlusColors.secondaryText(), R.string.account_status_connecting);
+ } else if (mAccount != null && mAccount.getStatus() == Account.State.DISABLED && !mInitMode) {
+ TextViewUtil.enable(mSaveButton, ConversationsPlusColors.primaryText(), R.string.enable);
+ } else {
+ TextViewUtil.enable(mSaveButton, ConversationsPlusColors.primaryText());
+ if (!mInitMode) {
+ if (mAccount != null && mAccount.isOnlineAndConnected()) {
+ this.mSaveButton.setText(R.string.save);
+ if (!accountInfoEdited()) {
+ TextViewUtil.disable(mSaveButton, ConversationsPlusColors.secondaryText());
+ }
+ } else {
+ this.mSaveButton.setText(R.string.connect);
+ }
+ } else {
+ this.mSaveButton.setText(R.string.next);
+ }
+ }
+ }
+
+ protected boolean accountInfoEdited() {
+ if (this.mAccount == null) {
+ return false;
+ }
+ final String unmodified;
+ if (Config.DOMAIN_LOCK != null) {
+ unmodified = this.mAccount.getJid().getLocalpart();
+ } else {
+ unmodified = this.mAccount.getJid().toBareJid().toString();
+ }
+ return !unmodified.equals(this.mAccountJid.getText().toString()) ||
+ !this.mAccount.getPassword().equals(this.mPassword.getText().toString()) ||
+ !this.mAccount.getHostname().equals(this.mHostname.getText().toString()) ||
+ !String.valueOf(this.mAccount.getPort()).equals(this.mPort.getText().toString());
+ }
+
+ @Override
+ protected String getShareableUri() {
+ if (mAccount != null) {
+ return mAccount.getShareableUri();
+ } else {
+ return "";
+ }
+ }
+
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_edit_account);
+ this.mAccountJid = (AutoCompleteTextView) findViewById(R.id.account_jid);
+ this.mAccountJid.addTextChangedListener(this.mTextWatcher);
+ this.mAccountJidLabel = (TextView) findViewById(R.id.account_jid_label);
+ if (Config.DOMAIN_LOCK != null) {
+ this.mAccountJidLabel.setText(R.string.username);
+ this.mAccountJid.setHint(R.string.username_hint);
+ }
+ this.mPassword = (EditText) findViewById(R.id.account_password);
+ this.mPassword.addTextChangedListener(this.mTextWatcher);
+ this.mPasswordConfirm = (EditText) findViewById(R.id.account_password_confirm);
+ this.mAvatar = (ImageView) findViewById(R.id.avater);
+ this.mAvatar.setOnClickListener(this.mAvatarClickListener);
+ this.mRegisterNew = (CheckBox) findViewById(R.id.account_register_new);
+ this.mStats = (LinearLayout) findViewById(R.id.stats);
+ this.mBatteryOptimizations = (RelativeLayout) findViewById(R.id.battery_optimization);
+ this.mDisableBatterOptimizations = (Button) findViewById(R.id.batt_op_disable);
+ this.mDisableBatterOptimizations.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);
+ startActivityForResult(intent,REQUEST_BATTERY_OP);
+ }
+ });
+ this.mSessionEst = (TextView) findViewById(R.id.session_est);
+ this.mServerInfoRosterVersion = (TextView) findViewById(R.id.server_info_roster_version);
+ this.mServerInfoCarbons = (TextView) findViewById(R.id.server_info_carbons);
+ this.mServerInfoMam = (TextView) findViewById(R.id.server_info_mam);
+ this.mServerInfoCSI = (TextView) findViewById(R.id.server_info_csi);
+ this.mServerInfoBlocking = (TextView) findViewById(R.id.server_info_blocking);
+ this.mServerInfoSm = (TextView) findViewById(R.id.server_info_sm);
+ this.mServerInfoPep = (TextView) findViewById(R.id.server_info_pep);
+ this.mServerInfoHttpUpload = (TextView) findViewById(R.id.server_info_http_upload);
+ this.mPushRow = (TableRow) findViewById(R.id.push_row);
+ this.mServerInfoPush = (TextView) findViewById(R.id.server_info_push);
+ this.mOtrFingerprint = (TextView) findViewById(R.id.otr_fingerprint);
+ this.mOtrFingerprintBox = (RelativeLayout) findViewById(R.id.otr_fingerprint_box);
+ this.mOtrFingerprintToClipboardButton = (ImageButton) findViewById(R.id.action_copy_to_clipboard);
+ this.mAxolotlFingerprint = (TextView) findViewById(R.id.axolotl_fingerprint);
+ this.mAxolotlFingerprintBox = (RelativeLayout) findViewById(R.id.axolotl_fingerprint_box);
+ this.mAxolotlFingerprintToClipboardButton = (ImageButton) findViewById(R.id.action_copy_axolotl_to_clipboard);
+ this.mRegenerateAxolotlKeyButton = (ImageButton) findViewById(R.id.action_regenerate_axolotl_key);
+ this.keysCard = (LinearLayout) findViewById(R.id.other_device_keys_card);
+ this.keys = (LinearLayout) findViewById(R.id.other_device_keys);
+ this.mNamePort = (LinearLayout) findViewById(R.id.name_port);
+ this.mHostname = (EditText) findViewById(R.id.hostname);
+ this.mHostname.addTextChangedListener(mTextWatcher);
+ this.mPort = (EditText) findViewById(R.id.port);
+ this.mPort.setText("5222");
+ this.mPort.addTextChangedListener(mTextWatcher);
+ this.mSaveButton = (Button) findViewById(R.id.save_button);
+ this.mCancelButton = (Button) findViewById(R.id.cancel_button);
+ this.mSaveButton.setOnClickListener(this.mSaveButtonClickListener);
+ this.mCancelButton.setOnClickListener(this.mCancelButtonClickListener);
+ this.mMoreTable = (TableLayout) findViewById(R.id.server_info_more);
+ final OnCheckedChangeListener OnCheckedShowConfirmPassword = new OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(final CompoundButton buttonView,
+ final boolean isChecked) {
+ if (isChecked) {
+ mPasswordConfirm.setVisibility(View.VISIBLE);
+ } else {
+ mPasswordConfirm.setVisibility(View.GONE);
+ }
+ updateSaveButton();
+ }
+ };
+ this.mRegisterNew.setOnCheckedChangeListener(OnCheckedShowConfirmPassword);
+ if (Config.DISALLOW_REGISTRATION_IN_UI) {
+ this.mRegisterNew.setVisibility(View.GONE);
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(final Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ getMenuInflater().inflate(R.menu.editaccount, menu);
+ final MenuItem showQrCode = menu.findItem(R.id.action_show_qr_code);
+ final MenuItem showBlocklist = menu.findItem(R.id.action_show_block_list);
+ 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 clearDevices = menu.findItem(R.id.action_clear_devices);
+ final MenuItem renewCertificate = menu.findItem(R.id.action_renew_certificate);
+ final MenuItem mamPrefs = menu.findItem(R.id.action_mam_prefs);
+
+ renewCertificate.setVisible(mAccount != null && mAccount.getPrivateKeyAlias() != null);
+
+ if (mAccount != null && mAccount.isOnlineAndConnected()) {
+ if (!mAccount.getXmppConnection().getFeatures().blocking()) {
+ showBlocklist.setVisible(false);
+ }
+ if (!mAccount.getXmppConnection().getFeatures().register()) {
+ changePassword.setVisible(false);
+ }
+ mamPrefs.setVisible(mAccount.getXmppConnection().getFeatures().mam());
+ Set<Integer> otherDevices = mAccount.getAxolotlService().getOwnDeviceIds();
+ if (otherDevices == null || otherDevices.isEmpty()) {
+ clearDevices.setVisible(false);
+ }
+ } else {
+ showQrCode.setVisible(false);
+ showBlocklist.setVisible(false);
+ showMoreInfo.setVisible(false);
+ changePassword.setVisible(false);
+ clearDevices.setVisible(false);
+ mamPrefs.setVisible(false);
+ }
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ if (getIntent() != null) {
+ try {
+ this.jidToEdit = Jid.fromString(getIntent().getStringExtra("jid"));
+ } catch (final InvalidJidException | NullPointerException ignored) {
+ this.jidToEdit = null;
+ }
+ this.mInitMode = getIntent().getBooleanExtra("init", false) || this.jidToEdit == null;
+ this.messageFingerprint = getIntent().getStringExtra("fingerprint");
+ if (!mInitMode) {
+ this.mRegisterNew.setVisibility(View.GONE);
+ if (getActionBar() != null) {
+ getActionBar().setTitle(getString(R.string.account_details));
+ }
+ } else {
+ this.mAvatar.setVisibility(View.GONE);
+ if (getActionBar() != null) {
+ getActionBar().setTitle(R.string.action_add_account);
+ }
+ }
+ }
+ this.mShowOptions = ConversationsPlusPreferences.showConnectionOptions();
+ this.mNamePort.setVisibility(mShowOptions ? View.VISIBLE : View.GONE);
+ }
+
+ @Override
+ protected void onBackendConnected() {
+ if (this.jidToEdit != null) {
+ this.mAccount = xmppConnectionService.findAccountByJid(jidToEdit);
+ this.mInitMode |= this.mAccount.isOptionSet(Account.OPTION_REGISTER);
+ if (this.mAccount != null) {
+ if (this.mAccount.getPrivateKeyAlias() != null) {
+ this.mPassword.setHint(R.string.authenticate_with_certificate);
+ if (this.mInitMode) {
+ this.mPassword.requestFocus();
+ }
+ }
+ updateAccountInformation(true);
+ }
+ } else if (this.xmppConnectionService.getAccounts().size() == 0) {
+ if (getActionBar() != null) {
+ getActionBar().setDisplayHomeAsUpEnabled(false);
+ getActionBar().setDisplayShowHomeEnabled(false);
+ getActionBar().setHomeButtonEnabled(false);
+ }
+ TextViewUtil.disable(mCancelButton, ConversationsPlusColors.secondaryText());
+ }
+ if (Config.DOMAIN_LOCK == null) {
+ final KnownHostsAdapter mKnownHostsAdapter = new KnownHostsAdapter(this,
+ android.R.layout.simple_list_item_1,
+ xmppConnectionService.getKnownHosts());
+ this.mAccountJid.setAdapter(mKnownHostsAdapter);
+ }
+ updateSaveButton();
+ invalidateOptionsMenu();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.action_show_block_list:
+ final Intent showBlocklistIntent = new Intent(this, BlocklistActivity.class);
+ showBlocklistIntent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().toString());
+ startActivity(showBlocklistIntent);
+ break;
+ case R.id.action_server_info_show_more:
+ mMoreTable.setVisibility(item.isChecked() ? View.GONE : View.VISIBLE);
+ item.setChecked(!item.isChecked());
+ break;
+ case R.id.action_change_password_on_server:
+ final Intent changePasswordIntent = new Intent(this, ChangePasswordActivity.class);
+ changePasswordIntent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().toString());
+ startActivity(changePasswordIntent);
+ break;
+ case R.id.action_mam_prefs:
+ editMamPrefs();
+ break;
+ case R.id.action_clear_devices:
+ showWipePepDialog();
+ break;
+ case R.id.action_renew_certificate:
+ renewCertificate();
+ break;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private void renewCertificate() {
+ KeyChain.choosePrivateKeyAlias(this, this, null, null, null, -1, null);
+ }
+
+ @Override
+ public void alias(String alias) {
+ if (alias != null) {
+ xmppConnectionService.updateKeyInAccount(mAccount, alias);
+ }
+ }
+
+ private void updateAccountInformation(boolean init) {
+ if (init) {
+ this.mAccountJid.getEditableText().clear();
+ if (Config.DOMAIN_LOCK != null) {
+ this.mAccountJid.getEditableText().append(this.mAccount.getJid().getLocalpart());
+ } else {
+ this.mAccountJid.getEditableText().append(this.mAccount.getJid().toBareJid().toString());
+ }
+ this.mPassword.setText(this.mAccount.getPassword());
+ this.mHostname.setText("");
+ this.mHostname.getEditableText().append(this.mAccount.getHostname());
+ this.mPort.setText("");
+ this.mPort.getEditableText().append(String.valueOf(this.mAccount.getPort()));
+ this.mNamePort.setVisibility(mShowOptions ? View.VISIBLE : View.GONE);
+
+ }
+ mPassword.setEnabled(!Config.LOCK_SETTINGS);
+ mAccountJid.setEnabled(!Config.LOCK_SETTINGS);
+ mHostname.setEnabled(!Config.LOCK_SETTINGS);
+ mPort.setEnabled(!Config.LOCK_SETTINGS);
+ mPasswordConfirm.setEnabled(!Config.LOCK_SETTINGS);
+ mRegisterNew.setEnabled(!Config.LOCK_SETTINGS);
+
+ if (!mInitMode) {
+ this.mAvatar.setVisibility(View.VISIBLE);
+ this.mAvatar.setImageBitmap(AvatarService.getInstance().get(this.mAccount, getPixel(72)));
+ } else {
+ this.mAvatar.setVisibility(View.GONE);
+ }
+ if (this.mAccount.isOptionSet(Account.OPTION_REGISTER)) {
+ this.mRegisterNew.setVisibility(View.VISIBLE);
+ this.mRegisterNew.setChecked(true);
+ this.mPasswordConfirm.setText(this.mAccount.getPassword());
+ } else {
+ this.mRegisterNew.setVisibility(View.GONE);
+ this.mRegisterNew.setChecked(false);
+ }
+ if (this.mAccount.isOnlineAndConnected() && !this.mFetchingAvatar) {
+ this.findViewById(R.id.editAccountBoxes).setVisibility(View.GONE);
+ this.findViewById(R.id.displayAccountFrame).setVisibility(View.VISIBLE);
+ TextView detailsAccountJid = (TextView)this.findViewById(R.id.detailsAccountJid);
+ if (this.mAccount.countPresences() > 0) {
+ detailsAccountJid.setText(this.mAccount.getJid().toBareJid().toString() + " (" + this.mAccount.countPresences() + ")");
+ detailsAccountJid.setOnClickListener(new ShowResourcesListDialogListener(EditAccountActivity.this, this.mAccount.getRoster().getContact(this.mAccount.getJid().toBareJid())));
+ } else {
+ detailsAccountJid.setText(this.mAccount.getJid().toBareJid().toString());
+ }
+ Features features = this.mAccount.getXmppConnection().getFeatures();
+ this.mStats.setVisibility(View.VISIBLE);
+ boolean showOptimizingWarning = !xmppConnectionService.getPushManagementService().available(mAccount) && isOptimizingBattery();
+ this.mBatteryOptimizations.setVisibility(showOptimizingWarning ? View.VISIBLE : View.GONE);
+ this.mSessionEst.setText(UIHelper.readableTimeDifferenceFull(this, this.mAccount.getXmppConnection()
+ .getLastSessionEstablished()));
+ if (features.rosterVersioning()) {
+ this.mServerInfoRosterVersion.setText(R.string.server_info_available);
+ } else {
+ this.mServerInfoRosterVersion.setText(R.string.server_info_unavailable);
+ }
+ if (features.carbons()) {
+ this.mServerInfoCarbons.setText(R.string.server_info_available);
+ } else {
+ this.mServerInfoCarbons
+ .setText(R.string.server_info_unavailable);
+ }
+ if (features.mam()) {
+ this.mServerInfoMam.setText(R.string.server_info_available);
+ } else {
+ this.mServerInfoMam.setText(R.string.server_info_unavailable);
+ }
+ if (features.csi()) {
+ this.mServerInfoCSI.setText(R.string.server_info_available);
+ } else {
+ this.mServerInfoCSI.setText(R.string.server_info_unavailable);
+ }
+ if (features.blocking()) {
+ this.mServerInfoBlocking.setText(R.string.server_info_available);
+ } else {
+ this.mServerInfoBlocking.setText(R.string.server_info_unavailable);
+ }
+ if (features.sm()) {
+ this.mServerInfoSm.setText(R.string.server_info_available);
+ } else {
+ this.mServerInfoSm.setText(R.string.server_info_unavailable);
+ }
+ if (features.pep()) {
+ AxolotlService axolotlService = this.mAccount.getAxolotlService();
+ if (axolotlService != null && axolotlService.isPepBroken()) {
+ this.mServerInfoPep.setText(R.string.server_info_broken);
+ } else {
+ this.mServerInfoPep.setText(R.string.server_info_available);
+ }
+ } else {
+ this.mServerInfoPep.setText(R.string.server_info_unavailable);
+ }
+ if (features.httpUpload(0)) {
+ this.mServerInfoHttpUpload.setText(R.string.server_info_available);
+ } else {
+ this.mServerInfoHttpUpload.setText(R.string.server_info_unavailable);
+ }
+
+ this.mPushRow.setVisibility(xmppConnectionService.getPushManagementService().isStub() ? View.GONE : View.VISIBLE);
+
+ if (xmppConnectionService.getPushManagementService().available(mAccount)) {
+ this.mServerInfoPush.setText(R.string.server_info_available);
+ } else {
+ this.mServerInfoPush.setText(R.string.server_info_unavailable);
+ }
+ final String otrFingerprint = this.mAccount.getOtrFingerprint();
+ if (otrFingerprint != null) {
+ this.mOtrFingerprintBox.setVisibility(View.VISIBLE);
+ this.mOtrFingerprint.setText(CryptoHelper.prettifyFingerprint(otrFingerprint));
+ this.mOtrFingerprintToClipboardButton
+ .setVisibility(View.VISIBLE);
+ this.mOtrFingerprintToClipboardButton
+ .setOnClickListener(new View.OnClickListener() {
+
+ @Override
+ public void onClick(final View v) {
+
+ if (copyTextToClipboard(otrFingerprint, R.string.otr_fingerprint)) {
+ Toast.makeText(
+ EditAccountActivity.this,
+ R.string.toast_message_otr_fingerprint,
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+ });
+ } else {
+ this.mOtrFingerprintBox.setVisibility(View.GONE);
+ }
+ final String axolotlFingerprint = this.mAccount.getAxolotlService().getOwnFingerprint();
+ if (axolotlFingerprint != null) {
+ this.mAxolotlFingerprintBox.setVisibility(View.VISIBLE);
+ this.mAxolotlFingerprint.setText(CryptoHelper.prettifyFingerprint(axolotlFingerprint.substring(2)));
+ this.mAxolotlFingerprintToClipboardButton
+ .setVisibility(View.VISIBLE);
+ this.mAxolotlFingerprintToClipboardButton
+ .setOnClickListener(new View.OnClickListener() {
+
+ @Override
+ public void onClick(final View v) {
+
+ if (copyTextToClipboard(axolotlFingerprint.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 {
+ this.mAxolotlFingerprintBox.setVisibility(View.GONE);
+ }
+ final String ownFingerprint = mAccount.getAxolotlService().getOwnFingerprint();
+ boolean hasKeys = false;
+ keys.removeAllViews();
+ for (final String fingerprint : mAccount.getAxolotlService().getFingerprintsForOwnSessions()) {
+ if (ownFingerprint.equals(fingerprint)) {
+ continue;
+ }
+ boolean highlight = fingerprint.equals(messageFingerprint);
+ hasKeys |= addFingerprintRow(keys, mAccount, fingerprint, highlight, null);
+ }
+ if (hasKeys) {
+ keysCard.setVisibility(View.VISIBLE);
+ } else {
+ keysCard.setVisibility(View.GONE);
+ }
+ } else {
+ if (this.mAccount.errorStatus()) {
+ final EditText errorTextField;
+ if (this.mAccount.getStatus() == Account.State.UNAUTHORIZED) {
+ errorTextField = this.mPassword;
+ } else if (mShowOptions
+ && this.mAccount.getStatus() == Account.State.SERVER_NOT_FOUND
+ && this.mHostname.getText().length() > 0) {
+ errorTextField = this.mHostname;
+ } else {
+ errorTextField = this.mAccountJid;
+ }
+ errorTextField.setError(getString(this.mAccount.getStatus().getReadableId()));
+ if (init || !accountInfoEdited()) {
+ errorTextField.requestFocus();
+ }
+ } else {
+ this.mAccountJid.setError(null);
+ this.mPassword.setError(null);
+ this.mHostname.setError(null);
+ }
+ this.mStats.setVisibility(View.GONE);
+ }
+ }
+
+ public void showRegenerateAxolotlKeyDialog() {
+ Builder builder = new Builder(this);
+ builder.setTitle("Regenerate Key");
+ builder.setIconAttribute(android.R.attr.alertDialogIcon);
+ builder.setMessage("Are you sure you want to regenerate your Identity Key? (This will also wipe all established sessions and contact Identity Keys)");
+ builder.setNegativeButton(getString(R.string.cancel), null);
+ builder.setPositiveButton("Yes",
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ mAccount.getAxolotlService().regenerateKeys(false);
+ }
+ });
+ builder.create().show();
+ }
+
+ public void showWipePepDialog() {
+ Builder builder = new Builder(this);
+ builder.setTitle(getString(R.string.clear_other_devices));
+ builder.setIconAttribute(android.R.attr.alertDialogIcon);
+ builder.setMessage(getString(R.string.clear_other_devices_desc));
+ builder.setNegativeButton(getString(R.string.cancel), null);
+ builder.setPositiveButton(getString(R.string.accept),
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ mAccount.getAxolotlService().wipeOtherPepDevices();
+ }
+ });
+ builder.create().show();
+ }
+
+ private void editMamPrefs() {
+ this.mFetchingMamPrefsToast = Toast.makeText(this, R.string.fetching_mam_prefs, Toast.LENGTH_LONG);
+ this.mFetchingMamPrefsToast.show();
+ xmppConnectionService.fetchMamPreferences(mAccount, this);
+ }
+
+ @Override
+ public void onKeyStatusUpdated(AxolotlService.FetchStatus report) {
+ refreshUi();
+ }
+
+ @Override
+ public void onCaptchaRequested(final Account account, final String id, final Data data,
+ final Bitmap captcha) {
+ final AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ final ImageView view = new ImageView(this);
+ final LinearLayout layout = new LinearLayout(this);
+ final EditText input = new EditText(this);
+
+ view.setImageBitmap(captcha);
+ view.setScaleType(ImageView.ScaleType.FIT_CENTER);
+
+ input.setHint(getString(R.string.captcha_hint));
+
+ layout.setOrientation(LinearLayout.VERTICAL);
+ layout.addView(view);
+ layout.addView(input);
+
+ builder.setTitle(getString(R.string.captcha_required));
+ builder.setView(layout);
+
+ builder.setPositiveButton(getString(R.string.ok),
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ String rc = input.getText().toString();
+ data.put("username", account.getUsername());
+ data.put("password", account.getPassword());
+ data.put("ocr", rc);
+ data.submit();
+
+ if (xmppConnectionServiceBound) {
+ xmppConnectionService.sendCreateAccountWithCaptchaPacket(
+ account, id, data);
+ }
+ }
+ });
+ builder.setNegativeButton(getString(R.string.cancel), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (xmppConnectionService != null) {
+ xmppConnectionService.sendCreateAccountWithCaptchaPacket(account, null, null);
+ }
+ }
+ });
+
+ builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ if (xmppConnectionService != null) {
+ xmppConnectionService.sendCreateAccountWithCaptchaPacket(account, null, null);
+ }
+ }
+ });
+
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if ((mCaptchaDialog != null) && mCaptchaDialog.isShowing()) {
+ mCaptchaDialog.dismiss();
+ }
+ mCaptchaDialog = builder.create();
+ mCaptchaDialog.show();
+ }
+ });
+ }
+
+ public void onShowErrorToast(final int resId) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(EditAccountActivity.this, resId, Toast.LENGTH_SHORT).show();
+ }
+ });
+ }
+
+ @Override
+ public void onPreferencesFetched(final Element prefs) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (mFetchingMamPrefsToast != null) {
+ mFetchingMamPrefsToast.cancel();
+ }
+ AlertDialog.Builder builder = new Builder(EditAccountActivity.this);
+ builder.setTitle(R.string.server_side_mam_prefs);
+ String defaultAttr = prefs.getAttribute("default");
+ final List<String> defaults = Arrays.asList("never", "roster", "always");
+ final AtomicInteger choice = new AtomicInteger(Math.max(0,defaults.indexOf(defaultAttr)));
+ builder.setSingleChoiceItems(R.array.mam_prefs, choice.get(), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ choice.set(which);
+ }
+ });
+ builder.setNegativeButton(R.string.cancel, null);
+ builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ prefs.setAttribute("default",defaults.get(choice.get()));
+ xmppConnectionService.pushMamPreferences(mAccount, prefs);
+ }
+ });
+ builder.create().show();
+ }
+ });
+ }
+
+ @Override
+ public void onPreferencesFetchFailed() {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (mFetchingMamPrefsToast != null) {
+ mFetchingMamPrefsToast.cancel();
+ }
+ Toast.makeText(EditAccountActivity.this,R.string.unable_to_fetch_mam_prefs,Toast.LENGTH_LONG).show();
+ }
+ });
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/EditMessage.java b/src/main/java/eu/siacs/conversations/ui/EditMessage.java
new file mode 100644
index 00000000..06868a98
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/EditMessage.java
@@ -0,0 +1,90 @@
+package eu.siacs.conversations.ui;
+
+import android.content.Context;
+import android.os.Handler;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+
+import eu.siacs.conversations.Config;
+import github.ankushsachdeva.emojicon.EmojiconEditText;
+
+public class EditMessage extends EmojiconEditText {
+
+ public EditMessage(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public EditMessage(Context context) {
+ super(context);
+ }
+
+ protected Handler mTypingHandler = new Handler();
+
+ protected Runnable mTypingTimeout = new Runnable() {
+ @Override
+ public void run() {
+ if (isUserTyping && keyboardListener != null) {
+ keyboardListener.onTypingStopped();
+ isUserTyping = false;
+ }
+ }
+ };
+
+ private boolean isUserTyping = false;
+
+ private boolean lastInputWasTab = false;
+
+ protected KeyboardListener keyboardListener;
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent e) {
+ if (keyCode == KeyEvent.KEYCODE_ENTER && !e.isShiftPressed()) {
+ lastInputWasTab = false;
+ if (keyboardListener != null && keyboardListener.onEnterPressed()) {
+ return true;
+ }
+ } else if (keyCode == KeyEvent.KEYCODE_TAB && !e.isAltPressed() && !e.isCtrlPressed()) {
+ if (keyboardListener != null && keyboardListener.onTabPressed(this.lastInputWasTab)) {
+ lastInputWasTab = true;
+ return true;
+ }
+ } else {
+ lastInputWasTab = false;
+ }
+ return super.onKeyDown(keyCode, e);
+ }
+
+ @Override
+ public void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
+ super.onTextChanged(text,start,lengthBefore,lengthAfter);
+ lastInputWasTab = false;
+ if (this.mTypingHandler != null && this.keyboardListener != null) {
+ this.mTypingHandler.removeCallbacks(mTypingTimeout);
+ this.mTypingHandler.postDelayed(mTypingTimeout, Config.TYPING_TIMEOUT * 1000);
+ final int length = text.length();
+ if (!isUserTyping && length > 0) {
+ this.isUserTyping = true;
+ this.keyboardListener.onTypingStarted();
+ } else if (length == 0) {
+ this.isUserTyping = false;
+ this.keyboardListener.onTextDeleted();
+ }
+ }
+ }
+
+ public void setKeyboardListener(KeyboardListener listener) {
+ this.keyboardListener = listener;
+ if (listener != null) {
+ this.isUserTyping = false;
+ }
+ }
+
+ public interface KeyboardListener {
+ boolean onEnterPressed();
+ void onTypingStarted();
+ void onTypingStopped();
+ void onTextDeleted();
+ boolean onTabPressed(boolean repeated);
+ }
+
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java b/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java
new file mode 100644
index 00000000..a6b3c73c
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java
@@ -0,0 +1,134 @@
+package eu.siacs.conversations.ui;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ArrayAdapter;
+import android.widget.AutoCompleteTextView;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import java.util.List;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.ui.adapter.KnownHostsAdapter;
+import eu.siacs.conversations.xmpp.jid.InvalidJidException;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+public class EnterJidDialog {
+ public interface OnEnterJidDialogPositiveListener {
+ boolean onEnterJidDialogPositive(Jid account, Jid contact) throws EnterJidDialog.JidError;
+ }
+
+ public static class JidError extends Exception {
+ final String msg;
+
+ public JidError(final String msg) {
+ this.msg = msg;
+ }
+
+ public String toString() {
+ return msg;
+ }
+ }
+
+ protected final AlertDialog dialog;
+ protected View.OnClickListener dialogOnClick;
+ protected OnEnterJidDialogPositiveListener listener = null;
+
+ public EnterJidDialog(
+ final Context context, List<String> knownHosts, final List<String> activatedAccounts,
+ final String title, final String positiveButton,
+ final String prefilledJid, final String account, boolean allowEditJid
+ ) {
+ final boolean lock = Config.LOCK_DOMAINS_IN_CONVERSATIONS && Config.DOMAIN_LOCK != null;
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(title);
+ View dialogView = LayoutInflater.from(context).inflate(R.layout.enter_jid_dialog, null);
+ final TextView jabberIdDesc = (TextView) dialogView.findViewById(R.id.jabber_id);
+ jabberIdDesc.setText(lock ? R.string.username : R.string.account_settings_jabber_id);
+ final Spinner spinner = (Spinner) dialogView.findViewById(R.id.account);
+ final AutoCompleteTextView jid = (AutoCompleteTextView) dialogView.findViewById(R.id.jid);
+ if (!lock) {
+ jid.setAdapter(new KnownHostsAdapter(context, android.R.layout.simple_list_item_1, knownHosts));
+ }
+ if (prefilledJid != null) {
+ jid.append(prefilledJid);
+ if (!allowEditJid) {
+ jid.setFocusable(false);
+ jid.setFocusableInTouchMode(false);
+ jid.setClickable(false);
+ jid.setCursorVisible(false);
+ }
+ }
+
+ jid.setHint(Config.LOCK_DOMAINS_IN_CONVERSATIONS && Config.DOMAIN_LOCK != null ? R.string.username_hint : R.string.account_settings_example_jabber_id);
+
+ if (account == null) {
+ StartConversationActivity.populateAccountSpinner(context, activatedAccounts, spinner);
+ } else {
+ ArrayAdapter<String> adapter = new ArrayAdapter<>(context,
+ android.R.layout.simple_spinner_item,
+ new String[] { account });
+ spinner.setEnabled(false);
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ spinner.setAdapter(adapter);
+ }
+
+ builder.setView(dialogView);
+ builder.setNegativeButton(R.string.cancel, null);
+ builder.setPositiveButton(positiveButton, null);
+ this.dialog = builder.create();
+
+ this.dialogOnClick = new View.OnClickListener() {
+ @Override
+ public void onClick(final View v) {
+ final Jid accountJid;
+ if (!spinner.isEnabled() && account == null) {
+ return;
+ }
+ try {
+ if (Config.DOMAIN_LOCK != null) {
+ accountJid = Jid.fromParts((String) spinner.getSelectedItem(), Config.DOMAIN_LOCK, null);
+ } else {
+ accountJid = Jid.fromString((String) spinner.getSelectedItem());
+ }
+ } catch (final InvalidJidException e) {
+ return;
+ }
+ final Jid contactJid;
+ try {
+ if (lock) {
+ contactJid = Jid.fromParts(jid.getText().toString(), Config.DOMAIN_LOCK, null);
+ } else {
+ contactJid = Jid.fromString(jid.getText().toString());
+ }
+ } catch (final InvalidJidException e) {
+ jid.setError(context.getString(lock ? R.string.invalid_username : R.string.invalid_jid));
+ return;
+ }
+
+ if(listener != null) {
+ try {
+ if(listener.onEnterJidDialogPositive(accountJid, contactJid)) {
+ dialog.dismiss();
+ }
+ } catch(JidError error) {
+ jid.setError(error.toString());
+ }
+ }
+ }
+ };
+ }
+
+ public void setOnEnterJidDialogPositiveListener(OnEnterJidDialogPositiveListener listener) {
+ this.listener = listener;
+ }
+
+ public void show() {
+ this.dialog.show();
+ this.dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(this.dialogOnClick);
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/ExportLogsPreference.java b/src/main/java/eu/siacs/conversations/ui/ExportLogsPreference.java
new file mode 100644
index 00000000..bedb4172
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/ExportLogsPreference.java
@@ -0,0 +1,36 @@
+package eu.siacs.conversations.ui;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.preference.Preference;
+import android.util.AttributeSet;
+
+import eu.siacs.conversations.services.ExportLogsService;
+
+public class ExportLogsPreference extends Preference {
+
+ public ExportLogsPreference(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public ExportLogsPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public ExportLogsPreference(Context context) {
+ super(context);
+ }
+
+ protected void onClick() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
+ && getContext().checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
+ return;
+ }
+ final Intent startIntent = new Intent(getContext(), ExportLogsService.class);
+ getContext().startService(startIntent);
+ super.onClick();
+ }
+} \ No newline at end of file
diff --git a/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java
new file mode 100644
index 00000000..c83a0275
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java
@@ -0,0 +1,390 @@
+package eu.siacs.conversations.ui;
+
+import android.app.ActionBar;
+import android.app.AlertDialog;
+import android.content.ActivityNotFoundException;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.os.Bundle;
+import android.security.KeyChain;
+import android.security.KeyChainAliasCallback;
+import android.util.Pair;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ListView;
+import android.widget.Toast;
+
+import org.openintents.openpgp.util.OpenPgpApi;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
+import eu.siacs.conversations.ui.adapter.AccountAdapter;
+import eu.siacs.conversations.xmpp.jid.InvalidJidException;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+public class ManageAccountActivity extends XmppActivity implements OnAccountUpdate, KeyChainAliasCallback, XmppConnectionService.OnAccountCreated {
+
+ private final String STATE_SELECTED_ACCOUNT = "selected_account";
+
+ protected Account selectedAccount = null;
+ protected Jid selectedAccountJid = null;
+
+ protected final List<Account> accountList = new ArrayList<>();
+ protected ListView accountListView;
+ protected AccountAdapter mAccountAdapter;
+ protected AtomicBoolean mInvokedAddAccount = new AtomicBoolean(false);
+
+ protected Pair<Integer, Intent> mPostponedActivityResult = null;
+
+ @Override
+ public void onAccountUpdate() {
+ refreshUi();
+ }
+
+ @Override
+ protected void refreshUiReal() {
+ synchronized (this.accountList) {
+ accountList.clear();
+ accountList.addAll(xmppConnectionService.getAccounts());
+ }
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null) {
+ actionBar.setHomeButtonEnabled(this.accountList.size() > 0);
+ actionBar.setDisplayHomeAsUpEnabled(this.accountList.size() > 0);
+ }
+ invalidateOptionsMenu();
+ mAccountAdapter.notifyDataSetChanged();
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.manage_accounts);
+
+ if (savedInstanceState != null) {
+ String jid = savedInstanceState.getString(STATE_SELECTED_ACCOUNT);
+ if (jid != null) {
+ try {
+ this.selectedAccountJid = Jid.fromString(jid);
+ } catch (InvalidJidException e) {
+ this.selectedAccountJid = null;
+ }
+ }
+ }
+
+ accountListView = (ListView) findViewById(R.id.account_list);
+ this.mAccountAdapter = new AccountAdapter(this, accountList);
+ accountListView.setAdapter(this.mAccountAdapter);
+ accountListView.setOnItemClickListener(new OnItemClickListener() {
+
+ @Override
+ public void onItemClick(AdapterView<?> arg0, View view,
+ int position, long arg3) {
+ switchToAccount(accountList.get(position));
+ }
+ });
+ registerForContextMenu(accountListView);
+ }
+
+ @Override
+ public void onSaveInstanceState(final Bundle savedInstanceState) {
+ if (selectedAccount != null) {
+ savedInstanceState.putString(STATE_SELECTED_ACCOUNT, selectedAccount.getJid().toBareJid().toString());
+ }
+ super.onSaveInstanceState(savedInstanceState);
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ ManageAccountActivity.this.getMenuInflater().inflate(
+ R.menu.manageaccounts_context, menu);
+ AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo;
+ this.selectedAccount = accountList.get(acmi.position);
+ if (this.selectedAccount.isOptionSet(Account.OPTION_DISABLED)) {
+ menu.findItem(R.id.mgmt_account_disable).setVisible(false);
+ menu.findItem(R.id.mgmt_account_announce_pgp).setVisible(false);
+ menu.findItem(R.id.mgmt_account_publish_avatar).setVisible(false);
+ } else {
+ menu.findItem(R.id.mgmt_account_enable).setVisible(false);
+ menu.findItem(R.id.mgmt_account_announce_pgp).setVisible(Config.supportOpenPgp());
+ }
+ menu.setHeaderTitle(this.selectedAccount.getJid().toBareJid().toString());
+ }
+
+ @Override
+ void onBackendConnected() {
+ if (selectedAccountJid != null) {
+ this.selectedAccount = xmppConnectionService.findAccountByJid(selectedAccountJid);
+ }
+ refreshUiReal();
+ if (this.mPostponedActivityResult != null) {
+ this.onActivityResult(mPostponedActivityResult.first, RESULT_OK, mPostponedActivityResult.second);
+ }
+ if (Config.X509_VERIFICATION && this.accountList.size() == 0) {
+ if (mInvokedAddAccount.compareAndSet(false, true)) {
+ addAccountFromKey();
+ }
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.manageaccounts, menu);
+ MenuItem enableAll = menu.findItem(R.id.action_enable_all);
+ MenuItem addAccount = menu.findItem(R.id.action_add_account);
+ MenuItem addAccountWithCertificate = menu.findItem(R.id.action_add_account_with_cert);
+
+ if (Config.X509_VERIFICATION) {
+ addAccount.setVisible(false);
+ addAccountWithCertificate.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
+ } else {
+ addAccount.setVisible(!Config.LOCK_SETTINGS);
+ }
+ addAccountWithCertificate.setVisible(!Config.LOCK_SETTINGS);
+
+ if (!accountsLeftToEnable()) {
+ enableAll.setVisible(false);
+ }
+ MenuItem disableAll = menu.findItem(R.id.action_disable_all);
+ if (!accountsLeftToDisable()) {
+ disableAll.setVisible(false);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.mgmt_account_publish_avatar:
+ publishAvatar(selectedAccount);
+ return true;
+ case R.id.mgmt_account_disable:
+ disableAccount(selectedAccount);
+ return true;
+ case R.id.mgmt_account_enable:
+ enableAccount(selectedAccount);
+ return true;
+ case R.id.mgmt_account_delete:
+ deleteAccount(selectedAccount);
+ return true;
+ case R.id.mgmt_account_announce_pgp:
+ publishOpenPGPPublicKey(selectedAccount);
+ return true;
+ default:
+ return super.onContextItemSelected(item);
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.action_add_account:
+ startActivity(new Intent(getApplicationContext(),
+ EditAccountActivity.class));
+ break;
+ case R.id.action_disable_all:
+ disableAllAccounts();
+ break;
+ case R.id.action_enable_all:
+ enableAllAccounts();
+ break;
+ case R.id.action_add_account_with_cert:
+ addAccountFromKey();
+ break;
+ default:
+ break;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public boolean onNavigateUp() {
+ if (xmppConnectionService.getConversations().size() == 0) {
+ Intent contactsIntent = new Intent(this,
+ StartConversationActivity.class);
+ contactsIntent.setFlags(
+ // if activity exists in stack, pop the stack and go back to it
+ Intent.FLAG_ACTIVITY_CLEAR_TOP |
+ // otherwise, make a new task for it
+ Intent.FLAG_ACTIVITY_NEW_TASK |
+ // don't use the new activity animation; finish
+ // animation runs instead
+ Intent.FLAG_ACTIVITY_NO_ANIMATION);
+ startActivity(contactsIntent);
+ finish();
+ return true;
+ } else {
+ return super.onNavigateUp();
+ }
+ }
+
+ public void onClickTglAccountState(Account account, boolean enable) {
+ if (enable) {
+ enableAccount(account);
+ } else {
+ disableAccount(account);
+ }
+ }
+
+ private void addAccountFromKey() {
+ try {
+ KeyChain.choosePrivateKeyAlias(this, this, null, null, null, -1, null);
+ } catch (ActivityNotFoundException e) {
+ Toast.makeText(this, R.string.device_does_not_support_certificates, Toast.LENGTH_LONG).show();
+ }
+ }
+
+ private void publishAvatar(Account account) {
+ Intent intent = new Intent(getApplicationContext(),
+ PublishProfilePictureActivity.class);
+ intent.putExtra(EXTRA_ACCOUNT, account.getJid().toString());
+ startActivity(intent);
+ }
+
+ private void disableAllAccounts() {
+ List<Account> list = new ArrayList<>();
+ synchronized (this.accountList) {
+ for (Account account : this.accountList) {
+ if (!account.isOptionSet(Account.OPTION_DISABLED)) {
+ list.add(account);
+ }
+ }
+ }
+ for (Account account : list) {
+ disableAccount(account);
+ }
+ }
+
+ private boolean accountsLeftToDisable() {
+ synchronized (this.accountList) {
+ for (Account account : this.accountList) {
+ if (!account.isOptionSet(Account.OPTION_DISABLED)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
+ private boolean accountsLeftToEnable() {
+ synchronized (this.accountList) {
+ for (Account account : this.accountList) {
+ if (account.isOptionSet(Account.OPTION_DISABLED)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
+ private void enableAllAccounts() {
+ List<Account> list = new ArrayList<>();
+ synchronized (this.accountList) {
+ for (Account account : this.accountList) {
+ if (account.isOptionSet(Account.OPTION_DISABLED)) {
+ list.add(account);
+ }
+ }
+ }
+ for (Account account : list) {
+ enableAccount(account);
+ }
+ }
+
+ private void disableAccount(Account account) {
+ account.setOption(Account.OPTION_DISABLED, true);
+ xmppConnectionService.updateAccount(account);
+ }
+
+ private void enableAccount(Account account) {
+ account.setOption(Account.OPTION_DISABLED, false);
+ xmppConnectionService.updateAccount(account);
+ }
+
+ private void publishOpenPGPPublicKey(Account account) {
+ if (ManageAccountActivity.this.hasPgp()) {
+ choosePgpSignId(selectedAccount);
+ } else {
+ this.showInstallPgpDialog();
+ }
+ }
+
+ private void deleteAccount(final Account account) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(
+ ManageAccountActivity.this);
+ builder.setTitle(getString(R.string.mgmt_account_are_you_sure));
+ builder.setIconAttribute(android.R.attr.alertDialogIcon);
+ builder.setMessage(getString(R.string.mgmt_account_delete_confirm_text));
+ builder.setPositiveButton(getString(R.string.delete),
+ new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ xmppConnectionService.deleteAccount(account);
+ selectedAccount = null;
+ }
+ });
+ builder.setNegativeButton(getString(R.string.cancel), null);
+ builder.create().show();
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (resultCode == RESULT_OK) {
+ if (xmppConnectionServiceBound) {
+ if (requestCode == REQUEST_CHOOSE_PGP_ID) {
+ if (data.getExtras().containsKey(OpenPgpApi.EXTRA_SIGN_KEY_ID)) {
+ selectedAccount.setPgpSignId(data.getExtras().getLong(OpenPgpApi.EXTRA_SIGN_KEY_ID));
+ announcePgp(selectedAccount, null);
+ } else {
+ choosePgpSignId(selectedAccount);
+ }
+ } else if (requestCode == REQUEST_ANNOUNCE_PGP) {
+ announcePgp(selectedAccount, null);
+ }
+ this.mPostponedActivityResult = null;
+ } else {
+ this.mPostponedActivityResult = new Pair<>(requestCode, data);
+ }
+ }
+ }
+
+ @Override
+ public void alias(String alias) {
+ if (alias != null) {
+ xmppConnectionService.createAccountFromKey(alias, this);
+ }
+ }
+
+ @Override
+ public void onAccountCreated(Account account) {
+ switchToAccount(account, true);
+ }
+
+ @Override
+ public void informUser(final int r) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(ManageAccountActivity.this, r, Toast.LENGTH_LONG).show();
+ }
+ });
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java
new file mode 100644
index 00000000..1916947b
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java
@@ -0,0 +1,320 @@
+package eu.siacs.conversations.ui;
+
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnLongClickListener;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.soundcloud.android.crop.Crop;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+
+import de.thedevstack.conversationsplus.ConversationsPlusColors;
+import de.thedevstack.conversationsplus.utils.ImageUtil;
+import de.thedevstack.conversationsplus.utils.ui.TextViewUtil;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.services.AvatarService;
+import eu.siacs.conversations.utils.ExifHelper;
+import eu.siacs.conversations.utils.FileUtils;
+import eu.siacs.conversations.utils.PhoneHelper;
+import eu.siacs.conversations.xmpp.pep.Avatar;
+
+public class PublishProfilePictureActivity extends XmppActivity {
+
+ private static final int REQUEST_CHOOSE_FILE_AND_CROP = 0xac23;
+ private static final int REQUEST_CHOOSE_FILE = 0xac24;
+ private ImageView avatar;
+ private TextView accountTextView;
+ private TextView hintOrWarning;
+ private TextView secondaryHint;
+ private Button cancelButton;
+ private Button publishButton;
+ private Uri avatarUri;
+ private Uri defaultUri;
+ private Account account;
+ private boolean support = false;
+ private OnLongClickListener backToDefaultListener = new OnLongClickListener() {
+
+ @Override
+ public boolean onLongClick(View v) {
+ avatarUri = defaultUri;
+ loadImageIntoPreview(defaultUri);
+ return true;
+ }
+ };
+ private boolean mInitialAccountSetup;
+ private UiCallback<Avatar> avatarPublication = new UiCallback<Avatar>() {
+
+ @Override
+ public void success(Avatar object) {
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ if (mInitialAccountSetup) {
+ Intent intent = new Intent(getApplicationContext(),
+ StartConversationActivity.class);
+ intent.putExtra("init", true);
+ startActivity(intent);
+ }
+ Toast.makeText(PublishProfilePictureActivity.this,
+ R.string.avatar_has_been_published,
+ Toast.LENGTH_SHORT).show();
+ finish();
+ }
+ });
+ }
+
+ @Override
+ public void error(final int errorCode, Avatar object) {
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ hintOrWarning.setText(errorCode);
+ hintOrWarning.setTextColor(ConversationsPlusColors.warning());
+ TextViewUtil.enable(publishButton, ConversationsPlusColors.primaryText(), R.string.publish);
+ }
+ });
+
+ }
+
+ @Override
+ public void userInputRequried(PendingIntent pi, Avatar object) {
+ }
+ };
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_publish_profile_picture);
+ this.avatar = (ImageView) findViewById(R.id.account_image);
+ this.cancelButton = (Button) findViewById(R.id.cancel_button);
+ this.publishButton = (Button) findViewById(R.id.publish_button);
+ this.accountTextView = (TextView) findViewById(R.id.account);
+ this.hintOrWarning = (TextView) findViewById(R.id.hint_or_warning);
+ this.secondaryHint = (TextView) findViewById(R.id.secondary_hint);
+ this.publishButton.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (avatarUri != null) {
+ TextViewUtil.disable(publishButton, ConversationsPlusColors.secondaryText(), R.string.publishing);
+ AvatarService.getInstance().publishAvatar(account, avatarUri,
+ avatarPublication);
+ }
+ }
+ });
+ this.cancelButton.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (mInitialAccountSetup) {
+ Intent intent = new Intent(getApplicationContext(),
+ StartConversationActivity.class);
+ if (xmppConnectionService != null && xmppConnectionService.getAccounts().size() == 1) {
+ intent.putExtra("init", true);
+ }
+ startActivity(intent);
+ }
+ finish();
+ }
+ });
+ this.avatar.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (hasStoragePermission(REQUEST_CHOOSE_FILE)) {
+ chooseAvatar(false);
+ }
+
+ }
+ });
+ this.defaultUri = PhoneHelper.getSefliUri(getApplicationContext());
+ }
+
+ private void chooseAvatar(boolean crop) {
+ Intent attachFileIntent = new Intent();
+ attachFileIntent.setType("image/*");
+ attachFileIntent.setAction(Intent.ACTION_GET_CONTENT);
+ Intent chooser = Intent.createChooser(attachFileIntent, getString(R.string.attach_file));
+ startActivityForResult(chooser, crop ? REQUEST_CHOOSE_FILE_AND_CROP : REQUEST_CHOOSE_FILE);
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
+ if (grantResults.length > 0)
+ if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ if (requestCode == REQUEST_CHOOSE_FILE_AND_CROP) {
+ chooseAvatar(true);
+ } else if (requestCode == REQUEST_CHOOSE_FILE) {
+ chooseAvatar(false);
+ }
+ } else {
+ Toast.makeText(this, R.string.no_storage_permission, Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.publish_avatar, menu);
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ if (item.getItemId() == R.id.action_crop_image) {
+ if (hasStoragePermission(REQUEST_CHOOSE_FILE_AND_CROP)) {
+ chooseAvatar(true);
+ }
+ return true;
+ } else {
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, final Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (resultCode == RESULT_OK) {
+ switch (requestCode) {
+ case REQUEST_CHOOSE_FILE_AND_CROP:
+ Uri source = data.getData();
+ String original = FileUtils.getPath(source);
+ if (original != null) {
+ source = Uri.parse("file://"+original);
+ }
+ Uri destination = Uri.fromFile(new File(getCacheDir(), "croppedAvatar"));
+ final int size = getPixel(192);
+ Crop.of(source, destination).asSquare().withMaxSize(size, size).start(this);
+ break;
+ case REQUEST_CHOOSE_FILE:
+ this.avatarUri = data.getData();
+ if (xmppConnectionServiceBound) {
+ loadImageIntoPreview(this.avatarUri);
+ }
+ break;
+ case Crop.REQUEST_CROP:
+ this.avatarUri = Uri.fromFile(new File(getCacheDir(), "croppedAvatar"));
+ if (xmppConnectionServiceBound) {
+ loadImageIntoPreview(this.avatarUri);
+ }
+ break;
+ }
+ } else {
+ if (requestCode == Crop.REQUEST_CROP && data != null) {
+ Throwable throwable = Crop.getError(data);
+ if (throwable != null && throwable instanceof OutOfMemoryError) {
+ Toast.makeText(this,R.string.selection_too_large, Toast.LENGTH_SHORT).show();
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void onBackendConnected() {
+ this.account = extractAccount(getIntent());
+ if (this.account != null) {
+ if (this.account.getXmppConnection() != null) {
+ this.support = this.account.getXmppConnection().getFeatures().pep();
+ }
+ if (this.avatarUri == null) {
+ if (this.account.getAvatar() != null
+ || this.defaultUri == null) {
+ this.avatar.setImageBitmap(AvatarService.getInstance().get(account, getPixel(192)));
+ if (this.defaultUri != null) {
+ this.avatar
+ .setOnLongClickListener(this.backToDefaultListener);
+ } else {
+ this.secondaryHint.setVisibility(View.INVISIBLE);
+ }
+ if (!support) {
+ this.hintOrWarning
+ .setTextColor(ConversationsPlusColors.warning());
+ this.hintOrWarning
+ .setText(R.string.error_publish_avatar_no_server_support);
+ }
+ } else {
+ this.avatarUri = this.defaultUri;
+ loadImageIntoPreview(this.defaultUri);
+ this.secondaryHint.setVisibility(View.INVISIBLE);
+ }
+ } else {
+ loadImageIntoPreview(avatarUri);
+ }
+ String account;
+ if (Config.DOMAIN_LOCK != null) {
+ account = this.account.getJid().getLocalpart();
+ } else {
+ account = this.account.getJid().toBareJid().toString();
+ }
+ this.accountTextView.setText(account);
+ }
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ if (getIntent() != null) {
+ this.mInitialAccountSetup = getIntent().getBooleanExtra("setup", false);
+ }
+ if (this.mInitialAccountSetup) {
+ this.cancelButton.setText(R.string.skip);
+ }
+ }
+
+ protected void loadImageIntoPreview(Uri uri) {
+ Bitmap bm = null;
+ try {
+ bm = ImageUtil.cropCenterSquare(uri, getPixel(192));
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ if (bm == null) {
+ TextViewUtil.disable(this.publishButton, ConversationsPlusColors.secondaryText());
+ this.hintOrWarning.setTextColor(ConversationsPlusColors.warning());
+ this.hintOrWarning
+ .setText(R.string.error_publish_avatar_converting);
+ return;
+ }
+ this.avatar.setImageBitmap(bm);
+ if (support) {
+ TextViewUtil.enable(this.publishButton, ConversationsPlusColors.primaryText(), R.string.publish);
+ this.hintOrWarning.setText(R.string.publish_avatar_explanation);
+ this.hintOrWarning.setTextColor(ConversationsPlusColors.primaryText());
+ } else {
+ TextViewUtil.disable(this.publishButton, ConversationsPlusColors.secondaryText());
+ this.hintOrWarning.setTextColor(ConversationsPlusColors.warning());
+ this.hintOrWarning
+ .setText(R.string.error_publish_avatar_no_server_support);
+ }
+ if (this.defaultUri != null && uri.equals(this.defaultUri)) {
+ this.secondaryHint.setVisibility(View.INVISIBLE);
+ this.avatar.setOnLongClickListener(null);
+ } else if (this.defaultUri != null) {
+ this.secondaryHint.setVisibility(View.VISIBLE);
+ this.avatar.setOnLongClickListener(this.backToDefaultListener);
+ }
+ }
+
+ public void refreshUiReal() {
+ //nothing to do. This Activity doesn't implement any listeners
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java
new file mode 100644
index 00000000..6dd8a84f
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java
@@ -0,0 +1,229 @@
+package eu.siacs.conversations.ui;
+
+import android.app.AlertDialog;
+import android.app.FragmentManager;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.PreferenceCategory;
+import android.preference.PreferenceManager;
+import android.widget.Toast;
+
+import java.security.KeyStoreException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+
+import de.duenndns.ssl.MemorizingTrustManager;
+import de.thedevstack.conversationsplus.ConversationsPlusApplication;
+import de.tzur.conversations.Settings;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.persistance.FileBackend;
+import eu.siacs.conversations.services.ExportLogsService;
+import eu.siacs.conversations.xmpp.XmppConnection;
+
+import github.ankushsachdeva.emojicon.EmojiconHandler;
+
+public class SettingsActivity extends XmppActivity implements
+ OnSharedPreferenceChangeListener {
+
+ public static final int REQUEST_WRITE_LOGS = 0xbf8701;
+ private SettingsFragment mSettingsFragment;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ FragmentManager fm = getFragmentManager();
+ mSettingsFragment = (SettingsFragment) fm.findFragmentById(android.R.id.content);
+ if (mSettingsFragment == null || !mSettingsFragment.getClass().equals(SettingsFragment.class)) {
+ mSettingsFragment = new SettingsFragment();
+ fm.beginTransaction().replace(android.R.id.content, mSettingsFragment).commit();
+ }
+ }
+
+ @Override
+ void onBackendConnected() {
+
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ PreferenceManager.getDefaultSharedPreferences(this).registerOnSharedPreferenceChangeListener(this);
+ ListPreference resources = (ListPreference) mSettingsFragment.findPreference("resource");
+ if (resources != null) {
+ ArrayList<CharSequence> entries = new ArrayList<>(Arrays.asList(resources.getEntries()));
+ if (!entries.contains(Build.MODEL)) {
+ entries.add(0, Build.MODEL);
+ resources.setEntries(entries.toArray(new CharSequence[entries.size()]));
+ resources.setEntryValues(entries.toArray(new CharSequence[entries.size()]));
+ }
+ }
+
+ final Preference removeCertsPreference = mSettingsFragment.findPreference("remove_trusted_certificates");
+ removeCertsPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ final MemorizingTrustManager mtm = ConversationsPlusApplication.getMemorizingTrustManager();
+ final ArrayList<String> aliases = Collections.list(mtm.getCertificates());
+ if (aliases.size() == 0) {
+ displayToast(getString(R.string.toast_no_trusted_certs));
+ return true;
+ }
+ final ArrayList selectedItems = new ArrayList<Integer>();
+ final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(SettingsActivity.this);
+ dialogBuilder.setTitle(getResources().getString(R.string.dialog_manage_certs_title));
+ dialogBuilder.setMultiChoiceItems(aliases.toArray(new CharSequence[aliases.size()]), null,
+ new DialogInterface.OnMultiChoiceClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int indexSelected,
+ boolean isChecked) {
+ if (isChecked) {
+ selectedItems.add(indexSelected);
+ } else if (selectedItems.contains(indexSelected)) {
+ selectedItems.remove(Integer.valueOf(indexSelected));
+ }
+ if (selectedItems.size() > 0)
+ ((AlertDialog) dialog).getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(true);
+ else {
+ ((AlertDialog) dialog).getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(false);
+ }
+ }
+ });
+
+ dialogBuilder.setPositiveButton(
+ getResources().getString(R.string.dialog_manage_certs_positivebutton), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ int count = selectedItems.size();
+ if (count > 0) {
+ for (int i = 0; i < count; i++) {
+ try {
+ Integer item = Integer.valueOf(selectedItems.get(i).toString());
+ String alias = aliases.get(item);
+ mtm.deleteCertificate(alias);
+ } catch (KeyStoreException e) {
+ e.printStackTrace();
+ displayToast("Error: " + e.getLocalizedMessage());
+ }
+ }
+ if (xmppConnectionServiceBound) {
+ reconnectAccounts();
+ }
+ displayToast(getResources().getQuantityString(R.plurals.toast_delete_certificates, count, count));
+ }
+ }
+ });
+ dialogBuilder.setNegativeButton(getResources().getString(R.string.dialog_manage_certs_negativebutton), null);
+ AlertDialog removeCertsDialog = dialogBuilder.create();
+ removeCertsDialog.show();
+ removeCertsDialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
+ return true;
+ }
+ });
+ // Avoid appearence of setting to enable or disable omemo in screen
+ Preference omemoEnabledPreference = this.mSettingsFragment.findPreference("omemo_enabled");
+ PreferenceCategory otherExpertSettingsGroup = (PreferenceCategory) this.mSettingsFragment.findPreference("other_expert_settings");
+ if (null != omemoEnabledPreference && null != otherExpertSettingsGroup) {
+ otherExpertSettingsGroup.removePreference(omemoEnabledPreference);
+ }
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ PreferenceManager.getDefaultSharedPreferences(this)
+ .unregisterOnSharedPreferenceChangeListener(this);
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences preferences, String name) {
+ final List<String> resendPresence = Arrays.asList(
+ "confirm_messages_list",
+ "xa_on_silent_mode",
+ "away_when_screen_off",
+ "treat_vibrate_as_silent");
+ // need to synchronize the settings class first
+ Settings.synchronizeSettingsClassWithPreferences(preferences, name);
+ if (name.equals("resource")) {
+ String resource = preferences.getString("resource", "mobile")
+ .toLowerCase(Locale.US);
+ if (xmppConnectionServiceBound) {
+ for (Account account : xmppConnectionService.getAccounts()) {
+ if (account.setResource(resource)) {
+ if (!account.isOptionSet(Account.OPTION_DISABLED)) {
+ XmppConnection connection = account.getXmppConnection();
+ if (connection != null) {
+ connection.resetStreamId();
+ }
+ xmppConnectionService.reconnectAccountInBackground(account);
+ }
+ }
+ }
+ }
+ } else if (name.equals("keep_foreground_service")) {
+ xmppConnectionService.toggleForegroundService();
+ } else if (resendPresence.contains(name)) {
+ if (xmppConnectionServiceBound) {
+ if (name.equals("away_when_screen_off")) {
+ xmppConnectionService.toggleScreenEventReceiver();
+ }
+ xmppConnectionService.refreshAllPresences();
+ }
+ } else if (name.equals("dont_trust_system_cas")) {
+ ConversationsPlusApplication.updateMemorizingTrustmanager();
+ reconnectAccounts();
+ } else if ("parse_emoticons".equals(name)) {
+ EmojiconHandler.setParseEmoticons(Settings.PARSE_EMOTICONS);
+ } else if ("file_transfer_folder".equals(name)) {
+ FileBackend.onFileTransferFolderChanged();
+ } else if ("img_transfer_folder".equals(name)) {
+ FileBackend.onImageTransferFolderChanged();
+ }
+
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
+ if (grantResults.length > 0)
+ if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ if (requestCode == REQUEST_WRITE_LOGS) {
+ getApplicationContext().startService(new Intent(getApplicationContext(), ExportLogsService.class));
+ }
+ } else {
+ Toast.makeText(this, R.string.no_storage_permission, Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ private void displayToast(final String msg) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(SettingsActivity.this, msg, Toast.LENGTH_LONG).show();
+ }
+ });
+ }
+
+ private void reconnectAccounts() {
+ for (Account account : xmppConnectionService.getAccounts()) {
+ if (!account.isOptionSet(Account.OPTION_DISABLED)) {
+ xmppConnectionService.reconnectAccountInBackground(account);
+ }
+ }
+ }
+
+ public void refreshUiReal() {
+ //nothing to do. This Activity doesn't implement any listeners
+ }
+
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/SettingsFragment.java b/src/main/java/eu/siacs/conversations/ui/SettingsFragment.java
new file mode 100644
index 00000000..e4185abc
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/SettingsFragment.java
@@ -0,0 +1,65 @@
+package eu.siacs.conversations.ui;
+
+import android.app.Dialog;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceScreen;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+
+import eu.siacs.conversations.R;
+
+public class SettingsFragment extends PreferenceFragment {
+
+ //http://stackoverflow.com/questions/16374820/action-bar-home-button-not-functional-with-nested-preferencescreen/16800527#16800527
+ private void initializeActionBar(PreferenceScreen preferenceScreen) {
+ final Dialog dialog = preferenceScreen.getDialog();
+
+ if (dialog != null) {
+ View homeBtn = dialog.findViewById(android.R.id.home);
+
+ if (homeBtn != null) {
+ View.OnClickListener dismissDialogClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ dialog.dismiss();
+ }
+ };
+
+ ViewParent homeBtnContainer = homeBtn.getParent();
+
+ if (homeBtnContainer instanceof FrameLayout) {
+ ViewGroup containerParent = (ViewGroup) homeBtnContainer.getParent();
+ if (containerParent instanceof LinearLayout) {
+ ((LinearLayout) containerParent).setOnClickListener(dismissDialogClickListener);
+ } else {
+ ((FrameLayout) homeBtnContainer).setOnClickListener(dismissDialogClickListener);
+ }
+ } else {
+ homeBtn.setOnClickListener(dismissDialogClickListener);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Load the preferences from an XML resource
+ addPreferencesFromResource(R.xml.preferences);
+ }
+
+ @Override
+ public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) {
+ super.onPreferenceTreeClick(preferenceScreen, preference);
+ if (preference instanceof PreferenceScreen) {
+ initializeActionBar((PreferenceScreen) preference);
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java b/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java
new file mode 100644
index 00000000..7fcd6b33
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java
@@ -0,0 +1,359 @@
+package eu.siacs.conversations.ui;
+
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ListView;
+import android.widget.Toast;
+
+import java.net.URLConnection;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import de.thedevstack.conversationsplus.ConversationsPlusPreferences;
+import de.thedevstack.conversationsplus.ui.dialogs.UserDecisionDialog;
+import de.thedevstack.conversationsplus.ui.listeners.ResizePictureUserDecisionListener;
+import de.thedevstack.conversationsplus.ui.listeners.ShareWithResizePictureUserDecisionListener;
+import de.thedevstack.conversationsplus.utils.ConversationUtil;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.persistance.FileBackend;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.ui.adapter.ConversationAdapter;
+import eu.siacs.conversations.utils.FileUtils;
+import eu.siacs.conversations.xmpp.jid.InvalidJidException;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+public class ShareWithActivity extends XmppActivity implements XmppConnectionService.OnConversationUpdate {
+
+ private boolean mReturnToPrevious = false;
+
+ @Override
+ public void onConversationUpdate() {
+ refreshUi();
+ }
+
+ private class Share {
+ public List<Uri> uris = new ArrayList<>();
+ public boolean image;
+ public String account;
+ public String contact;
+ public String text;
+ public String uuid;
+ public boolean multiple = false;
+ }
+
+ private Share share;
+
+ private static final int REQUEST_START_NEW_CONVERSATION = 0x0501;
+ private ListView mListView;
+ private ConversationAdapter mAdapter;
+ private List<Conversation> mConversations = new ArrayList<>();
+ private Toast mToast;
+ private AtomicInteger attachmentCounter = new AtomicInteger(0);
+
+ private UiCallback<Message> attachFileCallback = new UiCallback<Message>() {
+
+ @Override
+ public void userInputRequried(PendingIntent pi, Message object) {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void success(final Message message) {
+ xmppConnectionService.sendMessage(message);
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (attachmentCounter.decrementAndGet() <=0 ) {
+ int resId;
+ if (share.image && share.multiple) {
+ resId = R.string.shared_images_with_x;
+ } else if (share.image) {
+ resId = R.string.shared_image_with_x;
+ } else {
+ resId = R.string.shared_file_with_x;
+ }
+ replaceToast(getString(resId, message.getConversation().getName()));
+ if (mReturnToPrevious) {
+ finish();
+ } else {
+ switchToConversation(message.getConversation());
+ }
+ }
+ }
+ });
+ }
+
+ @Override
+ public void error(final int errorCode, Message object) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ replaceToast(getString(errorCode));
+ if (attachmentCounter.decrementAndGet() <=0 ) {
+ finish();
+ }
+ }
+ });
+ }
+ };
+
+ protected void hideToast() {
+ if (mToast != null) {
+ mToast.cancel();
+ }
+ }
+
+ protected void replaceToast(String msg) {
+ hideToast();
+ mToast = Toast.makeText(this, msg ,Toast.LENGTH_LONG);
+ mToast.show();
+ }
+
+ protected void onActivityResult(int requestCode, int resultCode, final Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == REQUEST_START_NEW_CONVERSATION
+ && resultCode == RESULT_OK) {
+ share.contact = data.getStringExtra("contact");
+ share.account = data.getStringExtra(EXTRA_ACCOUNT);
+ }
+ if (xmppConnectionServiceBound
+ && share != null
+ && share.contact != null
+ && share.account != null) {
+ share();
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ if (getActionBar() != null) {
+ getActionBar().setDisplayHomeAsUpEnabled(false);
+ getActionBar().setHomeButtonEnabled(false);
+ }
+
+ setContentView(R.layout.share_with);
+ setTitle(getString(R.string.title_activity_sharewith));
+
+ mListView = (ListView) findViewById(R.id.choose_conversation_list);
+ mAdapter = new ConversationAdapter(this, this.mConversations);
+ mListView.setAdapter(mAdapter);
+ mListView.setOnItemClickListener(new OnItemClickListener() {
+
+ @Override
+ public void onItemClick(AdapterView<?> arg0, View arg1, int position, long arg3) {
+ share(mConversations.get(position));
+ }
+ });
+
+ this.share = new Share();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.share_with, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.action_add:
+ final Intent intent = new Intent(getApplicationContext(), ChooseContactActivity.class);
+ startActivityForResult(intent, REQUEST_START_NEW_CONVERSATION);
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ Intent intent = getIntent();
+ if (intent == null) {
+ return;
+ }
+ this.mReturnToPrevious = ConversationsPlusPreferences.returnToPrevious();
+ final String type = intent.getType();
+ final String action = intent.getAction();
+ Log.d(Config.LOGTAG, "action: "+action+ ", type:"+type);
+ share.uuid = intent.getStringExtra("uuid");
+ if (Intent.ACTION_SEND.equals(action)) {
+ final String text = intent.getStringExtra(Intent.EXTRA_TEXT);
+ final Uri uri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
+ if (type != null && uri != null && (text == null || !type.equals("text/plain"))) {
+ this.share.uris.clear();
+ this.share.uris.add(uri);
+ this.share.image = type.startsWith("image/") || isImage(uri);
+ } else {
+ this.share.text = text;
+ }
+ } else if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
+ this.share.image = type != null && type.startsWith("image/");
+ if (!this.share.image) {
+ return;
+ }
+ this.share.uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
+ }
+ if (xmppConnectionServiceBound) {
+ if (share.uuid != null) {
+ share();
+ } else {
+ xmppConnectionService.populateWithOrderedConversations(mConversations, this.share.uris.size() == 0);
+ }
+ }
+
+ }
+
+ protected boolean isImage(Uri uri) {
+ try {
+ String guess = URLConnection.guessContentTypeFromName(uri.toString());
+ return (guess != null && guess.startsWith("image/"));
+ } catch (final StringIndexOutOfBoundsException ignored) {
+ return false;
+ }
+ }
+
+ @Override
+ void onBackendConnected() {
+ if (xmppConnectionServiceBound && share != null
+ && ((share.contact != null && share.account != null) || share.uuid != null)) {
+ share();
+ return;
+ }
+ refreshUiReal();
+ }
+
+ private void share() {
+ final Conversation conversation;
+ if (share.uuid != null) {
+ conversation = xmppConnectionService.findConversationByUuid(share.uuid);
+ if (conversation == null) {
+ return;
+ }
+ }else{
+ Account account;
+ try {
+ account = xmppConnectionService.findAccountByJid(Jid.fromString(share.account));
+ } catch (final InvalidJidException e) {
+ account = null;
+ }
+ if (account == null) {
+ return;
+ }
+
+ try {
+ conversation = xmppConnectionService
+ .findOrCreateConversation(account, Jid.fromString(share.contact), false);
+ } catch (final InvalidJidException e) {
+ return;
+ }
+ }
+ share(conversation);
+ }
+
+ private void share(final Conversation conversation) {
+ final Account account = conversation.getAccount();
+ mListView.setEnabled(false);
+ if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP && !hasPgp()) {
+ if (share.uuid == null) {
+ showInstallPgpDialog();
+ } else {
+ Toast.makeText(this,R.string.openkeychain_not_installed,Toast.LENGTH_SHORT).show();
+ finish();
+ }
+ return;
+ }
+ if (share.uris.size() != 0) {
+ final long max = account.getXmppConnection()
+ .getFeatures()
+ .getMaxHttpUploadSize();
+ OnPresenceSelected callback;
+ if (this.share.image) {
+ // TODO: attachementCounter should be set and decremented correctly
+ callback = new OnPresenceSelected() {
+ @Override
+ public void onPresenceSelected() {
+ ResizePictureUserDecisionListener userDecisionListener = new ShareWithResizePictureUserDecisionListener(ShareWithActivity.this, conversation, xmppConnectionService, share.uris);
+ UserDecisionDialog userDecisionDialog = new UserDecisionDialog(ShareWithActivity.this, R.string.userdecision_question_resize_picture, userDecisionListener);
+ userDecisionDialog.decide(ConversationsPlusPreferences.resizePicture());
+ }
+ };
+ } else {
+ attachmentCounter.set(share.uris.size());
+ callback = new OnPresenceSelected() {
+ @Override
+ public void onPresenceSelected() {
+ replaceToast(getString(R.string.preparing_file));
+ ConversationUtil.attachFileToConversation(conversation, share.uris.get(0), attachFileCallback);
+ switchToConversation(conversation, null, true);
+ finish();
+ }
+ };
+ }
+ if (account.httpUploadAvailable()
+ && (conversation.getMode() == Conversation.MODE_MULTI
+ || FileUtils.allFilesUnderSize(this, share.uris, max))
+ && conversation.getNextEncryption() != Message.ENCRYPTION_OTR) {
+ callback.onPresenceSelected();
+ } else {
+ selectPresence(conversation, callback);
+ }
+ } else {
+ if (mReturnToPrevious && this.share.text != null && !this.share.text.isEmpty() ) {
+ final OnPresenceSelected callback = new OnPresenceSelected() {
+ @Override
+ 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);
+ replaceToast(getString(R.string.shared_text_with_x, conversation.getName()));
+ finish();
+ }
+ };
+ if (conversation.getNextEncryption() == Message.ENCRYPTION_OTR) {
+ selectPresence(conversation, callback);
+ } else {
+ callback.onPresenceSelected();
+ }
+ } else {
+ switchToConversation(conversation, this.share.text, true);
+ finish();
+ }
+ }
+
+ }
+
+ public void refreshUiReal() {
+ xmppConnectionService.populateWithOrderedConversations(mConversations, this.share != null && this.share.uris.size() == 0);
+ mAdapter.notifyDataSetChanged();
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (attachmentCounter.get() >= 1) {
+ replaceToast(getString(R.string.sharing_files_please_wait));
+ } else {
+ super.onBackPressed();
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java
new file mode 100644
index 00000000..188cd3fe
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java
@@ -0,0 +1,886 @@
+package eu.siacs.conversations.ui;
+
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.app.ActionBar;
+import android.app.ActionBar.Tab;
+import android.app.ActionBar.TabListener;
+import android.app.AlertDialog;
+import android.app.Fragment;
+import android.app.FragmentTransaction;
+import android.app.ListFragment;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.nfc.NdefMessage;
+import android.nfc.NdefRecord;
+import android.nfc.NfcAdapter;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.support.v13.app.FragmentPagerAdapter;
+import android.support.v4.view.ViewPager;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.AutoCompleteTextView;
+import android.widget.CheckBox;
+import android.widget.Checkable;
+import android.widget.EditText;
+import android.widget.ListView;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.google.zxing.integration.android.IntentIntegrator;
+import com.google.zxing.integration.android.IntentResult;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import de.thedevstack.android.logcat.Logging;
+import de.thedevstack.conversationsplus.ConversationsPlusPreferences;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Blockable;
+import eu.siacs.conversations.entities.Bookmark;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.ListItem;
+import eu.siacs.conversations.entities.Presence;
+import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate;
+import eu.siacs.conversations.ui.adapter.KnownHostsAdapter;
+import eu.siacs.conversations.ui.adapter.ListItemAdapter;
+import eu.siacs.conversations.utils.XmppUri;
+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.Jid;
+
+public class StartConversationActivity extends XmppActivity implements OnRosterUpdate, OnUpdateBlocklist {
+
+ public int conference_context_id;
+ public int contact_context_id;
+ private Tab mContactsTab;
+ private Tab mConferencesTab;
+ private ViewPager mViewPager;
+ private MyListFragment mContactsListFragment = new MyListFragment();
+ private List<ListItem> contacts = new ArrayList<>();
+ private ArrayAdapter<ListItem> mContactsAdapter;
+ private MyListFragment mConferenceListFragment = new MyListFragment();
+ private List<ListItem> conferences = new ArrayList<ListItem>();
+ private ArrayAdapter<ListItem> mConferenceAdapter;
+ private List<String> mActivatedAccounts = new ArrayList<String>();
+ private List<String> mKnownHosts;
+ private List<String> mKnownConferenceHosts;
+ private Invite mPendingInvite = null;
+ private Menu mOptionsMenu;
+ private EditText mSearchEditText;
+ private AtomicBoolean mRequestedContactsPermission = new AtomicBoolean(false);
+ private final int REQUEST_SYNC_CONTACTS = 0x3b28cf;
+
+ private MenuItem.OnActionExpandListener mOnActionExpandListener = new MenuItem.OnActionExpandListener() {
+
+ @Override
+ public boolean onMenuItemActionExpand(MenuItem item) {
+ mSearchEditText.post(new Runnable() {
+
+ @Override
+ public void run() {
+ mSearchEditText.requestFocus();
+ InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.showSoftInput(mSearchEditText,
+ InputMethodManager.SHOW_IMPLICIT);
+ }
+ });
+
+ return true;
+ }
+
+ @Override
+ public boolean onMenuItemActionCollapse(MenuItem item) {
+ InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.hideSoftInputFromWindow(mSearchEditText.getWindowToken(),
+ InputMethodManager.HIDE_IMPLICIT_ONLY);
+ mSearchEditText.setText("");
+ filter(null);
+ return true;
+ }
+ };
+ private boolean mHideOfflineContacts = false;
+ private TabListener mTabListener = new TabListener() {
+
+ @Override
+ public void onTabUnselected(Tab tab, FragmentTransaction ft) {
+ return;
+ }
+
+ @Override
+ public void onTabSelected(Tab tab, FragmentTransaction ft) {
+ mViewPager.setCurrentItem(tab.getPosition());
+ onTabChanged();
+ }
+
+ @Override
+ public void onTabReselected(Tab tab, FragmentTransaction ft) {
+ return;
+ }
+ };
+ private ViewPager.SimpleOnPageChangeListener mOnPageChangeListener = new ViewPager.SimpleOnPageChangeListener() {
+ @Override
+ public void onPageSelected(int position) {
+ if (getActionBar() != null) {
+ getActionBar().setSelectedNavigationItem(position);
+ }
+ onTabChanged();
+ }
+ };
+ private TextWatcher mSearchTextWatcher = new TextWatcher() {
+
+ @Override
+ public void afterTextChanged(Editable editable) {
+ filter(editable.toString());
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count,
+ int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before,
+ int count) {
+ }
+ };
+ private MenuItem mMenuSearchView;
+ private ListItemAdapter.OnTagClickedListener mOnTagClickedListener = new ListItemAdapter.OnTagClickedListener() {
+ @Override
+ public void onTagClicked(String tag) {
+ if (mMenuSearchView != null) {
+ mMenuSearchView.expandActionView();
+ mSearchEditText.setText("");
+ mSearchEditText.append(tag);
+ filter(tag);
+ }
+ }
+ };
+ private String mInitialJid;
+
+ @Override
+ public void onRosterUpdate() {
+ this.refreshUi();
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_start_conversation);
+ this.mHideOfflineContacts = ConversationsPlusPreferences.hideOffline();
+ mViewPager = (ViewPager) findViewById(R.id.start_conversation_view_pager);
+ ActionBar actionBar = getActionBar();
+ actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
+
+ mContactsTab = actionBar.newTab().setText(R.string.contacts)
+ .setTabListener(mTabListener);
+ mConferencesTab = actionBar.newTab().setText(R.string.conferences)
+ .setTabListener(mTabListener);
+ actionBar.addTab(mContactsTab);
+ actionBar.addTab(mConferencesTab);
+
+ mViewPager.setOnPageChangeListener(mOnPageChangeListener);
+ mViewPager.setAdapter(new FragmentPagerAdapter(getFragmentManager()) {
+
+ @Override
+ public int getCount() {
+ return 2;
+ }
+
+ @Override
+ public Fragment getItem(int position) {
+ if (position == 0) {
+ return mContactsListFragment;
+ } else {
+ return mConferenceListFragment;
+ }
+ }
+ });
+
+ mConferenceAdapter = new ListItemAdapter(this, conferences);
+ mConferenceListFragment.setListAdapter(mConferenceAdapter);
+ mConferenceListFragment.setContextMenu(R.menu.conference_context);
+ mConferenceListFragment
+ .setOnListItemClickListener(new OnItemClickListener() {
+
+ @Override
+ public void onItemClick(AdapterView<?> arg0, View arg1,
+ int position, long arg3) {
+ openConversationForBookmark(position);
+ }
+ });
+
+ mContactsAdapter = new ListItemAdapter(this, contacts);
+ ((ListItemAdapter) mContactsAdapter).setOnTagClickedListener(this.mOnTagClickedListener);
+ mContactsListFragment.setListAdapter(mContactsAdapter);
+ mContactsListFragment.setContextMenu(R.menu.contact_context);
+ mContactsListFragment
+ .setOnListItemClickListener(new OnItemClickListener() {
+
+ @Override
+ public void onItemClick(AdapterView<?> arg0, View arg1,
+ int position, long arg3) {
+ openConversationForContact(position);
+ }
+ });
+
+
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ askForContactsPermissions();
+ }
+
+ protected void openConversationForContact(int position) {
+ Contact contact = (Contact) contacts.get(position);
+ Conversation conversation = xmppConnectionService
+ .findOrCreateConversation(contact.getAccount(),
+ contact.getJid(), false);
+ switchToConversation(conversation);
+ }
+
+ protected void openConversationForContact() {
+ int position = contact_context_id;
+ openConversationForContact(position);
+ }
+
+ protected void openConversationForBookmark() {
+ openConversationForBookmark(conference_context_id);
+ }
+
+ protected void openConversationForBookmark(int position) {
+ Bookmark bookmark = (Bookmark) conferences.get(position);
+ Jid jid = bookmark.getJid();
+ if (jid == null) {
+ Toast.makeText(this,R.string.invalid_jid,Toast.LENGTH_SHORT).show();
+ return;
+ }
+ Conversation conversation = xmppConnectionService.findOrCreateConversation(bookmark.getAccount(),jid, true);
+ conversation.setBookmark(bookmark);
+ if (!conversation.getMucOptions().online()) {
+ xmppConnectionService.joinMuc(conversation);
+ }
+ if (!bookmark.autojoin() && ConversationsPlusPreferences.autojoin()) {
+ bookmark.setAutojoin(true);
+ xmppConnectionService.pushBookmarks(bookmark.getAccount());
+ }
+ switchToConversation(conversation);
+ }
+
+ protected void openDetailsForContact() {
+ int position = contact_context_id;
+ Contact contact = (Contact) contacts.get(position);
+ switchToContactDetails(contact);
+ }
+
+ protected void toggleContactBlock() {
+ final int position = contact_context_id;
+ BlockContactDialog.show(this, xmppConnectionService, (Contact) contacts.get(position));
+ }
+
+ protected void deleteContact() {
+ final int position = contact_context_id;
+ final Contact contact = (Contact) contacts.get(position);
+ final AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setNegativeButton(R.string.cancel, null);
+ builder.setTitle(R.string.action_delete_contact);
+ builder.setMessage(getString(R.string.remove_contact_text,
+ contact.getJid()));
+ builder.setPositiveButton(R.string.delete, new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ xmppConnectionService.deleteContactOnServer(contact);
+ filter(mSearchEditText.getText().toString());
+ }
+ });
+ builder.create().show();
+ }
+
+ protected void deleteConference() {
+ int position = conference_context_id;
+ final Bookmark bookmark = (Bookmark) conferences.get(position);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setNegativeButton(R.string.cancel, null);
+ builder.setTitle(R.string.delete_bookmark);
+ builder.setMessage(getString(R.string.remove_bookmark_text,
+ bookmark.getJid()));
+ builder.setPositiveButton(R.string.delete, new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ bookmark.unregisterConversation();
+ Account account = bookmark.getAccount();
+ account.getBookmarks().remove(bookmark);
+ xmppConnectionService.pushBookmarks(account);
+ filter(mSearchEditText.getText().toString());
+ }
+ });
+ builder.create().show();
+
+ }
+
+ @SuppressLint("InflateParams")
+ protected void showCreateContactDialog(final String prefilledJid, final String fingerprint) {
+ EnterJidDialog dialog = new EnterJidDialog(
+ this, mKnownHosts, mActivatedAccounts,
+ getString(R.string.create_contact), getString(R.string.create),
+ prefilledJid, null, fingerprint == null
+ );
+
+ dialog.setOnEnterJidDialogPositiveListener(new EnterJidDialog.OnEnterJidDialogPositiveListener() {
+ @Override
+ public boolean onEnterJidDialogPositive(Jid accountJid, Jid contactJid) throws EnterJidDialog.JidError {
+ if (!xmppConnectionServiceBound) {
+ return false;
+ }
+
+ final Account account = xmppConnectionService.findAccountByJid(accountJid);
+ if (account == null) {
+ return true;
+ }
+
+ final Contact contact = account.getRoster().getContact(contactJid);
+ if (contact.showInRoster()) {
+ throw new EnterJidDialog.JidError(getString(R.string.contact_already_exists));
+ } else {
+ contact.addOtrFingerprint(fingerprint);
+ xmppConnectionService.createContact(contact);
+ switchToConversation(contact);
+ return true;
+ }
+ }
+ });
+
+ dialog.show();
+ }
+
+ @SuppressLint("InflateParams")
+ protected void showJoinConferenceDialog(final String prefilledJid) {
+ final AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(R.string.join_conference);
+ final View dialogView = getLayoutInflater().inflate(R.layout.join_conference_dialog, null);
+ final Spinner spinner = (Spinner) dialogView.findViewById(R.id.account);
+ final AutoCompleteTextView jid = (AutoCompleteTextView) dialogView.findViewById(R.id.jid);
+ final boolean lock = Config.LOCK_DOMAINS_IN_CONVERSATIONS && Config.CONFERENCE_DOMAIN_LOCK != null;
+ final TextView jabberIdDesc = (TextView) dialogView.findViewById(R.id.jabber_id);
+ jabberIdDesc.setText(lock ? R.string.conference_name : R.string.conference_address);
+ jid.setHint(lock ? R.string.conference_name : R.string.conference_address_example);
+ if (!lock) {
+ jid.setAdapter(new KnownHostsAdapter(this, android.R.layout.simple_list_item_1, mKnownConferenceHosts));
+ }
+ if (prefilledJid != null) {
+ jid.append(prefilledJid);
+ }
+ populateAccountSpinner(this, mActivatedAccounts, spinner);
+ final Checkable bookmarkCheckBox = (CheckBox) dialogView
+ .findViewById(R.id.bookmark);
+ builder.setView(dialogView);
+ builder.setNegativeButton(R.string.cancel, null);
+ builder.setPositiveButton(R.string.join, null);
+ final AlertDialog dialog = builder.create();
+ dialog.show();
+ dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(
+ new View.OnClickListener() {
+
+ @Override
+ public void onClick(final View v) {
+ if (!xmppConnectionServiceBound) {
+ return;
+ }
+ final Account account = getSelectedAccount(spinner);
+ if (account == null) {
+ return;
+ }
+ final Jid conferenceJid;
+ try {
+ if (lock) {
+ conferenceJid = Jid.fromParts(jid.getText().toString(),Config.CONFERENCE_DOMAIN_LOCK, null);
+ } else {
+ conferenceJid = Jid.fromString(jid.getText().toString());
+ }
+ } catch (final InvalidJidException e) {
+ jid.setError(getString(lock ? R.string.invalid_conference_name : R.string.invalid_jid));
+ return;
+ }
+
+ if (bookmarkCheckBox.isChecked()) {
+ if (account.hasBookmarkFor(conferenceJid)) {
+ jid.setError(getString(R.string.bookmark_already_exists));
+ } else {
+ final Bookmark bookmark = new Bookmark(account, conferenceJid.toBareJid());
+ bookmark.setAutojoin(ConversationsPlusPreferences.autojoin());
+ String nick = conferenceJid.getResourcepart();
+ if (nick != null && !nick.isEmpty()) {
+ bookmark.setNick(nick);
+ }
+ account.getBookmarks().add(bookmark);
+ xmppConnectionService.pushBookmarks(account);
+ final Conversation conversation = xmppConnectionService
+ .findOrCreateConversation(account,
+ conferenceJid, true);
+ conversation.setBookmark(bookmark);
+ if (!conversation.getMucOptions().online()) {
+ xmppConnectionService.joinMuc(conversation);
+ }
+ dialog.dismiss();
+ switchToConversation(conversation);
+ }
+ } else {
+ final Conversation conversation = xmppConnectionService
+ .findOrCreateConversation(account,
+ conferenceJid, true);
+ if (!conversation.getMucOptions().online()) {
+ xmppConnectionService.joinMuc(conversation);
+ }
+ dialog.dismiss();
+ switchToConversation(conversation);
+ }
+ }
+ });
+ }
+
+ private Account getSelectedAccount(Spinner spinner) {
+ if (!spinner.isEnabled()) {
+ return null;
+ }
+ Jid jid;
+ try {
+ if (Config.DOMAIN_LOCK != null) {
+ jid = Jid.fromParts((String) spinner.getSelectedItem(), Config.DOMAIN_LOCK, null);
+ } else {
+ jid = Jid.fromString((String) spinner.getSelectedItem());
+ }
+ } catch (final InvalidJidException e) {
+ return null;
+ }
+ return xmppConnectionService.findAccountByJid(jid);
+ }
+
+ protected void switchToConversation(Contact contact) {
+ Conversation conversation = xmppConnectionService
+ .findOrCreateConversation(contact.getAccount(),
+ contact.getJid(), false);
+ switchToConversation(conversation);
+ }
+
+ public static void populateAccountSpinner(Context context, List<String> accounts, Spinner spinner) {
+ if (accounts.size() > 0) {
+ ArrayAdapter<String> adapter = new ArrayAdapter<>(context,
+ android.R.layout.simple_spinner_item, accounts);
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ spinner.setAdapter(adapter);
+ spinner.setEnabled(true);
+ } else {
+ ArrayAdapter<String> adapter = new ArrayAdapter<>(context,
+ android.R.layout.simple_spinner_item,
+ Arrays.asList(new String[]{context.getString(R.string.no_accounts)}));
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ spinner.setAdapter(adapter);
+ spinner.setEnabled(false);
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ this.mOptionsMenu = menu;
+ getMenuInflater().inflate(R.menu.start_conversation, menu);
+ MenuItem menuCreateContact = menu.findItem(R.id.action_create_contact);
+ MenuItem menuCreateConference = menu.findItem(R.id.action_join_conference);
+ MenuItem menuHideOffline = menu.findItem(R.id.action_hide_offline);
+ menuHideOffline.setChecked(this.mHideOfflineContacts);
+ mMenuSearchView = menu.findItem(R.id.action_search);
+ mMenuSearchView.setOnActionExpandListener(mOnActionExpandListener);
+ View mSearchView = mMenuSearchView.getActionView();
+ mSearchEditText = (EditText) mSearchView
+ .findViewById(R.id.search_field);
+ mSearchEditText.addTextChangedListener(mSearchTextWatcher);
+ if (getActionBar().getSelectedNavigationIndex() == 0) {
+ menuCreateConference.setVisible(false);
+ } else {
+ menuCreateContact.setVisible(false);
+ }
+ if (mInitialJid != null) {
+ mMenuSearchView.expandActionView();
+ mSearchEditText.append(mInitialJid);
+ filter(mInitialJid);
+ }
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.action_create_contact:
+ showCreateContactDialog(null,null);
+ return true;
+ case R.id.action_join_conference:
+ showJoinConferenceDialog(null);
+ return true;
+ case R.id.action_scan_qr_code:
+ new IntentIntegrator(this).initiateScan();
+ return true;
+ case R.id.action_hide_offline:
+ mHideOfflineContacts = !item.isChecked(); // the item is the menu item which is displayed, the inversion here calculates the new value
+ ConversationsPlusPreferences.commitHideOffline(mHideOfflineContacts);
+ if (mSearchEditText != null) {
+ filter(mSearchEditText.getText().toString());
+ }
+ invalidateOptionsMenu(); // Since the selection of this item changed the checked value, the options menu is now invalid
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_SEARCH && !event.isLongPress()) {
+ mOptionsMenu.findItem(R.id.action_search).expandActionView();
+ return true;
+ }
+ return super.onKeyUp(keyCode, event);
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent intent) {
+ if ((requestCode & 0xFFFF) == IntentIntegrator.REQUEST_CODE) {
+ IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent);
+ if (scanResult != null && scanResult.getFormatName() != null) {
+ String data = scanResult.getContents();
+ Invite invite = new Invite(data);
+ if (xmppConnectionServiceBound) {
+ invite.invite();
+ } else if (invite.getJid() != null) {
+ this.mPendingInvite = invite;
+ } else {
+ this.mPendingInvite = null;
+ }
+ }
+ }
+ super.onActivityResult(requestCode, requestCode, intent);
+ }
+
+ private void askForContactsPermissions() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ if (checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
+ if (mRequestedContactsPermission.compareAndSet(false, true)) {
+ if (shouldShowRequestPermissionRationale(Manifest.permission.READ_CONTACTS)) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(R.string.sync_with_contacts);
+ builder.setMessage(R.string.sync_with_contacts_long);
+ builder.setPositiveButton(R.string.next, new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS);
+ }
+ }
+ });
+ builder.create().show();
+ } else {
+ requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, 0);
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
+ if (grantResults.length > 0)
+ if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ if (requestCode == REQUEST_SYNC_CONTACTS && xmppConnectionServiceBound) {
+ xmppConnectionService.loadPhoneContacts();
+ }
+ }
+ }
+
+ @Override
+ protected void onBackendConnected() {
+ this.mActivatedAccounts.clear();
+ for (Account account : xmppConnectionService.getAccounts()) {
+ if (account.getStatus() != Account.State.DISABLED) {
+ if (Config.DOMAIN_LOCK != null) {
+ this.mActivatedAccounts.add(account.getJid().getLocalpart());
+ } else {
+ this.mActivatedAccounts.add(account.getJid().toBareJid().toString());
+ }
+ }
+ }
+ final Intent intent = getIntent();
+ final ActionBar ab = getActionBar();
+ if (intent != null && intent.getBooleanExtra("init",false) && ab != null) {
+ ab.setDisplayShowHomeEnabled(false);
+ ab.setDisplayHomeAsUpEnabled(false);
+ ab.setHomeButtonEnabled(false);
+ }
+ this.mKnownHosts = xmppConnectionService.getKnownHosts();
+ this.mKnownConferenceHosts = xmppConnectionService.getKnownConferenceHosts();
+ if (this.mPendingInvite != null) {
+ mPendingInvite.invite();
+ this.mPendingInvite = null;
+ } else if (!handleIntent(getIntent())) {
+ if (mSearchEditText != null) {
+ filter(mSearchEditText.getText().toString());
+ } else {
+ filter(null);
+ }
+ }
+ setIntent(null);
+ }
+
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ Invite getInviteJellyBean(NdefRecord record) {
+ return new Invite(record.toUri());
+ }
+
+ protected boolean handleIntent(Intent intent) {
+ if (intent == null || intent.getAction() == null) {
+ return false;
+ }
+ switch (intent.getAction()) {
+ case Intent.ACTION_SENDTO:
+ case Intent.ACTION_VIEW:
+ Logging.d(Config.LOGTAG, "received uri=" + intent.getData());
+ return new Invite(intent.getData()).invite();
+ case NfcAdapter.ACTION_NDEF_DISCOVERED:
+ for (Parcelable message : getIntent().getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)) {
+ if (message instanceof NdefMessage) {
+ Logging.d(Config.LOGTAG, "received message=" + message);
+ for (NdefRecord record : ((NdefMessage) message).getRecords()) {
+ switch (record.getTnf()) {
+ case NdefRecord.TNF_WELL_KNOWN:
+ if (Arrays.equals(record.getType(), NdefRecord.RTD_URI)) {
+ if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ return getInviteJellyBean(record).invite();
+ } else {
+ byte[] payload = record.getPayload();
+ if (payload[0] == 0) {
+ return new Invite(Uri.parse(new String(Arrays.copyOfRange(
+ payload, 1, payload.length)))).invite();
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ private boolean handleJid(Invite invite) {
+ List<Contact> contacts = xmppConnectionService.findContacts(invite.getJid());
+ if (contacts.size() == 0) {
+ showCreateContactDialog(invite.getJid().toString(),invite.getFingerprint());
+ return false;
+ } else if (contacts.size() == 1) {
+ Contact contact = contacts.get(0);
+ if (invite.getFingerprint() != null) {
+ if (contact.addOtrFingerprint(invite.getFingerprint())) {
+ Logging.d(Config.LOGTAG,"added new fingerprint");
+ xmppConnectionService.syncRosterToDisk(contact.getAccount());
+ }
+ }
+ switchToConversation(contact);
+ return true;
+ } else {
+ if (mMenuSearchView != null) {
+ mMenuSearchView.expandActionView();
+ mSearchEditText.setText("");
+ mSearchEditText.append(invite.getJid().toString());
+ filter(invite.getJid().toString());
+ } else {
+ mInitialJid = invite.getJid().toString();
+ }
+ return true;
+ }
+ }
+
+ protected void filter(String needle) {
+ if (xmppConnectionServiceBound) {
+ this.filterContacts(needle);
+ this.filterConferences(needle);
+ }
+ }
+
+ protected void filterContacts(String needle) {
+ this.contacts.clear();
+ for (Account account : xmppConnectionService.getAccounts()) {
+ if (account.getStatus() != Account.State.DISABLED) {
+ for (Contact contact : account.getRoster().getContacts()) {
+ Presence p = contact.getPresences().getMostAvailablePresence();
+ Presence.Status s = p == null ? Presence.Status.OFFLINE : p.getStatus();
+ if (contact.showInRoster() && contact.match(needle)
+ && (!this.mHideOfflineContacts
+ || (needle != null && !needle.isEmpty())
+ || s.compareTo(Presence.Status.OFFLINE) < 0)) {
+ this.contacts.add(contact);
+ }
+ }
+ }
+ }
+ Collections.sort(this.contacts);
+ mContactsAdapter.notifyDataSetChanged();
+ }
+
+ protected void filterConferences(String needle) {
+ this.conferences.clear();
+ for (Account account : xmppConnectionService.getAccounts()) {
+ if (account.getStatus() != Account.State.DISABLED) {
+ for (Bookmark bookmark : account.getBookmarks()) {
+ if (bookmark.match(needle)) {
+ this.conferences.add(bookmark);
+ }
+ }
+ }
+ }
+ Collections.sort(this.conferences);
+ mConferenceAdapter.notifyDataSetChanged();
+ }
+
+ private void onTabChanged() {
+ invalidateOptionsMenu();
+ }
+
+ @Override
+ public void OnUpdateBlocklist(final Status status) {
+ refreshUi();
+ }
+
+ @Override
+ protected void refreshUiReal() {
+ if (mSearchEditText != null) {
+ filter(mSearchEditText.getText().toString());
+ }
+ }
+
+ public static class MyListFragment extends ListFragment {
+ private AdapterView.OnItemClickListener mOnItemClickListener;
+ private int mResContextMenu;
+
+ public void setContextMenu(final int res) {
+ this.mResContextMenu = res;
+ }
+
+ @Override
+ public void onListItemClick(final ListView l, final View v, final int position, final long id) {
+ if (mOnItemClickListener != null) {
+ mOnItemClickListener.onItemClick(l, v, position, id);
+ }
+ }
+
+ public void setOnListItemClickListener(AdapterView.OnItemClickListener l) {
+ this.mOnItemClickListener = l;
+ }
+
+ @Override
+ public void onViewCreated(final View view, final Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ registerForContextMenu(getListView());
+ getListView().setFastScrollEnabled(true);
+ }
+
+ @Override
+ public void onCreateContextMenu(final ContextMenu menu, final View v,
+ final ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ final StartConversationActivity activity = (StartConversationActivity) getActivity();
+ activity.getMenuInflater().inflate(mResContextMenu, menu);
+ final AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo;
+ if (mResContextMenu == R.menu.conference_context) {
+ activity.conference_context_id = acmi.position;
+ } else if (mResContextMenu == R.menu.contact_context){
+ activity.contact_context_id = acmi.position;
+ final Blockable contact = (Contact) activity.contacts.get(acmi.position);
+ final MenuItem blockUnblockItem = menu.findItem(R.id.context_contact_block_unblock);
+ XmppConnection xmpp = contact.getAccount().getXmppConnection();
+ if (xmpp != null && xmpp.getFeatures().blocking()) {
+ if (contact.isBlocked()) {
+ blockUnblockItem.setTitle(R.string.unblock_contact);
+ } else {
+ blockUnblockItem.setTitle(R.string.block_contact);
+ }
+ } else {
+ blockUnblockItem.setVisible(false);
+ }
+ }
+ }
+
+ @Override
+ public boolean onContextItemSelected(final MenuItem item) {
+ StartConversationActivity activity = (StartConversationActivity) getActivity();
+ switch (item.getItemId()) {
+ case R.id.context_start_conversation:
+ activity.openConversationForContact();
+ break;
+ case R.id.context_contact_details:
+ activity.openDetailsForContact();
+ break;
+ case R.id.context_contact_block_unblock:
+ activity.toggleContactBlock();
+ break;
+ case R.id.context_delete_contact:
+ activity.deleteContact();
+ break;
+ case R.id.context_join_conference:
+ activity.openConversationForBookmark();
+ break;
+ case R.id.context_delete_conference:
+ activity.deleteConference();
+ }
+ return true;
+ }
+ }
+
+ private class Invite extends XmppUri {
+
+ public Invite(final Uri uri) {
+ super(uri);
+ }
+
+ public Invite(final String uri) {
+ super(uri);
+ }
+
+ boolean invite() {
+ if (jid != null) {
+ if (muc) {
+ showJoinConferenceDialog(jid);
+ } else {
+ return handleJid(this);
+ }
+ }
+ return false;
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/TimePreference.java b/src/main/java/eu/siacs/conversations/ui/TimePreference.java
new file mode 100644
index 00000000..e32b068c
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/TimePreference.java
@@ -0,0 +1,105 @@
+package eu.siacs.conversations.ui;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.preference.DialogPreference;
+import android.preference.Preference;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.TimePicker;
+
+import java.text.DateFormat;
+import java.util.Calendar;
+import java.util.Date;
+
+public class TimePreference extends DialogPreference implements Preference.OnPreferenceChangeListener {
+ private TimePicker picker = null;
+ public final static long DEFAULT_VALUE = 0;
+
+ public TimePreference(final Context context, final AttributeSet attrs) {
+ super(context, attrs, 0);
+ this.setOnPreferenceChangeListener(this);
+ }
+
+ protected void setTime(final long time) {
+ persistLong(time);
+ notifyDependencyChange(shouldDisableDependents());
+ notifyChanged();
+ }
+
+ protected void updateSummary(final long time) {
+ final DateFormat dateFormat = android.text.format.DateFormat.getTimeFormat(getContext());
+ final Date date = new Date(time);
+ setSummary(dateFormat.format(date.getTime()));
+ }
+
+ @Override
+ protected View onCreateDialogView() {
+ picker = new TimePicker(getContext());
+ picker.setIs24HourView(android.text.format.DateFormat.is24HourFormat(getContext()));
+ return picker;
+ }
+
+ protected Calendar getPersistedTime() {
+ final Calendar c = Calendar.getInstance();
+ c.setTimeInMillis(getPersistedLong(DEFAULT_VALUE));
+
+ return c;
+ }
+
+ @SuppressWarnings("NullableProblems")
+ @Override
+ protected void onBindDialogView(final View v) {
+ super.onBindDialogView(v);
+ final Calendar c = getPersistedTime();
+
+ picker.setCurrentHour(c.get(Calendar.HOUR_OF_DAY));
+ picker.setCurrentMinute(c.get(Calendar.MINUTE));
+ }
+
+ @Override
+ protected void onDialogClosed(final boolean positiveResult) {
+ super.onDialogClosed(positiveResult);
+
+ if (positiveResult) {
+ final Calendar c = Calendar.getInstance();
+ c.set(Calendar.MINUTE, picker.getCurrentMinute());
+ c.set(Calendar.HOUR_OF_DAY, picker.getCurrentHour());
+
+
+ if (!callChangeListener(c.getTimeInMillis())) {
+ return;
+ }
+
+ setTime(c.getTimeInMillis());
+ }
+ }
+
+ @Override
+ protected Object onGetDefaultValue(final TypedArray a, final int index) {
+ return a.getInteger(index, 0);
+ }
+
+ @Override
+ protected void onSetInitialValue(final boolean restorePersistedValue, final Object defaultValue) {
+ long time;
+ if (defaultValue == null) {
+ time = restorePersistedValue ? getPersistedLong(DEFAULT_VALUE) : DEFAULT_VALUE;
+ } else if (defaultValue instanceof Long) {
+ time = restorePersistedValue ? getPersistedLong((Long) defaultValue) : (Long) defaultValue;
+ } else if (defaultValue instanceof Calendar) {
+ time = restorePersistedValue ? getPersistedLong(((Calendar)defaultValue).getTimeInMillis()) : ((Calendar)defaultValue).getTimeInMillis();
+ } else {
+ time = restorePersistedValue ? getPersistedLong(DEFAULT_VALUE) : DEFAULT_VALUE;
+ }
+
+ setTime(time);
+ updateSummary(time);
+ }
+
+ @Override
+ public boolean onPreferenceChange(final Preference preference, final Object newValue) {
+ ((TimePreference) preference).updateSummary((Long)newValue);
+ return true;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java b/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java
new file mode 100644
index 00000000..02a9823d
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java
@@ -0,0 +1,320 @@
+package eu.siacs.conversations.ui;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.CompoundButton;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import org.whispersystems.libaxolotl.IdentityKey;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import de.thedevstack.conversationsplus.ConversationsPlusColors;
+import de.thedevstack.conversationsplus.utils.ui.TextViewUtil;
+
+import eu.siacs.conversations.R;
+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.Conversation;
+import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
+import eu.siacs.conversations.xmpp.jid.InvalidJidException;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdated {
+ private List<Jid> contactJids;
+
+ private Account mAccount;
+ private Conversation mConversation;
+ private TextView keyErrorMessage;
+ private LinearLayout keyErrorMessageCard;
+ private TextView ownKeysTitle;
+ private LinearLayout ownKeys;
+ private LinearLayout ownKeysCard;
+ private LinearLayout foreignKeys;
+ private Button mSaveButton;
+ private Button mCancelButton;
+
+ private AxolotlService.FetchStatus lastFetchReport = AxolotlService.FetchStatus.SUCCESS;
+
+ private final Map<String, Boolean> ownKeysToTrust = new HashMap<>();
+ private final Map<Jid,Map<String, Boolean>> foreignKeysToTrust = new HashMap<>();
+
+ private final OnClickListener mSaveButtonListener = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ commitTrusts();
+ finishOk();
+ }
+ };
+
+ private final OnClickListener mCancelButtonListener = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+ };
+
+ @Override
+ protected void refreshUiReal() {
+ invalidateOptionsMenu();
+ populateView();
+ }
+
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_trust_keys);
+ this.contactJids = new ArrayList<>();
+ for(String jid : getIntent().getStringArrayExtra("contacts")) {
+ try {
+ this.contactJids.add(Jid.fromString(jid));
+ } catch (InvalidJidException e) {
+ e.printStackTrace();
+ }
+ }
+
+ keyErrorMessageCard = (LinearLayout) findViewById(R.id.key_error_message_card);
+ keyErrorMessage = (TextView) findViewById(R.id.key_error_message);
+ ownKeysTitle = (TextView) findViewById(R.id.own_keys_title);
+ ownKeys = (LinearLayout) findViewById(R.id.own_keys_details);
+ ownKeysCard = (LinearLayout) findViewById(R.id.own_keys_card);
+ foreignKeys = (LinearLayout) findViewById(R.id.foreign_keys);
+ mCancelButton = (Button) findViewById(R.id.cancel_button);
+ mCancelButton.setOnClickListener(mCancelButtonListener);
+ mSaveButton = (Button) findViewById(R.id.save_button);
+ mSaveButton.setOnClickListener(mSaveButtonListener);
+
+
+ if (getActionBar() != null) {
+ getActionBar().setHomeButtonEnabled(true);
+ getActionBar().setDisplayHomeAsUpEnabled(true);
+ }
+ }
+
+ private void populateView() {
+ setTitle(getString(R.string.trust_omemo_fingerprints));
+ ownKeys.removeAllViews();
+ foreignKeys.removeAllViews();
+ boolean hasOwnKeys = false;
+ boolean hasForeignKeys = false;
+ for(final String fingerprint : ownKeysToTrust.keySet()) {
+ hasOwnKeys = true;
+ addFingerprintRowWithListeners(ownKeys, mAccount, fingerprint, false,
+ XmppAxolotlSession.Trust.fromBoolean(ownKeysToTrust.get(fingerprint)), false,
+ new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ ownKeysToTrust.put(fingerprint, isChecked);
+ // own fingerprints have no impact on locked status.
+ }
+ },
+ null,
+ null
+ );
+ }
+
+ synchronized (this.foreignKeysToTrust) {
+ for (Map.Entry<Jid, Map<String, Boolean>> entry : foreignKeysToTrust.entrySet()) {
+ hasForeignKeys = true;
+ final LinearLayout layout = (LinearLayout) getLayoutInflater().inflate(R.layout.keys_card, foreignKeys, false);
+ final Jid jid = entry.getKey();
+ final TextView header = (TextView) layout.findViewById(R.id.foreign_keys_title);
+ final LinearLayout keysContainer = (LinearLayout) layout.findViewById(R.id.foreign_keys_details);
+ final TextView informNoKeys = (TextView) layout.findViewById(R.id.no_keys_to_accept);
+ header.setText(jid.toString());
+ final Map<String, Boolean> fingerprints = entry.getValue();
+ for (final String fingerprint : fingerprints.keySet()) {
+ addFingerprintRowWithListeners(keysContainer, mAccount, fingerprint, false,
+ XmppAxolotlSession.Trust.fromBoolean(fingerprints.get(fingerprint)), false,
+ new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ fingerprints.put(fingerprint, isChecked);
+ lockOrUnlockAsNeeded();
+ }
+ },
+ null,
+ null
+ );
+ }
+ if (fingerprints.size() == 0) {
+ informNoKeys.setVisibility(View.VISIBLE);
+ informNoKeys.setText(getString(R.string.no_keys_just_confirm,mAccount.getRoster().getContact(jid).getDisplayName()));
+ } else {
+ informNoKeys.setVisibility(View.GONE);
+ }
+ foreignKeys.addView(layout);
+ }
+ }
+
+ ownKeysTitle.setText(mAccount.getJid().toBareJid().toString());
+ ownKeysCard.setVisibility(hasOwnKeys ? View.VISIBLE : View.GONE);
+ foreignKeys.setVisibility(hasForeignKeys ? View.VISIBLE : View.GONE);
+ if(hasPendingKeyFetches()) {
+ TextViewUtil.disable(this.mSaveButton, ConversationsPlusColors.secondaryText(), R.string.fetching_keys);
+ } else {
+ if (!hasForeignKeys && hasNoOtherTrustedKeys()) {
+ keyErrorMessageCard.setVisibility(View.VISIBLE);
+ if (lastFetchReport == AxolotlService.FetchStatus.ERROR
+ || mAccount.getAxolotlService().fetchMapHasErrors(contactJids)) {
+ keyErrorMessage.setText(R.string.error_no_keys_to_trust_server_error);
+ } else {
+ keyErrorMessage.setText(R.string.error_no_keys_to_trust);
+ }
+ ownKeys.removeAllViews();
+ ownKeysCard.setVisibility(View.GONE);
+ foreignKeys.removeAllViews();
+ foreignKeys.setVisibility(View.GONE);
+ }
+ lockOrUnlockAsNeeded();
+ mSaveButton.setText(R.string.done);
+ }
+ }
+
+ private boolean reloadFingerprints() {
+ List<Jid> acceptedTargets = mConversation == null ? new ArrayList<Jid>() : mConversation.getAcceptedCryptoTargets();
+ ownKeysToTrust.clear();
+ AxolotlService service = this.mAccount.getAxolotlService();
+ Set<IdentityKey> ownKeysSet = service.getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED);
+ for(final IdentityKey identityKey : ownKeysSet) {
+ if(!ownKeysToTrust.containsKey(identityKey)) {
+ ownKeysToTrust.put(identityKey.getFingerprint().replaceAll("\\s", ""), false);
+ }
+ }
+ synchronized (this.foreignKeysToTrust) {
+ foreignKeysToTrust.clear();
+ for (Jid jid : contactJids) {
+ Set<IdentityKey> foreignKeysSet = service.getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED, jid);
+ if (hasNoOtherTrustedKeys(jid) && ownKeysSet.size() == 0) {
+ foreignKeysSet.addAll(service.getKeysWithTrust(XmppAxolotlSession.Trust.UNTRUSTED, jid));
+ }
+ Map<String, Boolean> foreignFingerprints = new HashMap<>();
+ for (final IdentityKey identityKey : foreignKeysSet) {
+ if (!foreignFingerprints.containsKey(identityKey)) {
+ foreignFingerprints.put(identityKey.getFingerprint().replaceAll("\\s", ""), false);
+ }
+ }
+ if (foreignFingerprints.size() > 0 || !acceptedTargets.contains(jid)) {
+ foreignKeysToTrust.put(jid, foreignFingerprints);
+ }
+ }
+ }
+ return ownKeysSet.size() + foreignKeysToTrust.size() > 0;
+ }
+
+ @Override
+ public void onBackendConnected() {
+ Intent intent = getIntent();
+ this.mAccount = extractAccount(intent);
+ if (this.mAccount != null && intent != null) {
+ String uuid = intent.getStringExtra("conversation");
+ this.mConversation = xmppConnectionService.findConversationByUuid(uuid);
+ reloadFingerprints();
+ populateView();
+ }
+ }
+
+ private boolean hasNoOtherTrustedKeys() {
+ return mAccount == null || mAccount.getAxolotlService().anyTargetHasNoTrustedKeys(contactJids);
+ }
+
+ private boolean hasNoOtherTrustedKeys(Jid contact) {
+ return mAccount == null || mAccount.getAxolotlService().getNumTrustedKeys(contact) == 0;
+ }
+
+ private boolean hasPendingKeyFetches() {
+ return mAccount != null && mAccount.getAxolotlService().hasPendingKeyFetches(mAccount, contactJids);
+ }
+
+
+ @Override
+ public void onKeyStatusUpdated(final AxolotlService.FetchStatus report) {
+ if (report != null) {
+ lastFetchReport = report;
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ switch (report) {
+ case ERROR:
+ Toast.makeText(TrustKeysActivity.this,R.string.error_fetching_omemo_key,Toast.LENGTH_SHORT).show();
+ break;
+ case SUCCESS_VERIFIED:
+ Toast.makeText(TrustKeysActivity.this,R.string.verified_omemo_key_with_certificate,Toast.LENGTH_LONG).show();
+ break;
+ }
+ }
+ });
+
+ }
+ boolean keysToTrust = reloadFingerprints();
+ if (keysToTrust || hasPendingKeyFetches() || hasNoOtherTrustedKeys()) {
+ refreshUi();
+ } else {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ finishOk();
+ }
+ });
+
+ }
+ }
+
+ private void finishOk() {
+ Intent data = new Intent();
+ data.putExtra("choice", getIntent().getIntExtra("choice", ConversationActivity.ATTACHMENT_CHOICE_INVALID));
+ setResult(RESULT_OK, data);
+ finish();
+ }
+
+ private void commitTrusts() {
+ for(final String fingerprint :ownKeysToTrust.keySet()) {
+ mAccount.getAxolotlService().setFingerprintTrust(
+ fingerprint,
+ XmppAxolotlSession.Trust.fromBoolean(ownKeysToTrust.get(fingerprint)));
+ }
+ List<Jid> acceptedTargets = mConversation == null ? new ArrayList<Jid>() : mConversation.getAcceptedCryptoTargets();
+ synchronized (this.foreignKeysToTrust) {
+ for (Map.Entry<Jid, Map<String, Boolean>> entry : foreignKeysToTrust.entrySet()) {
+ Jid jid = entry.getKey();
+ Map<String, Boolean> value = entry.getValue();
+ if (!acceptedTargets.contains(jid)) {
+ acceptedTargets.add(jid);
+ }
+ for (final String fingerprint : value.keySet()) {
+ mAccount.getAxolotlService().setFingerprintTrust(
+ fingerprint,
+ XmppAxolotlSession.Trust.fromBoolean(value.get(fingerprint)));
+ }
+ }
+ }
+ if (mConversation != null && mConversation.getMode() == Conversation.MODE_MULTI) {
+ mConversation.setAcceptedCryptoTargets(acceptedTargets);
+ xmppConnectionService.updateConversation(mConversation);
+ }
+ }
+
+ private void lockOrUnlockAsNeeded() {
+ synchronized (this.foreignKeysToTrust) {
+ for (Jid jid : contactJids) {
+ Map<String, Boolean> fingerprints = foreignKeysToTrust.get(jid);
+ if (hasNoOtherTrustedKeys(jid) && (fingerprints == null || !fingerprints.values().contains(true))) {
+ TextViewUtil.disable(this.mSaveButton, ConversationsPlusColors.secondaryText());
+ return;
+ }
+ }
+ }
+ TextViewUtil.enable(this.mSaveButton, ConversationsPlusColors.primaryText());
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/UiCallback.java b/src/main/java/eu/siacs/conversations/ui/UiCallback.java
new file mode 100644
index 00000000..d056d628
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/UiCallback.java
@@ -0,0 +1,11 @@
+package eu.siacs.conversations.ui;
+
+import android.app.PendingIntent;
+
+public interface UiCallback<T> {
+ void success(T object);
+
+ void error(int errorCode, T object);
+
+ void userInputRequried(PendingIntent pi, T object);
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java b/src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java
new file mode 100644
index 00000000..a310b6ce
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java
@@ -0,0 +1,444 @@
+package eu.siacs.conversations.ui;
+
+import android.app.ActionBar;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
+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 net.java.otr4j.OtrException;
+import net.java.otr4j.session.Session;
+
+import de.thedevstack.conversationsplus.ConversationsPlusColors;
+import de.thedevstack.conversationsplus.utils.ui.TextViewUtil;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.utils.XmppUri;
+import eu.siacs.conversations.xmpp.jid.InvalidJidException;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+public class VerifyOTRActivity extends XmppActivity implements XmppConnectionService.OnConversationUpdate {
+
+ public static final String ACTION_VERIFY_CONTACT = "verify_contact";
+ public static final int MODE_SCAN_FINGERPRINT = - 0x0502;
+ public static final int MODE_ASK_QUESTION = 0x0503;
+ public static final int MODE_ANSWER_QUESTION = 0x0504;
+ public static final int MODE_MANUAL_VERIFICATION = 0x0505;
+
+ private LinearLayout mManualVerificationArea;
+ private LinearLayout mSmpVerificationArea;
+ private TextView mRemoteFingerprint;
+ private TextView mYourFingerprint;
+ private TextView mVerificationExplain;
+ private TextView mStatusMessage;
+ private TextView mSharedSecretHint;
+ private EditText mSharedSecretHintEditable;
+ private EditText mSharedSecretSecret;
+ private Button mLeftButton;
+ private Button mRightButton;
+ private Account mAccount;
+ private Conversation mConversation;
+ private int mode = MODE_MANUAL_VERIFICATION;
+ private XmppUri mPendingUri = null;
+
+ private DialogInterface.OnClickListener mVerifyFingerprintListener = new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialogInterface, int click) {
+ mConversation.verifyOtrFingerprint();
+ xmppConnectionService.syncRosterToDisk(mConversation.getAccount());
+ Toast.makeText(VerifyOTRActivity.this,R.string.verified,Toast.LENGTH_SHORT).show();
+ finish();
+ }
+ };
+
+ private View.OnClickListener mCreateSharedSecretListener = new View.OnClickListener() {
+ @Override
+ public void onClick(final View view) {
+ if (isAccountOnline()) {
+ final String question = mSharedSecretHintEditable.getText().toString();
+ final String secret = mSharedSecretSecret.getText().toString();
+ if (question.trim().isEmpty()) {
+ mSharedSecretHintEditable.requestFocus();
+ mSharedSecretHintEditable.setError(getString(R.string.shared_secret_hint_should_not_be_empty));
+ } else if (secret.trim().isEmpty()) {
+ mSharedSecretSecret.requestFocus();
+ mSharedSecretSecret.setError(getString(R.string.shared_secret_can_not_be_empty));
+ } else {
+ mSharedSecretSecret.setError(null);
+ mSharedSecretHintEditable.setError(null);
+ initSmp(question, secret);
+ updateView();
+ }
+ }
+ }
+ };
+ private View.OnClickListener mCancelSharedSecretListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if (isAccountOnline()) {
+ abortSmp();
+ updateView();
+ }
+ }
+ };
+ private View.OnClickListener mRespondSharedSecretListener = new View.OnClickListener() {
+
+ @Override
+ public void onClick(View view) {
+ if (isAccountOnline()) {
+ final String question = mSharedSecretHintEditable.getText().toString();
+ final String secret = mSharedSecretSecret.getText().toString();
+ respondSmp(question, secret);
+ updateView();
+ }
+ }
+ };
+ private View.OnClickListener mRetrySharedSecretListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ mConversation.smp().status = Conversation.Smp.STATUS_NONE;
+ mConversation.smp().hint = null;
+ mConversation.smp().secret = null;
+ updateView();
+ }
+ };
+ private View.OnClickListener mFinishListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ mConversation.smp().status = Conversation.Smp.STATUS_NONE;
+ finish();
+ }
+ };
+
+ protected boolean initSmp(final String question, final String secret) {
+ final Session session = mConversation.getOtrSession();
+ if (session!=null) {
+ try {
+ session.initSmp(question, secret);
+ mConversation.smp().status = Conversation.Smp.STATUS_WE_REQUESTED;
+ mConversation.smp().secret = secret;
+ mConversation.smp().hint = question;
+ return true;
+ } catch (OtrException e) {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ protected boolean abortSmp() {
+ final Session session = mConversation.getOtrSession();
+ if (session!=null) {
+ try {
+ session.abortSmp();
+ mConversation.smp().status = Conversation.Smp.STATUS_NONE;
+ mConversation.smp().hint = null;
+ mConversation.smp().secret = null;
+ return true;
+ } catch (OtrException e) {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ protected boolean respondSmp(final String question, final String secret) {
+ final Session session = mConversation.getOtrSession();
+ if (session!=null) {
+ try {
+ session.respondSmp(question,secret);
+ return true;
+ } catch (OtrException e) {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ protected boolean verifyWithUri(XmppUri uri) {
+ Contact contact = mConversation.getContact();
+ if (this.mConversation.getContact().getJid().equals(uri.getJid()) && uri.getFingerprint() != null) {
+ contact.addOtrFingerprint(uri.getFingerprint());
+ Toast.makeText(this,R.string.verified,Toast.LENGTH_SHORT).show();
+ updateView();
+ xmppConnectionService.syncRosterToDisk(contact.getAccount());
+ return true;
+ } else {
+ Toast.makeText(this,R.string.could_not_verify_fingerprint,Toast.LENGTH_SHORT).show();
+ return false;
+ }
+ }
+
+ protected boolean isAccountOnline() {
+ if (this.mAccount.getStatus() != Account.State.ONLINE) {
+ Toast.makeText(this,R.string.not_connected_try_again,Toast.LENGTH_SHORT).show();
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ protected boolean handleIntent(Intent intent) {
+ if (intent != null && intent.getAction().equals(ACTION_VERIFY_CONTACT)) {
+ this.mAccount = extractAccount(intent);
+ if (this.mAccount == null) {
+ return false;
+ }
+ try {
+ this.mConversation = this.xmppConnectionService.find(this.mAccount,Jid.fromString(intent.getExtras().getString("contact")));
+ if (this.mConversation == null) {
+ return false;
+ }
+ } catch (final InvalidJidException ignored) {
+ return false;
+ }
+ this.mode = intent.getIntExtra("mode", MODE_MANUAL_VERIFICATION);
+ if (this.mode == MODE_SCAN_FINGERPRINT) {
+ new IntentIntegrator(this).initiateScan();
+ return false;
+ }
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent intent) {
+ if ((requestCode & 0xFFFF) == IntentIntegrator.REQUEST_CODE) {
+ IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent);
+ if (scanResult != null && scanResult.getFormatName() != null) {
+ String data = scanResult.getContents();
+ XmppUri uri = new XmppUri(data);
+ if (xmppConnectionServiceBound) {
+ verifyWithUri(uri);
+ finish();
+ } else {
+ this.mPendingUri = uri;
+ }
+ } else {
+ finish();
+ }
+ }
+ super.onActivityResult(requestCode, requestCode, intent);
+ }
+
+ @Override
+ protected void onBackendConnected() {
+ if (handleIntent(getIntent())) {
+ updateView();
+ } else if (mPendingUri!=null) {
+ verifyWithUri(mPendingUri);
+ finish();
+ mPendingUri = null;
+ }
+ setIntent(null);
+ }
+
+ protected void updateView() {
+ if (this.mConversation.hasValidOtrSession()) {
+ final ActionBar actionBar = getActionBar();
+ this.mVerificationExplain.setText(R.string.no_otr_session_found);
+ invalidateOptionsMenu();
+ switch(this.mode) {
+ case MODE_ASK_QUESTION:
+ if (actionBar != null ) {
+ actionBar.setTitle(R.string.ask_question);
+ }
+ this.updateViewAskQuestion();
+ break;
+ case MODE_ANSWER_QUESTION:
+ if (actionBar != null ) {
+ actionBar.setTitle(R.string.smp_requested);
+ }
+ this.updateViewAnswerQuestion();
+ break;
+ case MODE_MANUAL_VERIFICATION:
+ default:
+ if (actionBar != null ) {
+ actionBar.setTitle(R.string.manually_verify);
+ }
+ this.updateViewManualVerification();
+ break;
+ }
+ } else {
+ this.mManualVerificationArea.setVisibility(View.GONE);
+ this.mSmpVerificationArea.setVisibility(View.GONE);
+ }
+ }
+
+ protected void updateViewManualVerification() {
+ this.mVerificationExplain.setText(R.string.manual_verification_explanation);
+ this.mManualVerificationArea.setVisibility(View.VISIBLE);
+ this.mSmpVerificationArea.setVisibility(View.GONE);
+ this.mYourFingerprint.setText(CryptoHelper.prettifyFingerprint(this.mAccount.getOtrFingerprint()));
+ this.mRemoteFingerprint.setText(CryptoHelper.prettifyFingerprint(this.mConversation.getOtrFingerprint()));
+ if (this.mConversation.isOtrFingerprintVerified()) {
+ deactivateButton(this.mRightButton,R.string.verified);
+ activateButton(this.mLeftButton,R.string.cancel,this.mFinishListener);
+ } else {
+ activateButton(this.mLeftButton,R.string.cancel,this.mFinishListener);
+ activateButton(this.mRightButton,R.string.verify, new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ showManuallyVerifyDialog();
+ }
+ });
+ }
+ }
+
+ protected void updateViewAskQuestion() {
+ this.mManualVerificationArea.setVisibility(View.GONE);
+ this.mSmpVerificationArea.setVisibility(View.VISIBLE);
+ this.mVerificationExplain.setText(R.string.smp_explain_question);
+ final int smpStatus = this.mConversation.smp().status;
+ switch (smpStatus) {
+ case Conversation.Smp.STATUS_WE_REQUESTED:
+ this.mStatusMessage.setVisibility(View.GONE);
+ this.mSharedSecretHintEditable.setVisibility(View.VISIBLE);
+ this.mSharedSecretSecret.setVisibility(View.VISIBLE);
+ this.mSharedSecretHintEditable.setText(this.mConversation.smp().hint);
+ this.mSharedSecretSecret.setText(this.mConversation.smp().secret);
+ this.activateButton(this.mLeftButton, R.string.cancel, this.mCancelSharedSecretListener);
+ this.deactivateButton(this.mRightButton, R.string.in_progress);
+ break;
+ case Conversation.Smp.STATUS_FAILED:
+ this.mStatusMessage.setVisibility(View.GONE);
+ this.mSharedSecretHintEditable.setVisibility(View.VISIBLE);
+ this.mSharedSecretSecret.setVisibility(View.VISIBLE);
+ this.mSharedSecretSecret.requestFocus();
+ this.mSharedSecretSecret.setError(getString(R.string.secrets_do_not_match));
+ this.deactivateButton(this.mLeftButton, R.string.cancel);
+ this.activateButton(this.mRightButton, R.string.try_again, this.mRetrySharedSecretListener);
+ break;
+ case Conversation.Smp.STATUS_VERIFIED:
+ this.mSharedSecretHintEditable.setText("");
+ this.mSharedSecretHintEditable.setVisibility(View.GONE);
+ this.mSharedSecretSecret.setText("");
+ this.mSharedSecretSecret.setVisibility(View.GONE);
+ this.mStatusMessage.setVisibility(View.VISIBLE);
+ this.deactivateButton(this.mLeftButton, R.string.cancel);
+ this.activateButton(this.mRightButton, R.string.finish, this.mFinishListener);
+ break;
+ default:
+ this.mStatusMessage.setVisibility(View.GONE);
+ this.mSharedSecretHintEditable.setVisibility(View.VISIBLE);
+ this.mSharedSecretSecret.setVisibility(View.VISIBLE);
+ this.activateButton(this.mLeftButton,R.string.cancel,this.mFinishListener);
+ this.activateButton(this.mRightButton, R.string.ask_question, this.mCreateSharedSecretListener);
+ break;
+ }
+ }
+
+ protected void updateViewAnswerQuestion() {
+ this.mManualVerificationArea.setVisibility(View.GONE);
+ this.mSmpVerificationArea.setVisibility(View.VISIBLE);
+ this.mVerificationExplain.setText(R.string.smp_explain_answer);
+ this.mSharedSecretHintEditable.setVisibility(View.GONE);
+ this.mSharedSecretHint.setVisibility(View.VISIBLE);
+ this.deactivateButton(this.mLeftButton, R.string.cancel);
+ final int smpStatus = this.mConversation.smp().status;
+ switch (smpStatus) {
+ case Conversation.Smp.STATUS_CONTACT_REQUESTED:
+ this.mStatusMessage.setVisibility(View.GONE);
+ this.mSharedSecretHint.setText(this.mConversation.smp().hint);
+ this.activateButton(this.mRightButton,R.string.respond,this.mRespondSharedSecretListener);
+ break;
+ case Conversation.Smp.STATUS_VERIFIED:
+ this.mSharedSecretHintEditable.setText("");
+ this.mSharedSecretHintEditable.setVisibility(View.GONE);
+ this.mSharedSecretHint.setVisibility(View.GONE);
+ this.mSharedSecretSecret.setText("");
+ this.mSharedSecretSecret.setVisibility(View.GONE);
+ this.mStatusMessage.setVisibility(View.VISIBLE);
+ this.activateButton(this.mRightButton, R.string.finish, this.mFinishListener);
+ break;
+ case Conversation.Smp.STATUS_FAILED:
+ default:
+ this.mSharedSecretSecret.requestFocus();
+ this.mSharedSecretSecret.setError(getString(R.string.secrets_do_not_match));
+ this.activateButton(this.mRightButton,R.string.finish,this.mFinishListener);
+ break;
+ }
+ }
+
+ protected void activateButton(Button button, int text, View.OnClickListener listener) {
+ TextViewUtil.enable(button, ConversationsPlusColors.primaryText(), text);
+ button.setOnClickListener(listener);
+ }
+
+ protected void deactivateButton(Button button, int text) {
+ TextViewUtil.disable(button, ConversationsPlusColors.secondaryText(), text);
+ button.setOnClickListener(null);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_verify_otr);
+ this.mRemoteFingerprint = (TextView) findViewById(R.id.remote_fingerprint);
+ this.mYourFingerprint = (TextView) findViewById(R.id.your_fingerprint);
+ this.mLeftButton = (Button) findViewById(R.id.left_button);
+ this.mRightButton = (Button) findViewById(R.id.right_button);
+ this.mVerificationExplain = (TextView) findViewById(R.id.verification_explanation);
+ this.mStatusMessage = (TextView) findViewById(R.id.status_message);
+ this.mSharedSecretSecret = (EditText) findViewById(R.id.shared_secret_secret);
+ this.mSharedSecretHintEditable = (EditText) findViewById(R.id.shared_secret_hint_editable);
+ this.mSharedSecretHint = (TextView) findViewById(R.id.shared_secret_hint);
+ this.mManualVerificationArea = (LinearLayout) findViewById(R.id.manual_verification_area);
+ this.mSmpVerificationArea = (LinearLayout) findViewById(R.id.smp_verification_area);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(final Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ getMenuInflater().inflate(R.menu.verify_otr, menu);
+ return true;
+ }
+
+ private void showManuallyVerifyDialog() {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(R.string.manually_verify);
+ builder.setMessage(R.string.are_you_sure_verify_fingerprint);
+ builder.setNegativeButton(R.string.cancel, null);
+ builder.setPositiveButton(R.string.verify, mVerifyFingerprintListener);
+ builder.create().show();
+ }
+
+ @Override
+ protected String getShareableUri() {
+ if (mAccount!=null) {
+ return mAccount.getShareableUri();
+ } else {
+ return "";
+ }
+ }
+
+ public void onConversationUpdate() {
+ refreshUi();
+ }
+
+ @Override
+ protected void refreshUiReal() {
+ updateView();
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java
new file mode 100644
index 00000000..26b7909b
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java
@@ -0,0 +1,1174 @@
+package eu.siacs.conversations.ui;
+
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.app.ActionBar;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.AlertDialog.Builder;
+import android.app.PendingIntent;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.content.IntentSender.SendIntentException;
+import android.content.ServiceConnection;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.Point;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.nfc.NdefMessage;
+import android.nfc.NdefRecord;
+import android.nfc.NfcAdapter;
+import android.nfc.NfcEvent;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.PowerManager;
+import android.os.SystemClock;
+import android.preference.PreferenceManager;
+import android.text.InputType;
+import android.util.DisplayMetrics;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.CompoundButton;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.Switch;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.google.zxing.BarcodeFormat;
+import com.google.zxing.EncodeHintType;
+import com.google.zxing.WriterException;
+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 java.io.FileNotFoundException;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.concurrent.RejectedExecutionException;
+
+import de.thedevstack.android.logcat.Logging;
+import de.thedevstack.conversationsplus.ConversationsPlusColors;
+import de.thedevstack.conversationsplus.ConversationsPlusPreferences;
+import de.thedevstack.conversationsplus.utils.ImageUtil;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.MucOptions;
+import eu.siacs.conversations.entities.Presences;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder;
+import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.utils.ExceptionHelper;
+import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
+import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
+import eu.siacs.conversations.xmpp.jid.InvalidJidException;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+public abstract class XmppActivity extends Activity {
+
+ protected static final int REQUEST_ANNOUNCE_PGP = 0x0101;
+ protected static final int REQUEST_INVITE_TO_CONVERSATION = 0x0102;
+ protected static final int REQUEST_CHOOSE_PGP_ID = 0x0103;
+ protected static final int REQUEST_BATTERY_OP = 0x13849ff;
+
+ public static final String EXTRA_ACCOUNT = "account";
+
+ public XmppConnectionService xmppConnectionService;
+ public boolean xmppConnectionServiceBound = false;
+ protected boolean registeredListeners = false;
+
+ private DisplayMetrics metrics;
+ protected int mTheme;
+
+ private long mLastUiRefresh = 0;
+ private Handler mRefreshUiHandler = new Handler();
+ private Runnable mRefreshUiRunnable = new Runnable() {
+ @Override
+ public void run() {
+ mLastUiRefresh = SystemClock.elapsedRealtime();
+ refreshUiReal();
+ }
+ };
+
+ protected ConferenceInvite mPendingConferenceInvite = null;
+
+
+ protected final void refreshUi() {
+ final long diff = SystemClock.elapsedRealtime() - mLastUiRefresh;
+ if (diff > Config.REFRESH_UI_INTERVAL) {
+ mRefreshUiHandler.removeCallbacks(mRefreshUiRunnable);
+ runOnUiThread(mRefreshUiRunnable);
+ } else {
+ final long next = Config.REFRESH_UI_INTERVAL - diff;
+ mRefreshUiHandler.removeCallbacks(mRefreshUiRunnable);
+ mRefreshUiHandler.postDelayed(mRefreshUiRunnable,next);
+ }
+ }
+
+ abstract protected void refreshUiReal();
+
+ protected interface OnValueEdited {
+ public void onValueEdited(String value);
+ }
+
+ public interface OnPresenceSelected {
+ public void onPresenceSelected();
+ }
+
+ protected ServiceConnection mConnection = new ServiceConnection() {
+
+ @Override
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ XmppConnectionBinder binder = (XmppConnectionBinder) service;
+ xmppConnectionService = binder.getService();
+ xmppConnectionServiceBound = true;
+ if (!registeredListeners && shouldRegisterListeners()) {
+ registerListeners();
+ registeredListeners = true;
+ }
+ onBackendConnected();
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName arg0) {
+ xmppConnectionServiceBound = false;
+ }
+ };
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ if (!xmppConnectionServiceBound) {
+ connectToBackend();
+ } else {
+ if (!registeredListeners) {
+ this.registerListeners();
+ this.registeredListeners = true;
+ }
+ this.onBackendConnected();
+ }
+ }
+
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
+ protected boolean shouldRegisterListeners() {
+ if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ return !isDestroyed() && !isFinishing();
+ } else {
+ return !isFinishing();
+ }
+ }
+
+ public void connectToBackend() {
+ Intent intent = new Intent(this, XmppConnectionService.class);
+ intent.setAction("ui");
+ startService(intent);
+ bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ if (xmppConnectionServiceBound) {
+ if (registeredListeners) {
+ this.unregisterListeners();
+ this.registeredListeners = false;
+ }
+ unbindService(mConnection);
+ xmppConnectionServiceBound = false;
+ }
+ }
+
+ protected void hideKeyboard() {
+ InputMethodManager inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+
+ View focus = getCurrentFocus();
+
+ if (focus != null) {
+
+ inputManager.hideSoftInputFromWindow(focus.getWindowToken(),
+ InputMethodManager.HIDE_NOT_ALWAYS);
+ }
+ }
+
+ public boolean hasPgp() {
+ return xmppConnectionService.getPgpEngine() != null;
+ }
+
+ public void showInstallPgpDialog() {
+ Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(getString(R.string.openkeychain_required));
+ builder.setIconAttribute(android.R.attr.alertDialogIcon);
+ builder.setMessage(getText(R.string.openkeychain_required_long));
+ builder.setNegativeButton(getString(R.string.cancel), null);
+ builder.setNeutralButton(getString(R.string.restart),
+ new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (xmppConnectionServiceBound) {
+ unbindService(mConnection);
+ xmppConnectionServiceBound = false;
+ }
+ stopService(new Intent(XmppActivity.this,
+ XmppConnectionService.class));
+ finish();
+ }
+ });
+ builder.setPositiveButton(getString(R.string.install),
+ new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Uri uri = Uri
+ .parse("market://details?id=org.sufficientlysecure.keychain");
+ Intent marketIntent = new Intent(Intent.ACTION_VIEW,
+ uri);
+ PackageManager manager = getApplicationContext()
+ .getPackageManager();
+ List<ResolveInfo> infos = manager
+ .queryIntentActivities(marketIntent, 0);
+ if (infos.size() > 0) {
+ startActivity(marketIntent);
+ } else {
+ uri = Uri.parse("http://www.openkeychain.org/");
+ Intent browserIntent = new Intent(
+ Intent.ACTION_VIEW, uri);
+ startActivity(browserIntent);
+ }
+ finish();
+ }
+ });
+ builder.create().show();
+ }
+
+ abstract void onBackendConnected();
+
+ protected void registerListeners() {
+ if (this instanceof XmppConnectionService.OnConversationUpdate) {
+ this.xmppConnectionService.setOnConversationListChangedListener((XmppConnectionService.OnConversationUpdate) this);
+ }
+ if (this instanceof XmppConnectionService.OnAccountUpdate) {
+ this.xmppConnectionService.setOnAccountListChangedListener((XmppConnectionService.OnAccountUpdate) this);
+ }
+ if (this instanceof XmppConnectionService.OnCaptchaRequested) {
+ this.xmppConnectionService.setOnCaptchaRequestedListener((XmppConnectionService.OnCaptchaRequested) this);
+ }
+ if (this instanceof XmppConnectionService.OnRosterUpdate) {
+ this.xmppConnectionService.setOnRosterUpdateListener((XmppConnectionService.OnRosterUpdate) this);
+ }
+ if (this instanceof XmppConnectionService.OnMucRosterUpdate) {
+ this.xmppConnectionService.setOnMucRosterUpdateListener((XmppConnectionService.OnMucRosterUpdate) this);
+ }
+ if (this instanceof OnUpdateBlocklist) {
+ this.xmppConnectionService.setOnUpdateBlocklistListener((OnUpdateBlocklist) this);
+ }
+ if (this instanceof XmppConnectionService.OnShowErrorToast) {
+ this.xmppConnectionService.setOnShowErrorToastListener((XmppConnectionService.OnShowErrorToast) this);
+ }
+ if (this instanceof OnKeyStatusUpdated) {
+ this.xmppConnectionService.setOnKeyStatusUpdatedListener((OnKeyStatusUpdated) this);
+ }
+ }
+
+ protected void unregisterListeners() {
+ if (this instanceof XmppConnectionService.OnConversationUpdate) {
+ this.xmppConnectionService.removeOnConversationListChangedListener();
+ }
+ if (this instanceof XmppConnectionService.OnAccountUpdate) {
+ this.xmppConnectionService.removeOnAccountListChangedListener();
+ }
+ if (this instanceof XmppConnectionService.OnCaptchaRequested) {
+ this.xmppConnectionService.removeOnCaptchaRequestedListener();
+ }
+ if (this instanceof XmppConnectionService.OnRosterUpdate) {
+ this.xmppConnectionService.removeOnRosterUpdateListener();
+ }
+ if (this instanceof XmppConnectionService.OnMucRosterUpdate) {
+ this.xmppConnectionService.removeOnMucRosterUpdateListener();
+ }
+ if (this instanceof OnUpdateBlocklist) {
+ this.xmppConnectionService.removeOnUpdateBlocklistListener();
+ }
+ if (this instanceof XmppConnectionService.OnShowErrorToast) {
+ this.xmppConnectionService.removeOnShowErrorToastListener();
+ }
+ if (this instanceof OnKeyStatusUpdated) {
+ this.xmppConnectionService.removeOnNewKeysAvailableListener();
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.action_settings:
+ startActivity(new Intent(this, SettingsActivity.class));
+ break;
+ case R.id.action_accounts:
+ startActivity(new Intent(this, ManageAccountActivity.class));
+ break;
+ case android.R.id.home:
+ finish();
+ break;
+ case R.id.action_show_qr_code:
+ showQrCode();
+ break;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ metrics = getResources().getDisplayMetrics();
+ ExceptionHelper.init(getApplicationContext());
+ this.mTheme = findTheme();
+ setTheme(this.mTheme);
+ final ActionBar ab = getActionBar();
+ if (ab!=null) {
+ ab.setDisplayHomeAsUpEnabled(true);
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ final MenuItem menuSettings = menu.findItem(R.id.action_settings);
+ final MenuItem menuManageAccounts = menu.findItem(R.id.action_accounts);
+ if (menuSettings != null) {
+ menuSettings.setVisible(!Config.LOCK_SETTINGS);
+ }
+ if (menuManageAccounts != null) {
+ menuManageAccounts.setVisible(!Config.LOCK_SETTINGS);
+ }
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ protected boolean isOptimizingBattery() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
+ return !pm.isIgnoringBatteryOptimizations(getPackageName());
+ } else {
+ return false;
+ }
+ }
+
+ public void switchToConversation(Conversation conversation) {
+ switchToConversation(conversation, null, false);
+ }
+
+ public void switchToConversation(Conversation conversation, String text,
+ boolean newTask) {
+ switchToConversation(conversation,text,null,false,newTask);
+ }
+
+ public void highlightInMuc(Conversation conversation, String nick) {
+ switchToConversation(conversation, null, nick, false, false);
+ }
+
+ public void privateMsgInMuc(Conversation conversation, String nick) {
+ switchToConversation(conversation, null, nick, true, false);
+ }
+
+ private void switchToConversation(Conversation conversation, String text, String nick, boolean pm, boolean newTask) {
+ Intent viewConversationIntent = new Intent(this,
+ ConversationActivity.class);
+ viewConversationIntent.setAction(Intent.ACTION_VIEW);
+ viewConversationIntent.putExtra(ConversationActivity.CONVERSATION,
+ conversation.getUuid());
+ if (text != null) {
+ viewConversationIntent.putExtra(ConversationActivity.TEXT, text);
+ }
+ if (nick != null) {
+ viewConversationIntent.putExtra(ConversationActivity.NICK, nick);
+ viewConversationIntent.putExtra(ConversationActivity.PRIVATE_MESSAGE,pm);
+ }
+ viewConversationIntent.setType(ConversationActivity.VIEW_CONVERSATION);
+ if (newTask) {
+ viewConversationIntent.setFlags(viewConversationIntent.getFlags()
+ | Intent.FLAG_ACTIVITY_NEW_TASK
+ | Intent.FLAG_ACTIVITY_SINGLE_TOP);
+ } else {
+ viewConversationIntent.setFlags(viewConversationIntent.getFlags()
+ | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ }
+ startActivity(viewConversationIntent);
+ finish();
+ }
+
+ public void switchToContactDetails(Contact contact) {
+ switchToContactDetails(contact, null);
+ }
+
+ public void switchToContactDetails(Contact contact, String messageFingerprint) {
+ Intent intent = new Intent(this, ContactDetailsActivity.class);
+ intent.setAction(ContactDetailsActivity.ACTION_VIEW_CONTACT);
+ intent.putExtra(EXTRA_ACCOUNT, contact.getAccount().getJid().toBareJid().toString());
+ intent.putExtra("contact", contact.getJid().toString());
+ intent.putExtra("fingerprint", messageFingerprint);
+ startActivity(intent);
+ }
+
+ public void switchToAccount(Account account) {
+ switchToAccount(account, false);
+ }
+
+ public void switchToAccount(Account account, boolean init) {
+ Intent intent = new Intent(this, EditAccountActivity.class);
+ intent.putExtra("jid", account.getJid().toBareJid().toString());
+ intent.putExtra("init", init);
+ startActivity(intent);
+ }
+
+ protected void inviteToConversation(Conversation conversation) {
+ Intent intent = new Intent(getApplicationContext(),
+ ChooseContactActivity.class);
+ List<String> contacts = new ArrayList<>();
+ if (conversation.getMode() == Conversation.MODE_MULTI) {
+ for (MucOptions.User user : conversation.getMucOptions().getUsers()) {
+ Jid jid = user.getJid();
+ if (jid != null) {
+ contacts.add(jid.toBareJid().toString());
+ }
+ }
+ } else {
+ contacts.add(conversation.getJid().toBareJid().toString());
+ }
+ intent.putExtra("filter_contacts", contacts.toArray(new String[contacts.size()]));
+ intent.putExtra("conversation", conversation.getUuid());
+ intent.putExtra("multiple", true);
+ intent.putExtra("show_enter_jid", true);
+ intent.putExtra(EXTRA_ACCOUNT, conversation.getAccount().getJid().toBareJid().toString());
+ startActivityForResult(intent, REQUEST_INVITE_TO_CONVERSATION);
+ }
+
+ protected void announcePgp(Account account, final Conversation conversation) {
+ if (account.getPgpId() == -1) {
+ choosePgpSignId(account);
+ } else {
+ xmppConnectionService.getPgpEngine().generateSignature(account, "", new UiCallback<Account>() {
+
+ @Override
+ public void userInputRequried(PendingIntent pi,
+ Account account) {
+ try {
+ startIntentSenderForResult(pi.getIntentSender(),
+ REQUEST_ANNOUNCE_PGP, null, 0, 0, 0);
+ } catch (final SendIntentException ignored) {
+ }
+ }
+
+ @Override
+ public void success(Account account) {
+ xmppConnectionService.databaseBackend.updateAccount(account);
+ xmppConnectionService.sendPresence(account);
+ if (conversation != null) {
+ conversation.setNextEncryption(Message.ENCRYPTION_PGP);
+ xmppConnectionService.databaseBackend.updateConversation(conversation);
+ }
+ }
+
+ @Override
+ public void error(int error, Account account) {
+ displayErrorDialog(error);
+ }
+ });
+ }
+ }
+
+ protected void choosePgpSignId(Account account) {
+ xmppConnectionService.getPgpEngine().chooseKey(account, new UiCallback<Account>() {
+ @Override
+ public void success(Account account1) {
+ }
+
+ @Override
+ public void error(int errorCode, Account object) {
+
+ }
+
+ @Override
+ public void userInputRequried(PendingIntent pi, Account object) {
+ try {
+ startIntentSenderForResult(pi.getIntentSender(),
+ REQUEST_CHOOSE_PGP_ID, null, 0, 0, 0);
+ } catch (final SendIntentException ignored) {
+ }
+ }
+ });
+ }
+
+ public void displayErrorDialog(final int errorCode) {
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ AlertDialog.Builder builder = new AlertDialog.Builder(
+ XmppActivity.this);
+ builder.setIconAttribute(android.R.attr.alertDialogIcon);
+ builder.setTitle(getString(R.string.error));
+ builder.setMessage(errorCode);
+ builder.setNeutralButton(R.string.accept, null);
+ builder.create().show();
+ }
+ });
+
+ }
+
+ protected void showAddToRosterDialog(final Conversation conversation) {
+ showAddToRosterDialog(conversation.getContact());
+ }
+
+ protected void showAddToRosterDialog(final Contact contact) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(contact.getJid().toString());
+ builder.setMessage(getString(R.string.not_in_roster));
+ builder.setNegativeButton(getString(R.string.cancel), null);
+ builder.setPositiveButton(getString(R.string.add_contact),
+ new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ final Jid jid = contact.getJid();
+ Account account = contact.getAccount();
+ Contact contact = account.getRoster().getContact(jid);
+ xmppConnectionService.createContact(contact);
+ }
+ });
+ builder.create().show();
+ }
+
+ private void showAskForPresenceDialog(final Contact contact) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(contact.getJid().toString());
+ builder.setMessage(R.string.request_presence_updates);
+ builder.setNegativeButton(R.string.cancel, null);
+ builder.setPositiveButton(R.string.request_now,
+ new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (xmppConnectionServiceBound) {
+ xmppConnectionService.sendPresencePacket(contact
+ .getAccount(), xmppConnectionService
+ .getPresenceGenerator()
+ .requestPresenceUpdatesFrom(contact));
+ }
+ }
+ });
+ builder.create().show();
+ }
+
+ private void warnMutalPresenceSubscription(final Conversation conversation,
+ final OnPresenceSelected listener) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(conversation.getContact().getJid().toString());
+ builder.setMessage(R.string.without_mutual_presence_updates);
+ builder.setNegativeButton(R.string.cancel, null);
+ builder.setPositiveButton(R.string.ignore, new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ conversation.setNextCounterpart(null);
+ if (listener != null) {
+ listener.onPresenceSelected();
+ }
+ }
+ });
+ builder.create().show();
+ }
+
+ protected void quickEdit(String previousValue, OnValueEdited callback) {
+ quickEdit(previousValue, callback, false);
+ }
+
+ protected void quickPasswordEdit(String previousValue,
+ OnValueEdited callback) {
+ quickEdit(previousValue, callback, true);
+ }
+
+ @SuppressLint("InflateParams")
+ private void quickEdit(final String previousValue,
+ final OnValueEdited callback, boolean password) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ View view = getLayoutInflater().inflate(R.layout.quickedit, null);
+ final EditText editor = (EditText) view.findViewById(R.id.editor);
+ OnClickListener mClickListener = new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ String value = editor.getText().toString();
+ if (!previousValue.equals(value) && value.trim().length() > 0) {
+ callback.onValueEdited(value);
+ }
+ }
+ };
+ if (password) {
+ editor.setInputType(InputType.TYPE_CLASS_TEXT
+ | InputType.TYPE_TEXT_VARIATION_PASSWORD);
+ editor.setHint(R.string.password);
+ builder.setPositiveButton(R.string.accept, mClickListener);
+ } else {
+ builder.setPositiveButton(R.string.edit, mClickListener);
+ }
+ editor.requestFocus();
+ editor.setText(previousValue);
+ builder.setView(view);
+ builder.setNegativeButton(R.string.cancel, null);
+ 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;
+ }
+ };
+ 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());
+ trustToggle.setEnabled(!Config.X509_VERIFICATION || trust != XmppAxolotlSession.Trust.TRUSTED_X509);
+ if (Config.X509_VERIFICATION && trust == XmppAxolotlSession.Trust.TRUSTED_X509) {
+ trustToggle.setOnClickListener(null);
+ }
+ key.setTextColor(ConversationsPlusColors.primaryText());
+ keyType.setTextColor(ConversationsPlusColors.secondaryText());
+ break;
+ case UNDECIDED:
+ trustToggle.setChecked(false);
+ trustToggle.setEnabled(false);
+ key.setTextColor(ConversationsPlusColors.primaryText());
+ keyType.setTextColor(ConversationsPlusColors.secondaryText());
+ break;
+ case INACTIVE_UNTRUSTED:
+ case INACTIVE_UNDECIDED:
+ trustToggle.setOnClickListener(null);
+ trustToggle.setChecked(false);
+ trustToggle.setEnabled(false);
+ key.setTextColor(ConversationsPlusColors.tertiaryText());
+ keyType.setTextColor(ConversationsPlusColors.tertiaryText());
+ break;
+ case INACTIVE_TRUSTED:
+ case INACTIVE_TRUSTED_X509:
+ trustToggle.setOnClickListener(null);
+ trustToggle.setChecked(true);
+ trustToggle.setEnabled(false);
+ key.setTextColor(ConversationsPlusColors.tertiaryText());
+ keyType.setTextColor(ConversationsPlusColors.tertiaryText());
+ 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(ConversationsPlusColors.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);
+ 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) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
+ requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, requestCode);
+ return false;
+ } else {
+ return true;
+ }
+ } else {
+ return true;
+ }
+ }
+
+ public void selectPresence(final Conversation conversation,
+ final OnPresenceSelected listener) {
+ final Contact contact = conversation.getContact();
+ if (conversation.hasValidOtrSession()) {
+ SessionID id = conversation.getOtrSession().getSessionID();
+ Jid jid;
+ try {
+ jid = Jid.fromString(id.getAccountID() + "/" + id.getUserID());
+ } catch (InvalidJidException e) {
+ jid = null;
+ }
+ conversation.setNextCounterpart(jid);
+ listener.onPresenceSelected();
+ } else if (!contact.showInRoster()) {
+ showAddToRosterDialog(conversation);
+ } else {
+ Presences presences = contact.getPresences();
+ if (presences.size() == 0) {
+ if (!contact.getOption(Contact.Options.TO)
+ && !contact.getOption(Contact.Options.ASKING)
+ && contact.getAccount().getStatus() == Account.State.ONLINE) {
+ showAskForPresenceDialog(contact);
+ } else if (!contact.getOption(Contact.Options.TO)
+ || !contact.getOption(Contact.Options.FROM)) {
+ warnMutalPresenceSubscription(conversation, listener);
+ } else {
+ conversation.setNextCounterpart(null);
+ listener.onPresenceSelected();
+ }
+ } else if (presences.size() == 1) {
+ String presence = presences.asStringArray()[0];
+ try {
+ conversation.setNextCounterpart(Jid.fromParts(contact.getJid().getLocalpart(),contact.getJid().getDomainpart(),presence));
+ } catch (InvalidJidException e) {
+ conversation.setNextCounterpart(null);
+ }
+ listener.onPresenceSelected();
+ } else {
+ final StringBuilder presence = new StringBuilder();
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(getString(R.string.choose_presence));
+ final String[] presencesArray = presences.asStringArray();
+ int preselectedPresence = 0;
+ for (int i = 0; i < presencesArray.length; ++i) {
+ if (presencesArray[i].equals(contact.lastseen.presence)) {
+ preselectedPresence = i;
+ break;
+ }
+ }
+ presence.append(presencesArray[preselectedPresence]);
+ builder.setSingleChoiceItems(presencesArray,
+ preselectedPresence,
+ new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog,
+ int which) {
+ presence.delete(0, presence.length());
+ presence.append(presencesArray[which]);
+ }
+ });
+ builder.setNegativeButton(R.string.cancel, null);
+ builder.setPositiveButton(R.string.ok, new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ try {
+ conversation.setNextCounterpart(Jid.fromParts(contact.getJid().getLocalpart(),contact.getJid().getDomainpart(),presence.toString()));
+ } catch (InvalidJidException e) {
+ conversation.setNextCounterpart(null);
+ }
+ listener.onPresenceSelected();
+ }
+ });
+ builder.create().show();
+ }
+ }
+ }
+
+ protected void onActivityResult(int requestCode, int resultCode,
+ final Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == REQUEST_INVITE_TO_CONVERSATION && resultCode == RESULT_OK) {
+ mPendingConferenceInvite = ConferenceInvite.parse(data);
+ if (xmppConnectionServiceBound && mPendingConferenceInvite != null) {
+ mPendingConferenceInvite.execute(this);
+ mPendingConferenceInvite = null;
+ }
+ }
+ }
+
+ private UiCallback<Conversation> adhocCallback = new UiCallback<Conversation>() {
+ @Override
+ public void success(final Conversation conversation) {
+ switchToConversation(conversation);
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(XmppActivity.this,R.string.conference_created,Toast.LENGTH_LONG).show();
+ }
+ });
+ }
+
+ @Override
+ public void error(final int errorCode, Conversation object) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(XmppActivity.this,errorCode,Toast.LENGTH_LONG).show();
+ }
+ });
+ }
+
+ @Override
+ public void userInputRequried(PendingIntent pi, Conversation object) {
+
+ }
+ };
+
+ public int getPixel(int dp) {
+ DisplayMetrics metrics = getResources().getDisplayMetrics();
+ return ((int) (dp * metrics.density));
+ }
+
+ public boolean copyTextToClipboard(String text, int labelResId) {
+ ClipboardManager mClipBoardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
+ String label = getResources().getString(labelResId);
+ if (mClipBoardManager != null) {
+ ClipData mClipData = ClipData.newPlainText(label, text);
+ mClipBoardManager.setPrimaryClip(mClipData);
+ return true;
+ }
+ return false;
+ }
+
+ protected void registerNdefPushMessageCallback() {
+ NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this);
+ if (nfcAdapter != null && nfcAdapter.isEnabled()) {
+ nfcAdapter.setNdefPushMessageCallback(new NfcAdapter.CreateNdefMessageCallback() {
+ @Override
+ public NdefMessage createNdefMessage(NfcEvent nfcEvent) {
+ return new NdefMessage(new NdefRecord[]{
+ NdefRecord.createUri(getShareableUri()),
+ NdefRecord.createApplicationRecord("eu.siacs.conversations")
+ });
+ }
+ }, this);
+ }
+ }
+
+ protected void unregisterNdefPushMessageCallback() {
+ NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this);
+ if (nfcAdapter != null && nfcAdapter.isEnabled()) {
+ nfcAdapter.setNdefPushMessageCallback(null,this);
+ }
+ }
+
+ protected String getShareableUri() {
+ return null;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (this.getShareableUri()!=null) {
+ this.registerNdefPushMessageCallback();
+ }
+ }
+
+ protected int findTheme() {
+ if (ConversationsPlusPreferences.useLargerFont()) {
+ return R.style.ConversationsTheme_LargerText;
+ } else {
+ return R.style.ConversationsTheme;
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ this.unregisterNdefPushMessageCallback();
+ }
+
+ protected void showQrCode() {
+ String uri = getShareableUri();
+ if (uri!=null) {
+ Point size = new Point();
+ getWindowManager().getDefaultDisplay().getSize(size);
+ final int width = (size.x < size.y ? size.x : size.y);
+ Bitmap bitmap = createQrCodeBitmap(uri, width);
+ ImageView view = new ImageView(this);
+ view.setImageBitmap(bitmap);
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setView(view);
+ builder.create().show();
+ }
+ }
+
+ protected Bitmap createQrCodeBitmap(String input, int size) {
+ Logging.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);
+ Logging.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) {
+ String jid = intent != null ? intent.getStringExtra(EXTRA_ACCOUNT) : null;
+ try {
+ return jid != null ? xmppConnectionService.findAccountByJid(Jid.fromString(jid)) : null;
+ } catch (InvalidJidException e) {
+ return null;
+ }
+ }
+
+ public static class ConferenceInvite {
+ private String uuid;
+ private List<Jid> jids = new ArrayList<>();
+
+ public static ConferenceInvite parse(Intent data) {
+ ConferenceInvite invite = new ConferenceInvite();
+ invite.uuid = data.getStringExtra("conversation");
+ if (invite.uuid == null) {
+ return null;
+ }
+ try {
+ if (data.getBooleanExtra("multiple", false)) {
+ String[] toAdd = data.getStringArrayExtra("contacts");
+ for (String item : toAdd) {
+ invite.jids.add(Jid.fromString(item));
+ }
+ } else {
+ invite.jids.add(Jid.fromString(data.getStringExtra("contact")));
+ }
+ } catch (final InvalidJidException ignored) {
+ return null;
+ }
+ return invite;
+ }
+
+ public void execute(XmppActivity activity) {
+ XmppConnectionService service = activity.xmppConnectionService;
+ Conversation conversation = service.findConversationByUuid(this.uuid);
+ if (conversation == null) {
+ return;
+ }
+ if (conversation.getMode() == Conversation.MODE_MULTI) {
+ for (Jid jid : jids) {
+ service.invite(conversation, jid);
+ }
+ } else {
+ jids.add(conversation.getJid().toBareJid());
+ service.createAdhocConference(conversation.getAccount(), jids, activity.adhocCallback);
+ }
+ }
+ }
+
+ class BitmapWorkerTask extends AsyncTask<Message, Void, Bitmap> {
+ private final WeakReference<ImageView> imageViewReference;
+ private final boolean setSize;
+ private Message message = null;
+
+ public BitmapWorkerTask(ImageView imageView, boolean setSize) {
+ imageViewReference = new WeakReference<>(imageView);
+ this.setSize = setSize;
+ }
+
+ @Override
+ protected Bitmap doInBackground(Message... params) {
+ message = params[0];
+ try {
+ return ImageUtil.getThumbnail(message, (int) (metrics.density * 288), false);
+ } catch (FileNotFoundException e) {
+ return null;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(Bitmap bitmap) {
+ if (bitmap != null) {
+ final ImageView imageView = imageViewReference.get();
+ if (imageView != null) {
+ imageView.setImageBitmap(bitmap);
+ imageView.setBackgroundColor(0x00000000);
+ if (setSize) {
+ imageView.setLayoutParams(new LinearLayout.LayoutParams(
+ bitmap.getWidth(), bitmap.getHeight()));
+ }
+ }
+ }
+ }
+ }
+
+ public void loadBitmap(Message message, ImageView imageView, boolean setSize) {
+ Bitmap bm;
+ try {
+ bm = ImageUtil.getThumbnail(message,(int) (metrics.density * 288), true);
+ } catch (FileNotFoundException e) {
+ bm = null;
+ }
+
+ if (bm != null) {
+ imageView.setImageBitmap(bm);
+ imageView.setBackgroundColor(0x00000000);
+ if (setSize) {
+ imageView.setLayoutParams(new LinearLayout.LayoutParams(
+ bm.getWidth(), bm.getHeight()));
+ }
+ } else {
+ if (cancelPotentialWork(message, imageView)) {
+ imageView.setBackgroundColor(0xff333333);
+ imageView.setImageDrawable(null);
+ final BitmapWorkerTask task = new BitmapWorkerTask(imageView, setSize);
+ final AsyncDrawable asyncDrawable = new AsyncDrawable(
+ getResources(), null, task);
+ imageView.setImageDrawable(asyncDrawable);
+ try {
+ task.execute(message);
+ } catch (final RejectedExecutionException ignored) {
+ }
+ }
+ }
+ }
+
+ public static boolean cancelPotentialWork(Message message,
+ ImageView imageView) {
+ final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
+
+ if (bitmapWorkerTask != null) {
+ final Message oldMessage = bitmapWorkerTask.message;
+ if (oldMessage == null || message != oldMessage) {
+ bitmapWorkerTask.cancel(true);
+ } else {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
+ if (imageView != null) {
+ final Drawable drawable = imageView.getDrawable();
+ if (drawable instanceof AsyncDrawable) {
+ final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
+ return asyncDrawable.getBitmapWorkerTask();
+ }
+ }
+ return null;
+ }
+
+ static class AsyncDrawable extends BitmapDrawable {
+ private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
+
+ public AsyncDrawable(Resources res, Bitmap bitmap,
+ BitmapWorkerTask bitmapWorkerTask) {
+ super(res, bitmap);
+ bitmapWorkerTaskReference = new WeakReference<>(
+ bitmapWorkerTask);
+ }
+
+ public BitmapWorkerTask getBitmapWorkerTask() {
+ return bitmapWorkerTaskReference.get();
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java
new file mode 100644
index 00000000..55d3f5a7
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java
@@ -0,0 +1,77 @@
+package eu.siacs.conversations.ui.adapter;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.CompoundButton;
+import android.widget.ImageView;
+import android.widget.Switch;
+import android.widget.TextView;
+
+import java.util.List;
+
+import de.thedevstack.conversationsplus.ConversationsPlusColors;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.services.AvatarService;
+import eu.siacs.conversations.ui.ManageAccountActivity;
+import eu.siacs.conversations.ui.XmppActivity;
+
+public class AccountAdapter extends ArrayAdapter<Account> {
+
+ private XmppActivity activity;
+
+ public AccountAdapter(XmppActivity activity, List<Account> objects) {
+ super(activity, 0, objects);
+ this.activity = activity;
+ }
+
+ @Override
+ public View getView(int position, View view, ViewGroup parent) {
+ final Account account = getItem(position);
+ if (view == null) {
+ LayoutInflater inflater = (LayoutInflater) getContext()
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ view = inflater.inflate(R.layout.account_row, parent, false);
+ }
+ TextView jid = (TextView) view.findViewById(R.id.account_jid);
+ if (Config.DOMAIN_LOCK != null) {
+ jid.setText(account.getJid().getLocalpart());
+ } else {
+ jid.setText(account.getJid().toBareJid().toString());
+ }
+ TextView statusView = (TextView) view.findViewById(R.id.account_status);
+ ImageView imageView = (ImageView) view.findViewById(R.id.account_image);
+ imageView.setImageBitmap(AvatarService.getInstance().get(account, activity.getPixel(48)));
+ statusView.setText(getContext().getString(account.getStatus().getReadableId()));
+ switch (account.getStatus()) {
+ case ONLINE:
+ statusView.setTextColor(ConversationsPlusColors.online());
+ break;
+ case DISABLED:
+ case CONNECTING:
+ statusView.setTextColor(ConversationsPlusColors.secondaryText());
+ break;
+ default:
+ statusView.setTextColor(ConversationsPlusColors.warning());
+ break;
+ }
+ final Switch tglAccountState = (Switch) view.findViewById(R.id.tgl_account_status);
+ final boolean isDisabled = (account.getStatus() == Account.State.DISABLED);
+ tglAccountState.setChecked(!isDisabled);
+ tglAccountState.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
+ // Condition compoundButton.isPressed() added because of http://stackoverflow.com/a/28219410
+ if (compoundButton.isPressed() && b == isDisabled && activity instanceof ManageAccountActivity) {
+ ((ManageAccountActivity) activity).onClickTglAccountState(account, b);
+ }
+ }
+ });
+ return view;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java
new file mode 100644
index 00000000..317dcc78
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java
@@ -0,0 +1,247 @@
+package eu.siacs.conversations.ui.adapter;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.Typeface;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
+import android.util.Pair;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import java.lang.ref.WeakReference;
+import java.util.List;
+import java.util.concurrent.RejectedExecutionException;
+
+import de.thedevstack.conversationsplus.ConversationsPlusColors;
+import de.thedevstack.conversationsplus.ConversationsPlusPreferences;
+import de.thedevstack.conversationsplus.ui.listeners.ShowResourcesListDialogListener;
+import de.tzur.conversations.Settings;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.Presences;
+import eu.siacs.conversations.entities.Transferable;
+import eu.siacs.conversations.services.AvatarService;
+import eu.siacs.conversations.ui.ConversationActivity;
+import eu.siacs.conversations.ui.XmppActivity;
+import eu.siacs.conversations.utils.UIHelper;
+
+public class ConversationAdapter extends ArrayAdapter<Conversation> {
+
+ private XmppActivity activity;
+
+ public ConversationAdapter(XmppActivity activity,
+ List<Conversation> conversations) {
+ super(activity, 0, conversations);
+ this.activity = activity;
+ }
+
+ @Override
+ public View getView(int position, View view, ViewGroup parent) {
+ if (view == null) {
+ LayoutInflater inflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ view = inflater.inflate(R.layout.conversation_list_row,parent, false);
+ }
+ Conversation conversation = getItem(position);
+ // Highlight the currently selected conversation
+ if (this.activity instanceof ConversationActivity) {
+ ConversationActivity a = (ConversationActivity) this.activity;
+ int c = conversation == a.getSelectedConversation() ? ConversationsPlusColors.secondaryBackground() : ConversationsPlusColors.primaryBackground();
+ view.findViewById(R.id.conversationListRowContent).setBackgroundColor(c);
+ view.findViewById(R.id.conversationListRowFrame).setBackgroundColor(c);
+ }
+ TextView convName = (TextView) view.findViewById(R.id.conversation_name);
+ if (conversation.getMode() == Conversation.MODE_SINGLE || ConversationsPlusPreferences.useSubject()) {
+ convName.setText(conversation.getName());
+ } else {
+ convName.setText(conversation.getJid().toBareJid().toString());
+ }
+ TextView mLastMessage = (TextView) view.findViewById(R.id.conversation_lastmsg);
+ TextView mTimestamp = (TextView) view.findViewById(R.id.conversation_lastupdate);
+ ImageView imagePreview = (ImageView) view.findViewById(R.id.conversation_lastimage);
+ ImageView notificationStatus = (ImageView) view.findViewById(R.id.notification_status);
+ TextView txtUnreadCount = (TextView) view.findViewById(R.id.conversation_unreadcount);
+
+ if (Settings.SHOW_ONLINE_STATUS) {
+ int color = ConversationsPlusColors.offline();
+ if (conversation.getAccount().getStatus() == Account.State.ONLINE) {
+ if (conversation.getMode() == Conversation.MODE_SINGLE) {
+ color = UIHelper.getStatusColor(conversation.getContact().getMostAvailableStatus());
+ } else if (conversation.getMode() == Conversation.MODE_MULTI && conversation.getMucOptions().online()) {
+ color = ConversationsPlusColors.online();
+ }
+ }
+ TextView status = (TextView) view.findViewById(R.id.status);
+ status.setBackgroundColor(color);
+ }
+
+ Message message = conversation.getLatestMessage();
+
+ int txtUnreadCountVisibility = View.INVISIBLE;
+ if (!conversation.isRead()) {
+ convName.setTypeface(null, Typeface.BOLD);
+ txtUnreadCountVisibility = View.VISIBLE;
+ txtUnreadCount.setText(String.valueOf(conversation.unreadCount()));
+ } else {
+ convName.setTypeface(null, Typeface.NORMAL);
+ }
+ txtUnreadCount.setVisibility(txtUnreadCountVisibility);
+
+ if ((null != message.getFileParams() && message.getFileParams().getWidth() > 0) // TODO: Use FileParams.getMimeType()
+ && (message.getTransferable() == null
+ || message.getTransferable().getStatus() != Transferable.STATUS_DELETED)) {
+ mLastMessage.setVisibility(View.GONE);
+ imagePreview.setVisibility(View.VISIBLE);
+ activity.loadBitmap(message, imagePreview, false);
+ } else {
+ Pair<String,Boolean> preview = UIHelper.getMessagePreview(activity,message);
+ mLastMessage.setVisibility(View.VISIBLE);
+ imagePreview.setVisibility(View.GONE);
+ CharSequence msgText = preview.first;
+ String msgPrefix = null;
+ if (message.getStatus() == Message.STATUS_SEND
+ || message.getStatus() == Message.STATUS_SEND_DISPLAYED
+ || message.getStatus() == Message.STATUS_SEND_FAILED
+ || message.getStatus() == Message.STATUS_SEND_RECEIVED) {
+ msgPrefix = activity.getString(R.string.cplus_me);
+ } else if (conversation.getMode() == Conversation.MODE_MULTI) {
+ msgPrefix = UIHelper.getMessageDisplayName(message);
+ }
+ String lastMessagePreview = ((null == msgPrefix || msgPrefix.isEmpty()) ? "" : (msgPrefix + ": ")) + msgText;
+ mLastMessage.setText(lastMessagePreview);
+ int lastMessageTypeface = Typeface.NORMAL;
+ if (conversation.isRead()) {
+ if (preview.second) {
+ lastMessageTypeface = Typeface.ITALIC;
+ }
+ } else {
+ if (preview.second) {
+ lastMessageTypeface = Typeface.BOLD_ITALIC;
+ } else {
+ lastMessageTypeface = Typeface.BOLD;
+ }
+ }
+ mLastMessage.setTypeface(null, lastMessageTypeface);
+ }
+
+ long muted_till = conversation.getLongAttribute(Conversation.ATTRIBUTE_MUTED_TILL,0);
+ if (muted_till == Long.MAX_VALUE) {
+ notificationStatus.setVisibility(View.VISIBLE);
+ notificationStatus.setImageResource(R.drawable.ic_notifications_off_grey600_24dp);
+ } else if (muted_till >= System.currentTimeMillis()) {
+ notificationStatus.setVisibility(View.VISIBLE);
+ notificationStatus.setImageResource(R.drawable.ic_notifications_paused_grey600_24dp);
+ } else if (conversation.alwaysNotify()) {
+ notificationStatus.setImageResource(R.drawable.ic_notifications_grey600_24dp);
+ if (conversation.getMode() == Conversation.MODE_SINGLE) {
+ notificationStatus.setVisibility(View.GONE);
+ } else {
+ notificationStatus.setVisibility(View.VISIBLE);
+ }
+ } else {
+ notificationStatus.setVisibility(View.VISIBLE);
+ notificationStatus.setImageResource(R.drawable.ic_notifications_none_grey600_24dp);
+ }
+
+ mTimestamp.setText(UIHelper.readableTimeDifference(activity, message.getTimeSent()));
+ ImageView profilePicture = (ImageView) view.findViewById(R.id.conversation_image);
+ if (conversation.getMode() == Conversation.MODE_SINGLE) {
+ profilePicture.setOnLongClickListener(new ShowResourcesListDialogListener(activity, conversation.getContact()));
+ }
+ loadAvatar(conversation, profilePicture);
+
+ return view;
+ }
+
+ class BitmapWorkerTask extends AsyncTask<Conversation, Void, Bitmap> {
+ private final WeakReference<ImageView> imageViewReference;
+ private Conversation conversation = null;
+
+ public BitmapWorkerTask(ImageView imageView) {
+ imageViewReference = new WeakReference<>(imageView);
+ }
+
+ @Override
+ protected Bitmap doInBackground(Conversation... params) {
+ return AvatarService.getInstance().get(params[0], activity.getPixel(56));
+ }
+
+ @Override
+ protected void onPostExecute(Bitmap bitmap) {
+ if (bitmap != null) {
+ final ImageView imageView = imageViewReference.get();
+ if (imageView != null) {
+ imageView.setImageBitmap(bitmap);
+ imageView.setBackgroundColor(0x00000000);
+ }
+ }
+ }
+ }
+
+ public void loadAvatar(Conversation conversation, ImageView imageView) {
+ if (cancelPotentialWork(conversation, imageView)) {
+ final Bitmap bm = AvatarService.getInstance().get(conversation, activity.getPixel(56), true);
+ if (bm != null) {
+ imageView.setImageBitmap(bm);
+ imageView.setBackgroundColor(0x00000000);
+ } else {
+ imageView.setBackgroundColor(UIHelper.getColorForName(conversation.getName()));
+ imageView.setImageDrawable(null);
+ final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
+ final AsyncDrawable asyncDrawable = new AsyncDrawable(activity.getResources(), null, task);
+ imageView.setImageDrawable(asyncDrawable);
+ try {
+ task.execute(conversation);
+ } catch (final RejectedExecutionException ignored) {
+ }
+ }
+ }
+ }
+
+ public static boolean cancelPotentialWork(Conversation conversation, ImageView imageView) {
+ final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
+
+ if (bitmapWorkerTask != null) {
+ final Conversation oldConversation = bitmapWorkerTask.conversation;
+ if (oldConversation == null || conversation != oldConversation) {
+ bitmapWorkerTask.cancel(true);
+ } else {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
+ if (imageView != null) {
+ final Drawable drawable = imageView.getDrawable();
+ if (drawable instanceof AsyncDrawable) {
+ final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
+ return asyncDrawable.getBitmapWorkerTask();
+ }
+ }
+ return null;
+ }
+
+ static class AsyncDrawable extends BitmapDrawable {
+ private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
+
+ public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) {
+ super(res, bitmap);
+ bitmapWorkerTaskReference = new WeakReference<>(bitmapWorkerTask);
+ }
+
+ public BitmapWorkerTask getBitmapWorkerTask() {
+ return bitmapWorkerTaskReference.get();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java
new file mode 100644
index 00000000..39bfc082
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java
@@ -0,0 +1,70 @@
+package eu.siacs.conversations.ui.adapter;
+
+import android.content.Context;
+import android.widget.ArrayAdapter;
+import android.widget.Filter;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+public class KnownHostsAdapter extends ArrayAdapter<String> {
+ private ArrayList<String> domains;
+ private Filter domainFilter = new Filter() {
+
+ @Override
+ protected FilterResults performFiltering(CharSequence constraint) {
+ if (constraint != null) {
+ ArrayList<String> suggestions = new ArrayList<>();
+ final String[] split = constraint.toString().split("@");
+ if (split.length == 1) {
+ for (String domain : domains) {
+ suggestions.add(split[0].toLowerCase(Locale
+ .getDefault()) + "@" + domain);
+ }
+ } else if (split.length == 2) {
+ for (String domain : domains) {
+ if (domain.contentEquals(split[1])) {
+ suggestions.clear();
+ break;
+ } else if (domain.contains(split[1])) {
+ suggestions.add(split[0].toLowerCase(Locale
+ .getDefault()) + "@" + domain);
+ }
+ }
+ } else {
+ return new FilterResults();
+ }
+ FilterResults filterResults = new FilterResults();
+ filterResults.values = suggestions;
+ filterResults.count = suggestions.size();
+ return filterResults;
+ } else {
+ return new FilterResults();
+ }
+ }
+
+ @Override
+ protected void publishResults(CharSequence constraint,
+ FilterResults results) {
+ ArrayList filteredList = (ArrayList) results.values;
+ if (results != null && results.count > 0) {
+ clear();
+ for (Object c : filteredList) {
+ add((String) c);
+ }
+ notifyDataSetChanged();
+ }
+ }
+ };
+
+ public KnownHostsAdapter(Context context, int viewResourceId, List<String> mKnownHosts) {
+ super(context, viewResourceId, new ArrayList<String>());
+ domains = new ArrayList<>(mKnownHosts);
+ }
+
+ @Override
+ public Filter getFilter() {
+ return domainFilter;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java
new file mode 100644
index 00000000..29d706c7
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java
@@ -0,0 +1,188 @@
+package eu.siacs.conversations.ui.adapter;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import java.lang.ref.WeakReference;
+import java.util.List;
+import java.util.concurrent.RejectedExecutionException;
+
+import de.thedevstack.conversationsplus.ConversationsPlusPreferences;
+import de.tzur.conversations.Settings;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.ListItem;
+import eu.siacs.conversations.services.AvatarService;
+import eu.siacs.conversations.ui.XmppActivity;
+import eu.siacs.conversations.utils.UIHelper;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+public class ListItemAdapter extends ArrayAdapter<ListItem> {
+
+ protected XmppActivity activity;
+ protected boolean showDynamicTags = false;
+ private View.OnClickListener onTagTvClick = new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if (view instanceof TextView && mOnTagClickedListener != null) {
+ TextView tv = (TextView) view;
+ final String tag = tv.getText().toString();
+ mOnTagClickedListener.onTagClicked(tag);
+ }
+ }
+ };
+ private OnTagClickedListener mOnTagClickedListener = null;
+
+ public ListItemAdapter(XmppActivity activity, List<ListItem> objects) {
+ super(activity, 0, objects);
+ this.activity = activity;
+ this.showDynamicTags = ConversationsPlusPreferences.showDynamicTags();
+ }
+
+ @Override
+ public View getView(int position, View view, ViewGroup parent) {
+ LayoutInflater inflater = (LayoutInflater) getContext()
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ ListItem item = getItem(position);
+ if (view == null) {
+ view = inflater.inflate(R.layout.contact, parent, false);
+ }
+
+ if (Settings.SHOW_ONLINE_STATUS) {
+ TextView tvStatus = (TextView) view.findViewById(R.id.contact_status);
+ tvStatus.setBackgroundColor(item.getStatusColor());
+ }
+
+ TextView tvName = (TextView) view.findViewById(R.id.contact_display_name);
+ TextView tvJid = (TextView) view.findViewById(R.id.contact_jid);
+ ImageView picture = (ImageView) view.findViewById(R.id.contact_photo);
+ LinearLayout tagLayout = (LinearLayout) view.findViewById(R.id.tags);
+
+ List<ListItem.Tag> tags = item.getTags();
+ if (tags.size() == 0 || !this.showDynamicTags) {
+ tagLayout.setVisibility(View.GONE);
+ } else {
+ tagLayout.setVisibility(View.VISIBLE);
+ tagLayout.removeAllViewsInLayout();
+ for(ListItem.Tag tag : tags) {
+ TextView tv = (TextView) inflater.inflate(R.layout.list_item_tag,tagLayout,false);
+ tv.setText(tag.getName());
+ tv.setBackgroundColor(tag.getColor());
+ tv.setOnClickListener(this.onTagTvClick);
+ tagLayout.addView(tv);
+ }
+ }
+ final String jid = item.getDisplayJid();
+ if (jid != null) {
+ tvJid.setVisibility(View.VISIBLE);
+ tvJid.setText(jid);
+ } else {
+ tvJid.setVisibility(View.GONE);
+ }
+ tvName.setText(item.getDisplayName());
+ loadAvatar(item,picture);
+ return view;
+ }
+
+ public void setOnTagClickedListener(OnTagClickedListener listener) {
+ this.mOnTagClickedListener = listener;
+ }
+
+ public interface OnTagClickedListener {
+ void onTagClicked(String tag);
+ }
+
+ class BitmapWorkerTask extends AsyncTask<ListItem, Void, Bitmap> {
+ private final WeakReference<ImageView> imageViewReference;
+ private ListItem item = null;
+
+ public BitmapWorkerTask(ImageView imageView) {
+ imageViewReference = new WeakReference<>(imageView);
+ }
+
+ @Override
+ protected Bitmap doInBackground(ListItem... params) {
+ return AvatarService.getInstance().get(params[0], activity.getPixel(48));
+ }
+
+ @Override
+ protected void onPostExecute(Bitmap bitmap) {
+ if (bitmap != null) {
+ final ImageView imageView = imageViewReference.get();
+ if (imageView != null) {
+ imageView.setImageBitmap(bitmap);
+ imageView.setBackgroundColor(0x00000000);
+ }
+ }
+ }
+ }
+
+ public void loadAvatar(ListItem item, ImageView imageView) {
+ if (cancelPotentialWork(item, imageView)) {
+ final Bitmap bm = AvatarService.getInstance().get(item,activity.getPixel(48),true);
+ if (bm != null) {
+ imageView.setImageBitmap(bm);
+ imageView.setBackgroundColor(0x00000000);
+ } else {
+ imageView.setBackgroundColor(UIHelper.getColorForName(item.getDisplayName()));
+ imageView.setImageDrawable(null);
+ final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
+ final AsyncDrawable asyncDrawable = new AsyncDrawable(activity.getResources(), null, task);
+ imageView.setImageDrawable(asyncDrawable);
+ try {
+ task.execute(item);
+ } catch (final RejectedExecutionException ignored) {
+ }
+ }
+ }
+ }
+
+ public static boolean cancelPotentialWork(ListItem item, ImageView imageView) {
+ final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
+
+ if (bitmapWorkerTask != null) {
+ final ListItem oldItem = bitmapWorkerTask.item;
+ if (oldItem == null || item != oldItem) {
+ bitmapWorkerTask.cancel(true);
+ } else {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
+ if (imageView != null) {
+ final Drawable drawable = imageView.getDrawable();
+ if (drawable instanceof AsyncDrawable) {
+ final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
+ return asyncDrawable.getBitmapWorkerTask();
+ }
+ }
+ return null;
+ }
+
+ static class AsyncDrawable extends BitmapDrawable {
+ private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
+
+ public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) {
+ super(res, bitmap);
+ bitmapWorkerTaskReference = new WeakReference<>(bitmapWorkerTask);
+ }
+
+ public BitmapWorkerTask getBitmapWorkerTask() {
+ return bitmapWorkerTaskReference.get();
+ }
+ }
+
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java
new file mode 100644
index 00000000..0e0e086f
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java
@@ -0,0 +1,881 @@
+package eu.siacs.conversations.ui.adapter;
+
+import android.content.ActivityNotFoundException;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Typeface;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.RelativeSizeSpan;
+import android.text.style.StyleSpan;
+import android.text.util.Linkify;
+import android.util.Patterns;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnLongClickListener;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import java.lang.ref.WeakReference;
+import java.util.List;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import de.thedevstack.conversationsplus.ConversationsPlusApplication;
+import de.thedevstack.conversationsplus.ConversationsPlusColors;
+import de.thedevstack.conversationsplus.ConversationsPlusPreferences;
+import de.thedevstack.conversationsplus.entities.FileParams;
+import de.thedevstack.conversationsplus.enums.FileStatus;
+import de.thedevstack.conversationsplus.utils.MessageUtil;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.Transferable;
+import eu.siacs.conversations.http.HttpConnectionManager;
+import eu.siacs.conversations.persistance.FileBackend;
+import eu.siacs.conversations.providers.ConversationsPlusFileProvider;
+import eu.siacs.conversations.services.AvatarService;
+import eu.siacs.conversations.ui.ConversationActivity;
+import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.utils.GeoHelper;
+import eu.siacs.conversations.utils.UIHelper;
+
+public class MessageAdapter extends ArrayAdapter<Message> {
+
+ private static final int SENT = 0;
+ private static final int RECEIVED = 1;
+ private static final int STATUS = 2;
+ private static final int NULL = 3;
+ private static final Pattern XMPP_PATTERN = Pattern
+ .compile("xmpp\\:(?:(?:["
+ + Patterns.GOOD_IRI_CHAR
+ + "\\;\\/\\?\\@\\&\\=\\#\\~\\-\\.\\+\\!\\*\\'\\(\\)\\,\\_])"
+ + "|(?:\\%[a-fA-F0-9]{2}))+");
+
+ private ConversationActivity activity;
+
+ private OnContactPictureClicked mOnContactPictureClickedListener;
+ private OnContactPictureLongClicked mOnContactPictureLongClickedListener;
+
+ public MessageAdapter(ConversationActivity activity, List<Message> messages) {
+ super(activity, 0, messages);
+ this.activity = activity;
+ }
+
+ public void setOnContactPictureClicked(OnContactPictureClicked listener) {
+ this.mOnContactPictureClickedListener = listener;
+ }
+
+ public void setOnContactPictureLongClicked(
+ OnContactPictureLongClicked listener) {
+ this.mOnContactPictureLongClickedListener = listener;
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return 3;
+ }
+
+ public int getItemViewType(Message message) {
+ if (message.getType() == Message.TYPE_STATUS) {
+ return STATUS;
+ } else if (message.getStatus() <= Message.STATUS_RECEIVED) {
+ return RECEIVED;
+ }
+
+ return SENT;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return this.getItemViewType(getItem(position));
+ }
+
+ private int getMessageTextColor(boolean onDark, boolean primary) {
+ if (onDark) {
+ return primary ? ConversationsPlusColors.primaryTextOnDark() : ConversationsPlusColors.secondaryTextOnDark();
+ } else {
+ return primary ? ConversationsPlusColors.primaryText() : ConversationsPlusColors.secondaryText();
+ }
+ }
+
+ private void displayStatus(ViewHolder viewHolder, Message message, int type, boolean darkBackground) {
+ String filesize = null;
+ String info = null;
+ boolean error = false;
+ if (viewHolder.indicatorReceived != null) {
+ viewHolder.indicatorReceived.setVisibility(View.GONE);
+ }
+
+ boolean multiReceived = message.getConversation().getMode() == Conversation.MODE_MULTI
+ && message.getStatus() <= Message.STATUS_RECEIVED;
+ if (message.hasFileAttached() || message.getTransferable() != null) {
+ FileParams params = message.getFileParams();
+ if (null != params) {
+ filesize = UIHelper.getHumanReadableFileSize(params.getSize());
+ }
+
+ if (message.getTransferable() != null && message.getTransferable().getStatus() == Transferable.STATUS_FAILED) {
+ error = true;
+ }
+ }
+ switch (message.getStatus()) {
+ case Message.STATUS_WAITING:
+ info = getContext().getString(R.string.waiting);
+ break;
+ case Message.STATUS_UNSEND:
+ Transferable d = message.getTransferable();
+ if (d!=null) {
+ info = getContext().getString(R.string.sending_file,d.getProgress());
+ } else {
+ info = getContext().getString(R.string.sending);
+ }
+ break;
+ case Message.STATUS_OFFERED:
+ info = getContext().getString(R.string.offering);
+ break;
+ case Message.STATUS_SEND_RECEIVED:
+ if (ConversationsPlusPreferences.indicateReceived()) {
+ viewHolder.indicatorReceived.setVisibility(View.VISIBLE);
+ }
+ break;
+ case Message.STATUS_SEND_DISPLAYED:
+ if (ConversationsPlusPreferences.indicateReceived()) {
+ viewHolder.indicatorReceived.setVisibility(View.VISIBLE);
+ }
+ break;
+ case Message.STATUS_SEND_FAILED:
+ info = getContext().getString(R.string.send_failed);
+ error = true;
+ break;
+ default:
+ if (multiReceived) {
+ info = UIHelper.getMessageDisplayName(message);
+ }
+ break;
+ }
+ if (error && type == SENT) {
+ viewHolder.time.setTextColor(ConversationsPlusColors.warning());
+ } else {
+ viewHolder.time.setTextColor(this.getMessageTextColor(darkBackground,false));
+ }
+ if (message.getEncryption() == Message.ENCRYPTION_NONE) {
+ viewHolder.indicator.setVisibility(View.GONE);
+ } else {
+ 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) {
+ XmppAxolotlSession.Trust trust = message.getConversation()
+ .getAccount().getAxolotlService().getFingerprintTrust(
+ message.getFingerprint());
+
+ if(trust == null || (!trust.trusted() && !trust.trustedInactive())) {
+ viewHolder.indicator.setColorFilter(ConversationsPlusColors.warning());
+ 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);
+ }
+ }
+ }
+
+ String formatedTime = UIHelper.readableTimeDifferenceFull(getContext(),
+ message.getTimeSent());
+ if (message.getStatus() <= Message.STATUS_RECEIVED) {
+ StringBuilder timeText = new StringBuilder();
+ timeText.append((null != formatedTime) ? formatedTime + ((null != info || null != filesize) ? " \u00B7 " : "") : "");
+ timeText.append((null != filesize) ? filesize + ((null != info) ? " \u00B7 " : "") : "");
+ timeText.append((null != info) ? info : "");
+ viewHolder.time.setText(timeText);
+ } else {
+ if ((filesize != null) && (info != null)) {
+ viewHolder.time.setText(filesize + " \u00B7 " + info);
+ } else if ((filesize == null) && (info != null)) {
+ if (error) {
+ viewHolder.time.setText(info + " \u00B7 " + formatedTime);
+ } else {
+ viewHolder.time.setText(info);
+ }
+ } else if ((filesize != null) && (info == null)) {
+ viewHolder.time.setText(filesize + " \u00B7 " + formatedTime);
+ } else {
+ viewHolder.time.setText(formatedTime);
+ }
+ }
+
+ if (message.hasFileAttached() && null != message.getFileParams() && null != viewHolder.remoteFileStatus) {
+ FileStatus fileStatus = message.getFileParams().getFileStatus();
+ if (fileStatus == FileStatus.DELETE_FAILED || fileStatus == FileStatus.DELETED || fileStatus == FileStatus.DELETING) {
+ viewHolder.remoteFileStatus.setVisibility(View.VISIBLE);
+ viewHolder.remoteFileStatus.setTypeface(null, Typeface.ITALIC);
+ switch (fileStatus) {
+ case DELETE_FAILED:
+ viewHolder.remoteFileStatus.setText(R.string.remote_filestatus_delete_failed);
+ break;
+ case DELETED:
+ viewHolder.remoteFileStatus.setText(R.string.remote_filestatus_delete_success);
+ break;
+ case DELETING:
+ viewHolder.remoteFileStatus.setText(R.string.remote_filestatus_delete_inprogress);
+ break;
+ }
+ }
+ }
+ }
+
+ private void displayInfoMessage(ViewHolder viewHolder, String text, boolean darkBackground) {
+ if (viewHolder.download_button != null) {
+ viewHolder.download_button.setVisibility(View.GONE);
+ }
+ viewHolder.image.setVisibility(View.GONE);
+ viewHolder.messageBody.setVisibility(View.VISIBLE);
+ viewHolder.messageBody.setText(text);
+ viewHolder.messageBody.setTextColor(getMessageTextColor(darkBackground, false));
+ viewHolder.messageBody.setTypeface(null, Typeface.ITALIC);
+ viewHolder.messageBody.setTextIsSelectable(false);
+ }
+
+ private void displayDecryptionFailed(ViewHolder viewHolder, boolean darkBackground) {
+ if (viewHolder.download_button != null) {
+ viewHolder.download_button.setVisibility(View.GONE);
+ }
+ viewHolder.image.setVisibility(View.GONE);
+ viewHolder.messageBody.setVisibility(View.VISIBLE);
+ viewHolder.messageBody.setText(getContext().getString(
+ R.string.decryption_failed));
+ viewHolder.messageBody.setTextColor(getMessageTextColor(darkBackground, false));
+ viewHolder.messageBody.setTypeface(null, Typeface.NORMAL);
+ viewHolder.messageBody.setTextIsSelectable(false);
+ }
+
+ private void displayTextMessage(final ViewHolder viewHolder, final Message message, boolean darkBackground) {
+ if (viewHolder.download_button != null) {
+ viewHolder.download_button.setVisibility(View.GONE);
+ }
+ viewHolder.image.setVisibility(View.GONE);
+ viewHolder.messageBody.setVisibility(View.VISIBLE);
+ viewHolder.messageBody.setIncludeFontPadding(true);
+ if (message.getBody() != null) {
+ final String nick = UIHelper.getMessageDisplayName(message);
+ String body;
+ if (message.hasMeCommand()) {
+ body = message.getBodyReplacedMeCommand(nick);
+ } else {
+ body = message.getBody();
+ }
+ final SpannableString formattedBody = new SpannableString(body);
+ int i = body.indexOf(Message.MERGE_SEPARATOR);
+ while(i >= 0) {
+ final int end = i + Message.MERGE_SEPARATOR.length();
+ formattedBody.setSpan(new RelativeSizeSpan(0.3f),i,end,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ i = body.indexOf(Message.MERGE_SEPARATOR,end);
+ }
+ if (message.getType() != Message.TYPE_PRIVATE) {
+ if (message.hasMeCommand()) {
+ final Spannable span = new SpannableString(formattedBody);
+ span.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), 0, nick.length(),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ viewHolder.messageBody.setText(span);
+ } else {
+ viewHolder.messageBody.setText(formattedBody);
+ }
+ } else {
+ String privateMarker;
+ if (message.getStatus() <= Message.STATUS_RECEIVED) {
+ privateMarker = activity
+ .getString(R.string.private_message);
+ } else {
+ final String to;
+ if (message.getCounterpart() != null) {
+ to = message.getCounterpart().getResourcepart();
+ } else {
+ to = "";
+ }
+ privateMarker = activity.getString(R.string.private_message_to, to);
+ }
+ final Spannable span = new SpannableString(privateMarker + " "
+ + formattedBody);
+ span.setSpan(new ForegroundColorSpan(getMessageTextColor(darkBackground,false)), 0, privateMarker
+ .length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ span.setSpan(new StyleSpan(Typeface.BOLD), 0,
+ privateMarker.length(),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ if (message.hasMeCommand()) {
+ span.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), privateMarker.length() + 1,
+ privateMarker.length() + 1 + nick.length(),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ viewHolder.messageBody.setText(span);
+ }
+ int patternMatchCount = 0;
+ int oldAutoLinkMask = viewHolder.messageBody.getAutoLinkMask();
+
+ // first check if we have a match on XMPP_PATTERN so we do not have to check for EMAIL_ADDRESSES
+ patternMatchCount += countMatches(XMPP_PATTERN, body);
+ if ((Linkify.EMAIL_ADDRESSES & oldAutoLinkMask) != 0 && patternMatchCount > 0) {
+ oldAutoLinkMask -= Linkify.EMAIL_ADDRESSES;
+ }
+
+ // count matches for all patterns
+ if ((Linkify.WEB_URLS & oldAutoLinkMask) != 0) {
+ patternMatchCount += countMatches(Patterns.WEB_URL, body);
+ }
+ if ((Linkify.EMAIL_ADDRESSES & oldAutoLinkMask) != 0) {
+ patternMatchCount += countMatches(Patterns.EMAIL_ADDRESS, body);
+ }
+ if ((Linkify.PHONE_NUMBERS & oldAutoLinkMask) != 0) {
+ patternMatchCount += countMatches(Patterns.PHONE, body);
+ }
+
+ viewHolder.messageBody.setTextIsSelectable(patternMatchCount <= 1);
+ viewHolder.messageBody.setAutoLinkMask(0);
+ Linkify.addLinks(viewHolder.messageBody, XMPP_PATTERN, "xmpp");
+ viewHolder.messageBody.setAutoLinkMask(oldAutoLinkMask);
+ } else {
+ viewHolder.messageBody.setText("");
+ viewHolder.messageBody.setTextIsSelectable(false);
+ }
+ viewHolder.messageBody.setTextColor(this.getMessageTextColor(darkBackground, true));
+ viewHolder.messageBody.setTypeface(null, Typeface.NORMAL);
+ }
+
+ /**
+ * Counts the number of occurrences of the pattern in body.
+ * @param pattern the pattern to match
+ * @param body the body to find the pattern
+ * @return the number of occurrences
+ */
+ private int countMatches(Pattern pattern, String body) {
+ Matcher matcher = pattern.matcher(body);
+ int count = 0;
+ while (matcher.find()) {
+ count++;
+ }
+ return count;
+ }
+
+ private void displayDownloadButton(ViewHolder viewHolder, String btnText, OnClickListener onClickListener) {
+ viewHolder.download_button.setVisibility(View.VISIBLE);
+ viewHolder.download_button.setText(btnText);
+ viewHolder.download_button.setOnClickListener(onClickListener);
+ viewHolder.download_button.setOnLongClickListener(openContextMenu);
+ }
+
+ private void displayFileInfoForFileMessage(final Message message, ViewHolder viewHolder) {
+ viewHolder.messageBody.setVisibility(View.VISIBLE);
+ StringBuilder fileInfos = new StringBuilder();
+ String filename = UIHelper.getDisplayFilename(message);
+ fileInfos.append((null != filename && !filename.isEmpty()) ? (filename) : "");
+
+ int oldAutoLinkMask = viewHolder.messageBody.getAutoLinkMask();
+ viewHolder.messageBody.setAutoLinkMask(0);
+ viewHolder.messageBody.setText(fileInfos);
+ viewHolder.messageBody.setAutoLinkMask(oldAutoLinkMask);
+ }
+
+ private void displayDownloadableMessage(ViewHolder viewHolder, final Message message, int resId) {
+ viewHolder.image.setVisibility(View.GONE);
+ FileParams fileParams = message.getFileParams();
+ String btnText;
+ if (null != fileParams) {
+ this.displayFileInfoForFileMessage(message, viewHolder);
+ btnText = activity.getString(resId, "");
+ } else {
+ viewHolder.messageBody.setVisibility(View.GONE);
+ btnText = activity.getString(resId, UIHelper.getFileDescriptionString(activity, message));
+ }
+
+ this.displayDownloadButton(viewHolder, btnText, new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ activity.startDownloadable(message);
+ }
+ });
+ }
+
+ private void displayOpenableMessage(ViewHolder viewHolder,final Message message) {
+ viewHolder.image.setVisibility(View.GONE);
+
+ FileParams fileParams = message.getFileParams();
+ String btnText;
+ if (null != fileParams) {
+ this.displayFileInfoForFileMessage(message, viewHolder);
+ btnText = activity.getString(R.string.cplus_open);
+ } else {
+ viewHolder.messageBody.setVisibility(View.GONE);
+ btnText = activity.getString(R.string.open_x_file, UIHelper.getFileDescriptionString(activity, message));
+ }
+
+ this.displayDownloadButton(viewHolder, btnText, new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ openDownloadable(message);
+ }
+ });
+ }
+
+ private void displayLocationMessage(ViewHolder viewHolder, final Message message) {
+ viewHolder.image.setVisibility(View.GONE);
+ viewHolder.messageBody.setVisibility(View.GONE);
+ viewHolder.download_button.setVisibility(View.VISIBLE);
+ viewHolder.download_button.setText(R.string.show_location);
+ viewHolder.download_button.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ showLocation(message);
+ }
+ });
+ }
+
+ private void displayImageMessage(ViewHolder viewHolder,
+ final Message message) {
+ if (viewHolder.download_button != null) {
+ viewHolder.download_button.setVisibility(View.GONE);
+ }
+ viewHolder.messageBody.setVisibility(View.GONE);
+ viewHolder.image.setVisibility(View.VISIBLE);
+ //TODO: Check what value add the following lines have (compared with setting height/width in XmppActivity.loadBitmap from thumbnail after thumbnail is created)
+ /*
+ FileParams params = message.getFileParams();
+ double target = metrics.density * 288;
+ int scalledW;
+ int scalledH;
+ if (params.width <= params.height) {
+ scalledW = (int) (params.width / ((double) params.height / target));
+ scalledH = (int) target;
+ } else {
+ scalledW = (int) target;
+ scalledH = (int) (params.height / ((double) params.width / target));
+ }
+ LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(scalledW, scalledH);
+ layoutParams.setMargins(0, (int) (metrics.density * 4), 0, (int) (metrics.density * 4));
+ viewHolder.image.setLayoutParams(layoutParams);*/
+ //TODO Why should this be calculated by hand???
+ activity.loadBitmap(message, viewHolder.image, true);
+ viewHolder.image.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ openDownloadable(message);
+ }
+ });
+ }
+
+ private View displayStatusMessage(final Message message, ViewHolder viewHolder) {
+ if (null != viewHolder) {
+ final Conversation conversation = message.getConversation();
+
+ viewHolder.status_message.setVisibility(View.VISIBLE);
+ viewHolder.contact_picture.setVisibility(View.VISIBLE);
+ if (conversation.getMode() == Conversation.MODE_SINGLE) {
+ viewHolder.contact_picture.setImageBitmap(AvatarService.getInstance().get(conversation.getContact(),
+ activity.getPixel(32)));
+ viewHolder.contact_picture.setAlpha(0.5f);
+ }
+ viewHolder.status_message.setText(message.getBody());
+ }
+ return viewHolder.view;
+ }
+
+ private void displayFileMessage(final Message message, ViewHolder viewHolder) {
+ final FileParams fileParams = message.getFileParams();
+ String mimeType = (null != fileParams) ? fileParams.getMimeType() : null;
+ if ((message.getType() == Message.TYPE_IMAGE && message.getEncryption() != Message.ENCRYPTION_PGP && message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED)
+ || (fileParams != null && fileParams.getWidth() > 0)
+ || (null != mimeType && mimeType.startsWith("image/"))) {
+ displayImageMessage(viewHolder, message);
+ } else if ((message.getType() == Message.TYPE_FILE && message.getEncryption() != Message.ENCRYPTION_PGP && message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED)
+ && null != fileParams && (FileStatus.NEEDS_DOWNLOAD != fileParams.getFileStatus() || 0 == fileParams.getWidth())) {
+ displayOpenableMessage(viewHolder, message);
+ } else if (Message.Decision.NEVER == message.treatAsDownloadable()) {
+ displayTextMessage(viewHolder, message, getItemViewType(message) == RECEIVED && !message.isValidInSession());
+ } else {
+ displayDownloadableMessage(viewHolder, message, R.string.download_x_file);
+ }
+ }
+
+
+ private void loadAvatar(Message message, ImageView imageView) {
+ if (cancelPotentialWork(message, imageView)) {
+ int avatarPixel = imageView.getResources().getDimensionPixelOffset(R.dimen.msg_avatar_size);
+ final Bitmap bm = AvatarService.getInstance().get(message, avatarPixel, true);
+ if (bm != null) {
+ imageView.setImageBitmap(bm);
+ imageView.setBackgroundColor(0x00000000);
+ } else {
+ imageView.setBackgroundColor(UIHelper.getColorForName(UIHelper.getMessageDisplayName(message)));
+ imageView.setImageDrawable(null);
+ final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
+ final AsyncDrawable asyncDrawable = new AsyncDrawable(activity.getResources(), null, task);
+ imageView.setImageDrawable(asyncDrawable);
+ try {
+ task.execute(message);
+ } catch (final RejectedExecutionException ignored) {
+ }
+ }
+ }
+ }
+
+ private void displayAvatar(final Message message, int type, ViewHolder viewHolder) {
+ if (type == RECEIVED) {
+ ImageView imageView = viewHolder.contact_picture;
+ if (null != imageView) {
+ this.loadAvatar(message, imageView);
+ imageView.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (MessageAdapter.this.mOnContactPictureClickedListener != null) {
+ MessageAdapter.this.mOnContactPictureClickedListener
+ .onContactPictureClicked(message);
+ }
+
+ }
+ });
+ if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
+ imageView.setOnLongClickListener(new OnLongClickListener() {
+
+ @Override
+ public boolean onLongClick(View v) {
+ if (MessageAdapter.this.mOnContactPictureLongClickedListener != null) {
+ MessageAdapter.this.mOnContactPictureLongClickedListener
+ .onContactPictureLongClicked(message);
+ return true;
+ } else {
+ return false;
+ }
+ }
+ });
+ }
+ }
+ }
+ }
+
+ private View initializeView(int type, ViewGroup parent) {
+ Integer viewResId = null;
+
+ switch (type) {
+ case SENT:
+ viewResId = R.layout.message_sent;
+ break;
+ case RECEIVED:
+ viewResId = R.layout.message_received;
+ break;
+ case STATUS:
+ viewResId = R.layout.message_status;
+ break;
+ }
+
+ return activity.getLayoutInflater().inflate(viewResId, parent, false);
+ }
+
+ private ViewHolder initializeViewHolderAndView(Message message, int type, ViewGroup parent) {
+ View view = initializeView(type, parent);
+ ViewHolder viewHolder = new ViewHolder(view);
+ if (SENT == type
+ || RECEIVED == type) {
+ viewHolder.message_box = (LinearLayout) view.findViewById(R.id.message_box);
+ viewHolder.message_box.setVisibility(View.VISIBLE);
+ viewHolder.indicator = (ImageView) view.findViewById(R.id.security_indicator);
+ viewHolder.messageBody = (TextView) view.findViewById(R.id.message_body);
+ viewHolder.time = (TextView) view.findViewById(R.id.message_time);
+ viewHolder.indicatorReceived = (ImageView) view.findViewById(R.id.indicator_received);
+ }
+ if ((SENT == type
+ || RECEIVED == type)) {
+ viewHolder.download_button = (Button) view.findViewById(R.id.download_button);
+ viewHolder.image = (ImageView) view.findViewById(R.id.message_image);
+ }
+ if (RECEIVED == type) { // Extra block as preparation for new /me representation
+ viewHolder.contact_picture = (ImageView) view.findViewById(R.id.message_photo);
+ viewHolder.contact_picture.setVisibility(View.VISIBLE);
+ }
+ if (RECEIVED == type) {
+ viewHolder.encryption = (TextView) view.findViewById(R.id.message_encryption);
+ }
+ if (STATUS == type) {
+ viewHolder.contact_picture = (ImageView) view.findViewById(R.id.message_photo);
+ viewHolder.contact_picture.setVisibility(View.VISIBLE);
+ viewHolder.status_message = (TextView) view.findViewById(R.id.status_message);
+ viewHolder.status_message.setVisibility(View.VISIBLE);
+ }
+ view.setTag(viewHolder);
+
+ return viewHolder;
+ }
+
+ @Override
+ public View getView(int position, View view, ViewGroup parent) {
+ final Message message = getItem(position);
+ final boolean isInValidSession = message.isValidInSession();
+ final Conversation conversation = message.getConversation();
+ final Account account = conversation.getAccount();
+ final int type = getItemViewType(position);
+ ViewHolder viewHolder;
+ if (null == view) {
+ viewHolder = initializeViewHolderAndView(message, type, parent);
+ view = viewHolder.view;
+ } else {
+ viewHolder = (ViewHolder) view.getTag();
+ if (null == viewHolder) {
+ return view;
+ }
+ }
+
+ if (type == STATUS) {
+ return displayStatusMessage(message, viewHolder);
+ }
+
+ this.displayAvatar(message, type, viewHolder);
+
+ boolean darkBackground = (type == RECEIVED && !isInValidSession);
+ this.displayStatus(viewHolder, message, type, darkBackground);
+
+ final Transferable transferable = message.getTransferable();
+ if (null != transferable) {
+ switch (transferable.getStatus()) {
+ case Transferable.STATUS_OFFER:
+ displayDownloadableMessage(viewHolder, message, R.string.download_x_file);
+ break;
+ case Transferable.STATUS_OFFER_CHECK_FILESIZE:
+ displayDownloadableMessage(viewHolder, message, R.string.check_x_filesize);
+ break;
+ case Transferable.STATUS_UPLOADING:
+ displayFileMessage(message, viewHolder);
+ break;
+ case Transferable.STATUS_DELETED:
+ case Transferable.STATUS_CHECKING:
+ case Transferable.STATUS_FAILED:
+ case Transferable.STATUS_UNKNOWN:
+ displayInfoMessage(viewHolder, UIHelper.getMessagePreview(activity, message).first,darkBackground);
+ break;
+ }
+ } else if (message.hasFileAttached()) {
+ if (message.trusted()
+ && MessageUtil.needsDownload(message)
+ && ConversationsPlusPreferences.autoAcceptFileSize() > 0
+ && (message.isHttpUploaded() || ConversationsPlusPreferences.autoDownloadFileLink())) {
+ HttpConnectionManager.createNewDownloadConnection(message);
+ } else {
+ displayFileMessage(message, viewHolder);
+ }
+ } else if (GeoHelper.isGeoUri(message.getBody())) {
+ displayLocationMessage(viewHolder, message);
+ } else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
+ if (activity.hasPgp()) {
+ if (account.getPgpDecryptionService().isRunning()) {
+ displayInfoMessage(viewHolder, activity.getString(R.string.message_decrypting), darkBackground);
+ } else {
+ displayInfoMessage(viewHolder, activity.getString(R.string.pgp_message), darkBackground);
+ }
+ } else {
+ displayInfoMessage(viewHolder,activity.getString(R.string.install_openkeychain),darkBackground);
+ if (viewHolder != null) {
+ viewHolder.message_box
+ .setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ activity.showInstallPgpDialog();
+ }
+ });
+ }
+ }
+ } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) {
+ displayDecryptionFailed(viewHolder,darkBackground);
+ } else {
+ displayTextMessage(viewHolder, message, darkBackground);
+ }
+
+ if (type == RECEIVED) {
+ if(isInValidSession) {
+ viewHolder.encryption.setVisibility(View.GONE);
+ } else {
+ viewHolder.message_box.setBackgroundResource(R.drawable.message_bubble_received_warning);
+ viewHolder.encryption.setVisibility(View.VISIBLE);
+ viewHolder.encryption.setText(CryptoHelper.encryptionTypeToText(message.getEncryption()));
+ }
+ }
+
+ return view;
+ }
+
+ public void openDownloadable(Message message) {
+ DownloadableFile file = FileBackend.getFile(message);
+ if (!file.exists()) {
+ Toast.makeText(activity,R.string.file_deleted,Toast.LENGTH_SHORT).show();
+ return;
+ }
+ boolean bInPrivateStorage = false;
+ if (file.getAbsolutePath().startsWith(FileBackend.getPrivateFileDirectoryPath())) {
+ bInPrivateStorage = true;
+ }
+ Intent openIntent = new Intent(Intent.ACTION_VIEW);
+ String mime = file.getMimeType();
+ if (mime == null) {
+ mime = "*/*";
+ }
+ Uri uri;
+ if (bInPrivateStorage) {
+ uri = ConversationsPlusFileProvider.createUriForPrivateFile(file);
+ } else {
+ uri = Uri.fromFile(file);
+ }
+ openIntent.setDataAndType(uri, mime);
+ PackageManager manager = activity.getPackageManager();
+ List<ResolveInfo> infos = manager.queryIntentActivities(openIntent, 0);
+ if (bInPrivateStorage) {
+ for (ResolveInfo info : infos) {
+ ConversationsPlusApplication.getAppContext().grantUriPermission(info.activityInfo.packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ }
+ }
+ if (infos.size() == 0) {
+ openIntent.setDataAndType(uri,"*/*");
+ }
+ if (bInPrivateStorage) {
+ openIntent.putExtra(Intent.EXTRA_STREAM, uri);
+ openIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ }
+ try {
+ getContext().startActivity(openIntent);
+ return;
+ } catch (ActivityNotFoundException e) {
+ //ignored
+ }
+ Toast.makeText(activity,R.string.no_application_found_to_open_file,Toast.LENGTH_SHORT).show();
+ }
+
+ public void showLocation(Message message) {
+ for(Intent intent : GeoHelper.createGeoIntentsFromMessage(message)) {
+ if (intent.resolveActivity(getContext().getPackageManager()) != null) {
+ getContext().startActivity(intent);
+ return;
+ }
+ }
+ Toast.makeText(activity,R.string.no_application_found_to_display_location,Toast.LENGTH_SHORT).show();
+ }
+
+ public interface OnContactPictureClicked {
+ void onContactPictureClicked(Message message);
+ }
+
+ public interface OnContactPictureLongClicked {
+ void onContactPictureLongClicked(Message message);
+ }
+
+ private static class ViewHolder {
+ protected ViewHolder(View view) {
+ this.view = view;
+ }
+ protected View view;
+
+ protected LinearLayout message_box;
+ protected Button download_button;
+ protected ImageView image;
+ protected ImageView indicator;
+ protected ImageView indicatorReceived;
+ protected TextView time;
+ protected TextView messageBody;
+ protected ImageView contact_picture;
+ protected TextView status_message;
+ protected TextView encryption;
+ public TextView remoteFileStatus;
+ }
+
+ class BitmapWorkerTask extends AsyncTask<Message, Void, Bitmap> {
+ private final WeakReference<ImageView> imageViewReference;
+ private Message message = null;
+
+ public BitmapWorkerTask(ImageView imageView) {
+ imageViewReference = new WeakReference<>(imageView);
+ }
+
+ @Override
+ protected Bitmap doInBackground(Message... params) {
+ int avatarPixel = activity.getResources().getDimensionPixelOffset(R.dimen.msg_avatar_size);
+ return AvatarService.getInstance().get(params[0], avatarPixel, isCancelled());
+ }
+
+ @Override
+ protected void onPostExecute(Bitmap bitmap) {
+ if (bitmap != null) {
+ final ImageView imageView = imageViewReference.get();
+ if (imageView != null) {
+ imageView.setImageBitmap(bitmap);
+ imageView.setBackgroundColor(0x00000000);
+ }
+ }
+ }
+ }
+
+ public static boolean cancelPotentialWork(Message message, ImageView imageView) {
+ final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
+
+ if (bitmapWorkerTask != null) {
+ final Message oldMessage = bitmapWorkerTask.message;
+ if (oldMessage == null || message != oldMessage) {
+ bitmapWorkerTask.cancel(true);
+ } else {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
+ if (imageView != null) {
+ final Drawable drawable = imageView.getDrawable();
+ if (drawable instanceof AsyncDrawable) {
+ final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
+ return asyncDrawable.getBitmapWorkerTask();
+ }
+ }
+ return null;
+ }
+
+ static class AsyncDrawable extends BitmapDrawable {
+ private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
+
+ public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) {
+ super(res, bitmap);
+ bitmapWorkerTaskReference = new WeakReference<>(bitmapWorkerTask);
+ }
+
+ public BitmapWorkerTask getBitmapWorkerTask() {
+ return bitmapWorkerTaskReference.get();
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/forms/FormBooleanFieldWrapper.java b/src/main/java/eu/siacs/conversations/ui/forms/FormBooleanFieldWrapper.java
new file mode 100644
index 00000000..6cb357a9
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/forms/FormBooleanFieldWrapper.java
@@ -0,0 +1,80 @@
+package eu.siacs.conversations.ui.forms;
+
+import android.content.Context;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.xmpp.forms.Field;
+
+public class FormBooleanFieldWrapper extends FormFieldWrapper {
+
+ protected CheckBox checkBox;
+
+ protected FormBooleanFieldWrapper(Context context, Field field) {
+ super(context, field);
+ checkBox = (CheckBox) view.findViewById(R.id.field);
+ checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ checkBox.setError(null);
+ invokeOnFormFieldValuesEdited();
+ }
+ });
+ }
+
+ @Override
+ protected void setLabel(String label, boolean required) {
+ CheckBox checkBox = (CheckBox) view.findViewById(R.id.field);
+ checkBox.setText(createSpannableLabelString(label, required));
+ }
+
+ @Override
+ public List<String> getValues() {
+ List<String> values = new ArrayList<>();
+ values.add(Boolean.toString(checkBox.isChecked()));
+ return values;
+ }
+
+ @Override
+ protected void setValues(List<String> values) {
+ if (values.size() == 0) {
+ checkBox.setChecked(false);
+ } else {
+ checkBox.setChecked(Boolean.parseBoolean(values.get(0)));
+ }
+ }
+
+ @Override
+ public boolean validates() {
+ if (checkBox.isChecked() || !field.isRequired()) {
+ return true;
+ } else {
+ checkBox.setError(context.getString(R.string.this_field_is_required));
+ checkBox.requestFocus();
+ return false;
+ }
+ }
+
+ @Override
+ public boolean edited() {
+ if (field.getValues().size() == 0) {
+ return checkBox.isChecked();
+ } else {
+ return super.edited();
+ }
+ }
+
+ @Override
+ protected int getLayoutResource() {
+ return R.layout.form_boolean;
+ }
+
+ @Override
+ void setReadOnly(boolean readOnly) {
+ checkBox.setEnabled(!readOnly);
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/forms/FormFieldFactory.java b/src/main/java/eu/siacs/conversations/ui/forms/FormFieldFactory.java
new file mode 100644
index 00000000..ee306472
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/forms/FormFieldFactory.java
@@ -0,0 +1,30 @@
+package eu.siacs.conversations.ui.forms;
+
+import android.content.Context;
+
+import java.util.Hashtable;
+
+import eu.siacs.conversations.xmpp.forms.Field;
+
+
+
+public class FormFieldFactory {
+
+ private static final Hashtable<String, Class> typeTable = new Hashtable<>();
+
+ static {
+ typeTable.put("text-single", FormTextFieldWrapper.class);
+ typeTable.put("text-multi", FormTextFieldWrapper.class);
+ typeTable.put("text-private", FormTextFieldWrapper.class);
+ typeTable.put("jid-single", FormJidSingleFieldWrapper.class);
+ typeTable.put("boolean", FormBooleanFieldWrapper.class);
+ }
+
+ protected static FormFieldWrapper createFromField(Context context, Field field) {
+ Class clazz = typeTable.get(field.getType());
+ if (clazz == null) {
+ clazz = FormTextFieldWrapper.class;
+ }
+ return FormFieldWrapper.createFromField(clazz, context, field);
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/forms/FormFieldWrapper.java b/src/main/java/eu/siacs/conversations/ui/forms/FormFieldWrapper.java
new file mode 100644
index 00000000..cb504576
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/forms/FormFieldWrapper.java
@@ -0,0 +1,95 @@
+package eu.siacs.conversations.ui.forms;
+
+import android.content.Context;
+import android.text.SpannableString;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.StyleSpan;
+import android.view.LayoutInflater;
+import android.view.View;
+
+import java.util.List;
+
+import de.thedevstack.conversationsplus.ConversationsPlusColors;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.xmpp.forms.Field;
+
+public abstract class FormFieldWrapper {
+
+ protected final Context context;
+ protected final Field field;
+ protected final View view;
+ protected OnFormFieldValuesEdited onFormFieldValuesEditedListener;
+
+ protected FormFieldWrapper(Context context, Field field) {
+ this.context = context;
+ this.field = field;
+ LayoutInflater inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ this.view = inflater.inflate(getLayoutResource(), null);
+ String label = field.getLabel();
+ if (label == null) {
+ label = field.getFieldName();
+ }
+ setLabel(label, field.isRequired());
+ }
+
+ public final void submit() {
+ this.field.setValues(getValues());
+ }
+
+ public final View getView() {
+ return view;
+ }
+
+ protected abstract void setLabel(String label, boolean required);
+
+ abstract List<String> getValues();
+
+ protected abstract void setValues(List<String> values);
+
+ abstract boolean validates();
+
+ abstract protected int getLayoutResource();
+
+ abstract void setReadOnly(boolean readOnly);
+
+ protected SpannableString createSpannableLabelString(String label, boolean required) {
+ SpannableString spannableString = new SpannableString(label + (required ? " *" : ""));
+ if (required) {
+ int start = label.length();
+ int end = label.length() + 2;
+ spannableString.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), start, end, 0);
+ spannableString.setSpan(new ForegroundColorSpan(ConversationsPlusColors.accent()), start, end, 0);
+ }
+ return spannableString;
+ }
+
+ protected void invokeOnFormFieldValuesEdited() {
+ if (this.onFormFieldValuesEditedListener != null) {
+ this.onFormFieldValuesEditedListener.onFormFieldValuesEdited();
+ }
+ }
+
+ public boolean edited() {
+ return !field.getValues().equals(getValues());
+ }
+
+ public void setOnFormFieldValuesEditedListener(OnFormFieldValuesEdited listener) {
+ this.onFormFieldValuesEditedListener = listener;
+ }
+
+ protected static <F extends FormFieldWrapper> FormFieldWrapper createFromField(Class<F> c, Context context, Field field) {
+ try {
+ F fieldWrapper = c.getDeclaredConstructor(Context.class, Field.class).newInstance(context,field);
+ fieldWrapper.setValues(field.getValues());
+ return fieldWrapper;
+ } catch (Exception e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ public interface OnFormFieldValuesEdited {
+ void onFormFieldValuesEdited();
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/forms/FormJidSingleFieldWrapper.java b/src/main/java/eu/siacs/conversations/ui/forms/FormJidSingleFieldWrapper.java
new file mode 100644
index 00000000..553e8f21
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/forms/FormJidSingleFieldWrapper.java
@@ -0,0 +1,44 @@
+package eu.siacs.conversations.ui.forms;
+
+import android.content.Context;
+import android.text.InputType;
+
+import java.util.List;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.xmpp.forms.Field;
+import eu.siacs.conversations.xmpp.jid.InvalidJidException;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+public class FormJidSingleFieldWrapper extends FormTextFieldWrapper {
+
+ protected FormJidSingleFieldWrapper(Context context, Field field) {
+ super(context, field);
+ editText.setInputType(InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS);
+ editText.setHint(R.string.account_settings_example_jabber_id);
+ }
+
+ @Override
+ public boolean validates() {
+ String value = getValue();
+ if (!value.isEmpty()) {
+ try {
+ Jid.fromString(value);
+ } catch (InvalidJidException e) {
+ editText.setError(context.getString(R.string.invalid_jid));
+ editText.requestFocus();
+ return false;
+ }
+ }
+ return super.validates();
+ }
+
+ @Override
+ protected void setValues(List<String> values) {
+ StringBuilder builder = new StringBuilder("");
+ for(String value : values) {
+ builder.append(value);
+ }
+ editText.setText(builder.toString());
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/forms/FormTextFieldWrapper.java b/src/main/java/eu/siacs/conversations/ui/forms/FormTextFieldWrapper.java
new file mode 100644
index 00000000..b7dac951
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/forms/FormTextFieldWrapper.java
@@ -0,0 +1,97 @@
+package eu.siacs.conversations.ui.forms;
+
+import android.content.Context;
+import android.text.Editable;
+import android.text.InputType;
+import android.text.TextWatcher;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.xmpp.forms.Field;
+
+public class FormTextFieldWrapper extends FormFieldWrapper {
+
+ protected EditText editText;
+
+ protected FormTextFieldWrapper(Context context, Field field) {
+ super(context, field);
+ editText = (EditText) view.findViewById(R.id.field);
+ editText.setSingleLine(!"text-multi".equals(field.getType()));
+ if ("text-private".equals(field.getType())) {
+ editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
+ }
+ editText.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ editText.setError(null);
+ invokeOnFormFieldValuesEdited();
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ }
+ });
+ }
+
+ @Override
+ protected void setLabel(String label, boolean required) {
+ TextView textView = (TextView) view.findViewById(R.id.label);
+ textView.setText(createSpannableLabelString(label, required));
+ }
+
+ protected String getValue() {
+ return editText.getText().toString();
+ }
+
+ @Override
+ public List<String> getValues() {
+ List<String> values = new ArrayList<>();
+ for (String line : getValue().split("\\n")) {
+ if (line.length() > 0) {
+ values.add(line);
+ }
+ }
+ return values;
+ }
+
+ @Override
+ protected void setValues(List<String> values) {
+ StringBuilder builder = new StringBuilder("");
+ for(int i = 0; i < values.size(); ++i) {
+ builder.append(values.get(i));
+ if (i < values.size() - 1 && "text-multi".equals(field.getType())) {
+ builder.append("\n");
+ }
+ }
+ editText.setText(builder.toString());
+ }
+
+ @Override
+ public boolean validates() {
+ if (getValue().trim().length() > 0 || !field.isRequired()) {
+ return true;
+ } else {
+ editText.setError(context.getString(R.string.this_field_is_required));
+ editText.requestFocus();
+ return false;
+ }
+ }
+
+ @Override
+ protected int getLayoutResource() {
+ return R.layout.form_text;
+ }
+
+ @Override
+ void setReadOnly(boolean readOnly) {
+ editText.setEnabled(!readOnly);
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/forms/FormWrapper.java b/src/main/java/eu/siacs/conversations/ui/forms/FormWrapper.java
new file mode 100644
index 00000000..eafe95cc
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/forms/FormWrapper.java
@@ -0,0 +1,72 @@
+package eu.siacs.conversations.ui.forms;
+
+import android.content.Context;
+import android.widget.LinearLayout;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import eu.siacs.conversations.xmpp.forms.Data;
+import eu.siacs.conversations.xmpp.forms.Field;
+
+public class FormWrapper {
+
+ private final LinearLayout layout;
+
+ private final Data form;
+
+ private final List<FormFieldWrapper> fieldWrappers = new ArrayList<>();
+
+ private FormWrapper(Context context, LinearLayout linearLayout, Data form) {
+ this.form = form;
+ this.layout = linearLayout;
+ this.layout.removeAllViews();
+ for(Field field : form.getFields()) {
+ FormFieldWrapper fieldWrapper = FormFieldFactory.createFromField(context,field);
+ if (fieldWrapper != null) {
+ layout.addView(fieldWrapper.getView());
+ fieldWrappers.add(fieldWrapper);
+ }
+ }
+ }
+
+ public Data submit() {
+ for(FormFieldWrapper fieldWrapper : fieldWrappers) {
+ fieldWrapper.submit();
+ }
+ this.form.submit();
+ return this.form;
+ }
+
+ public boolean validates() {
+ boolean validates = true;
+ for(FormFieldWrapper fieldWrapper : fieldWrappers) {
+ validates &= fieldWrapper.validates();
+ }
+ return validates;
+ }
+
+ public void setOnFormFieldValuesEditedListener(FormFieldWrapper.OnFormFieldValuesEdited listener) {
+ for(FormFieldWrapper fieldWrapper : fieldWrappers) {
+ fieldWrapper.setOnFormFieldValuesEditedListener(listener);
+ }
+ }
+
+ public void setReadOnly(boolean b) {
+ for(FormFieldWrapper fieldWrapper : fieldWrappers) {
+ fieldWrapper.setReadOnly(b);
+ }
+ }
+
+ public boolean edited() {
+ boolean edited = false;
+ for(FormFieldWrapper fieldWrapper : fieldWrappers) {
+ edited |= fieldWrapper.edited();
+ }
+ return edited;
+ }
+
+ public static FormWrapper createInLayout(Context context, LinearLayout layout, Data form) {
+ return new FormWrapper(context, layout, form);
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/listeners/ConversationMoreMessagesLoadedListener.java b/src/main/java/eu/siacs/conversations/ui/listeners/ConversationMoreMessagesLoadedListener.java
new file mode 100644
index 00000000..08916206
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/listeners/ConversationMoreMessagesLoadedListener.java
@@ -0,0 +1,143 @@
+package eu.siacs.conversations.ui.listeners;
+
+import android.view.View;
+import android.widget.ListView;
+import android.widget.Toast;
+
+import com.orangegangsters.github.swipyrefreshlayout.library.SwipyRefreshLayout;
+
+import java.util.List;
+
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.ui.ConversationActivity;
+import eu.siacs.conversations.ui.ConversationFragment;
+import eu.siacs.conversations.ui.adapter.MessageAdapter;
+
+/**
+ * This listener updates the UI when messages are loaded from the server.
+ */
+public class ConversationMoreMessagesLoadedListener implements XmppConnectionService.OnMoreMessagesLoaded {
+ private SwipyRefreshLayout swipeLayout;
+ private List<Message> messageList;
+ private ConversationFragment fragment;
+ private ListView messagesView;
+ private MessageAdapter messageListAdapter;
+ private Toast messageLoaderToast;
+ /*
+ The current loading status
+ */
+ private boolean loadingMessages = false;
+ /**
+ * Whether the user is loading only history messages or not.
+ * History messages are messages which are older than the oldest in the database.
+ */
+ private boolean loadHistory = true;
+
+ public ConversationMoreMessagesLoadedListener(SwipyRefreshLayout swipeLayout, List<Message> messageList, ConversationFragment fragment, ListView messagesView, MessageAdapter messageListAdapter) {
+ this.swipeLayout = swipeLayout;
+ this.messageList = messageList;
+ this.fragment = fragment;
+ this.messagesView = messagesView;
+ this.messageListAdapter = messageListAdapter;
+ }
+
+ public void setLoadHistory(boolean value) {
+ this.loadHistory = value;
+ }
+
+ public void setLoadingInProgress() {
+ this.loadingMessages = true;
+ }
+
+ public boolean isLoadingInProgress() {
+ return this.loadingMessages;
+ }
+
+ @Override
+ public void onMoreMessagesLoaded(final int c, final Conversation conversation) {
+ ConversationActivity activity = (ConversationActivity) fragment.getActivity();
+ // Current selected conversation is not the same the messages are loaded - skip updating message view and hide loading graphic
+ if (activity.getSelectedConversation() != conversation) {
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ swipeLayout.setRefreshing(false);
+ }
+ });
+ return;
+ }
+ // No new messages are loaded
+ if (0 == c) {
+ if (this.loadHistory) {
+ conversation.setHasMessagesLeftOnServer(false);
+ }
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ swipeLayout.setRefreshing(false);
+ }
+ });
+ }
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ final int oldPosition = messagesView.getFirstVisiblePosition(); // Always 0 - because loading starts always when hitting the top
+ String uuid = null;
+ boolean oldMessageListWasEmpty = messageList.isEmpty();
+ if (-1 < oldPosition && messageList.size() > oldPosition) {
+ Message message = messageList.get(oldPosition);
+ uuid = message != null ? message.getUuid() : null;
+ }
+ View v = messagesView.getChildAt(0);
+ final int pxOffset = (v == null) ? 0 : v.getTop();
+
+ conversation.populateWithMessages(messageList); // This overrides the old message list
+ fragment.updateStatusMessages(); // This adds "messages" to the list for the status
+ messageListAdapter.notifyDataSetChanged();
+ loadingMessages = false; // Loading of messages is finished - next query can be loaded
+
+ int pos = getIndexOf(uuid, messageList);
+
+ if (!oldMessageListWasEmpty) {
+ messagesView.setSelectionFromTop(pos, pxOffset);
+ }
+
+ if (messageLoaderToast != null) {
+ messageLoaderToast.cancel();
+ }
+ swipeLayout.setRefreshing(false);
+ }
+ });
+ }
+
+ @Override
+ public void informUser(final int resId) {
+ final ConversationActivity activity = (ConversationActivity) fragment.getActivity();
+
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (messageLoaderToast != null) {
+ messageLoaderToast.cancel();
+ }
+ messageLoaderToast = Toast.makeText(activity, resId, Toast.LENGTH_LONG);
+ messageLoaderToast.show();
+ }
+ });
+
+ }
+
+ private int getIndexOf(String uuid, List<Message> messages) {
+ if (uuid == null) {
+ return 0;
+ }
+ for (int i = 0; i < messages.size(); ++i) {
+ if (uuid.equals(messages.get(i).getUuid())) {
+ return i;
+ }
+ }
+ return 0;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/listeners/ConversationSwipeRefreshListener.java b/src/main/java/eu/siacs/conversations/ui/listeners/ConversationSwipeRefreshListener.java
new file mode 100644
index 00000000..0cbde814
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/listeners/ConversationSwipeRefreshListener.java
@@ -0,0 +1,103 @@
+package eu.siacs.conversations.ui.listeners;
+
+import android.widget.ListView;
+
+import com.orangegangsters.github.swipyrefreshlayout.library.SwipyRefreshLayout;
+import com.orangegangsters.github.swipyrefreshlayout.library.SwipyRefreshLayoutDirection;
+
+import java.util.List;
+
+import de.thedevstack.android.logcat.Logging;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.services.MessageArchiveService;
+import eu.siacs.conversations.ui.ConversationActivity;
+import eu.siacs.conversations.ui.ConversationFragment;
+import eu.siacs.conversations.ui.adapter.MessageAdapter;
+
+/**
+ * This listener starts loading messages from the server.
+ */
+public class ConversationSwipeRefreshListener implements SwipyRefreshLayout.OnRefreshListener {
+ private List<Message> messageList;
+ private ConversationFragment fragment;
+ private ConversationMoreMessagesLoadedListener listener;
+ private SwipyRefreshLayout swipeLayout;
+
+ public ConversationSwipeRefreshListener(List<Message> messageList, SwipyRefreshLayout swipeLayout, ConversationFragment fragment, ListView messagesView, MessageAdapter messageListAdapter) {
+ this.messageList = messageList;
+ this.fragment = fragment;
+ this.swipeLayout = swipeLayout;
+ this.listener = new ConversationMoreMessagesLoadedListener(swipeLayout, messageList, fragment, messagesView, messageListAdapter);
+ }
+
+ @Override
+ public void onRefresh(SwipyRefreshLayoutDirection direction) {
+ Logging.d(Config.LOGTAG, "Refresh swipe container");
+ Logging.d(Config.LOGTAG, "Refresh direction " + direction);
+ final ConversationActivity activity = (ConversationActivity) fragment.getActivity();
+ if (activity.getSelectedConversation().getAccount().getStatus() != Account.State.DISABLED) {
+ synchronized (this.messageList) {
+ long timestamp;
+ if (SwipyRefreshLayoutDirection.TOP == direction) { // Load history -> messages sent/received before first message in database
+ if (messageList.isEmpty()) {
+ timestamp = System.currentTimeMillis();
+ } else {
+ timestamp = this.messageList.get(0).getTimeSent(); // works only because of the ordering (last msg = first msg in list)
+ }
+ this.listener.setLoadHistory(true);
+ activity.xmppConnectionService.loadMoreMessages(activity.getSelectedConversation(), timestamp, this.listener);
+ } else if (SwipyRefreshLayoutDirection.BOTTOM == direction) { // load messages sent/received between last received or last session establishment and now
+ if (activity.getSelectedConversation().getAccount().isOnlineAndConnected()) {
+ Logging.d("mam", "loading missing messages from mam (last session establishing or last received message)");
+ long lastSessionEstablished = this.getTimestampOfLastSessionEstablished(activity.getSelectedConversation());
+ long lastReceivedMessage = this.getTimestampOfLastReceivedOrTransmittedMessage();
+ long startTimestamp = Math.min(lastSessionEstablished, lastReceivedMessage);
+ MessageArchiveService.Query query = activity.xmppConnectionService.getMessageArchiveService().query(activity.getSelectedConversation(), startTimestamp, System.currentTimeMillis(), this.listener);
+ if (query != null) {
+ this.listener.setLoadHistory(false);
+ } else {
+ Logging.d("mam", "no query built - no messages loaded");
+ this.listener.onMoreMessagesLoaded(0, activity.getSelectedConversation());
+ this.listener.informUser(R.string.no_more_history_on_server);
+ }
+ this.listener.informUser(R.string.fetching_history_from_server);
+ } else {
+ this.listener.informUser(R.string.not_connected_try_again);
+ swipeLayout.setRefreshing(false);
+ }
+ }
+ }
+ } else {
+ this.listener.informUser(R.string.this_account_is_disabled);
+ swipeLayout.setRefreshing(false);
+ }
+ Logging.d(Config.LOGTAG, "End Refresh swipe container");
+ }
+
+ private long getTimestampOfLastReceivedOrTransmittedMessage() {
+ long lastReceivedOrTransmittedMessage = Long.MAX_VALUE;
+ if (null != this.messageList
+ && !this.messageList.isEmpty()) {
+ int lastMessageIndex = this.messageList.size() - 1;
+ if (0 <= lastMessageIndex && this.messageList.size() > lastMessageIndex) {
+ lastReceivedOrTransmittedMessage = this.messageList.get(lastMessageIndex).getTimeSent();
+ }
+ }
+
+ return lastReceivedOrTransmittedMessage;
+ }
+
+ private long getTimestampOfLastSessionEstablished(Conversation conversation) {
+ long lastSessionEstablished = Long.MAX_VALUE;
+ if (null != conversation
+ && null != conversation.getAccount()
+ && null != conversation.getAccount().getXmppConnection()) {
+ lastSessionEstablished = conversation.getAccount().getXmppConnection().getLastSessionEstablished();
+ }
+ return lastSessionEstablished;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java
new file mode 100644
index 00000000..1ef5fb3f
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java
@@ -0,0 +1,211 @@
+package eu.siacs.conversations.utils;
+
+import android.os.Bundle;
+import android.util.Pair;
+
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x500.style.BCStyle;
+import org.bouncycastle.asn1.x500.style.IETFUtils;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
+
+import java.security.MessageDigest;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.text.Normalizer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.xmpp.jid.InvalidJidException;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+public final class CryptoHelper {
+ public static final String FILETRANSFER = "?FILETRANSFERv1:";
+ private final static char[] hexArray = "0123456789abcdef".toCharArray();
+ final public static byte[] ONE = new byte[] { 0, 0, 0, 1 };
+
+ public static String bytesToHex(byte[] bytes) {
+ char[] hexChars = new char[bytes.length * 2];
+ for (int j = 0; j < bytes.length; j++) {
+ int v = bytes[j] & 0xFF;
+ hexChars[j * 2] = hexArray[v >>> 4];
+ hexChars[j * 2 + 1] = hexArray[v & 0x0F];
+ }
+ return new String(hexChars);
+ }
+
+ public static byte[] hexToBytes(String hexString) {
+ int len = hexString.length();
+ byte[] array = new byte[len / 2];
+ for (int i = 0; i < len; i += 2) {
+ array[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4) + Character
+ .digit(hexString.charAt(i + 1), 16));
+ }
+ return array;
+ }
+
+ public static String hexToString(final String hexString) {
+ return new String(hexToBytes(hexString));
+ }
+
+ public static byte[] concatenateByteArrays(byte[] a, byte[] b) {
+ byte[] result = new byte[a.length + b.length];
+ System.arraycopy(a, 0, result, 0, a.length);
+ System.arraycopy(b, 0, result, a.length, b.length);
+ return result;
+ }
+
+ /**
+ * Escapes usernames or passwords for SASL.
+ */
+ public static String saslEscape(final String s) {
+ final StringBuilder sb = new StringBuilder((int) (s.length() * 1.1));
+ for (int i = 0; i < s.length(); i++) {
+ char c = s.charAt(i);
+ switch (c) {
+ case ',':
+ sb.append("=2C");
+ break;
+ case '=':
+ sb.append("=3D");
+ break;
+ default:
+ sb.append(c);
+ break;
+ }
+ }
+ return sb.toString();
+ }
+
+ public static String saslPrep(final String s) {
+ return Normalizer.normalize(s, Normalizer.Form.NFKC);
+ }
+
+ public static String prettifyFingerprint(String fingerprint) {
+ if (fingerprint==null) {
+ return "";
+ } else if (fingerprint.length() < 40) {
+ return fingerprint;
+ }
+ StringBuilder builder = new StringBuilder(fingerprint.toLowerCase(Locale.US).replaceAll("\\s", ""));
+ for(int i=8;i<builder.length();i+=9) {
+ builder.insert(i, ' ');
+ }
+ return builder.toString();
+ }
+
+ public static String prettifyFingerprintCert(String fingerprint) {
+ StringBuilder builder = new StringBuilder(fingerprint);
+ for(int i=2;i < builder.length(); i+=3) {
+ builder.insert(i,':');
+ }
+ return builder.toString();
+ }
+
+ public static String[] getOrderedCipherSuites(final String[] platformSupportedCipherSuites) {
+ final Collection<String> cipherSuites = new LinkedHashSet<>(Arrays.asList(Config.ENABLED_CIPHERS));
+ final List<String> platformCiphers = Arrays.asList(platformSupportedCipherSuites);
+ cipherSuites.retainAll(platformCiphers);
+ cipherSuites.addAll(platformCiphers);
+ filterWeakCipherSuites(cipherSuites);
+ return cipherSuites.toArray(new String[cipherSuites.size()]);
+ }
+
+ private static void filterWeakCipherSuites(final Collection<String> cipherSuites) {
+ final Iterator<String> it = cipherSuites.iterator();
+ while (it.hasNext()) {
+ String cipherName = it.next();
+ // remove all ciphers with no or very weak encryption or no authentication
+ for (String weakCipherPattern : Config.WEAK_CIPHER_PATTERNS) {
+ if (cipherName.contains(weakCipherPattern)) {
+ it.remove();
+ break;
+ }
+ }
+ }
+ }
+
+ public static Pair<Jid,String> extractJidAndName(X509Certificate certificate) throws CertificateEncodingException, InvalidJidException, CertificateParsingException {
+ Collection<List<?>> alternativeNames = certificate.getSubjectAlternativeNames();
+ List<String> emails = new ArrayList<>();
+ if (alternativeNames != null) {
+ for(List<?> san : alternativeNames) {
+ Integer type = (Integer) san.get(0);
+ if (type == 1) {
+ emails.add((String) san.get(1));
+ }
+ }
+ }
+ X500Name x500name = new JcaX509CertificateHolder(certificate).getSubject();
+ if (emails.size() == 0) {
+ emails.add(IETFUtils.valueToString(x500name.getRDNs(BCStyle.EmailAddress)[0].getFirst().getValue()));
+ }
+ String name = IETFUtils.valueToString(x500name.getRDNs(BCStyle.CN)[0].getFirst().getValue());
+ if (emails.size() >= 1) {
+ return new Pair<>(Jid.fromString(emails.get(0)), name);
+ } else {
+ return null;
+ }
+ }
+
+ public static Bundle extractCertificateInformation(X509Certificate certificate) {
+ Bundle information = new Bundle();
+ try {
+ JcaX509CertificateHolder holder = new JcaX509CertificateHolder(certificate);
+ X500Name subject = holder.getSubject();
+ try {
+ information.putString("subject_cn", subject.getRDNs(BCStyle.CN)[0].getFirst().getValue().toString());
+ } catch (Exception e) {
+ //ignored
+ }
+ try {
+ information.putString("subject_o",subject.getRDNs(BCStyle.O)[0].getFirst().getValue().toString());
+ } catch (Exception e) {
+ //ignored
+ }
+
+ X500Name issuer = holder.getIssuer();
+ try {
+ information.putString("issuer_cn", issuer.getRDNs(BCStyle.CN)[0].getFirst().getValue().toString());
+ } catch (Exception e) {
+ //ignored
+ }
+ try {
+ information.putString("issuer_o", issuer.getRDNs(BCStyle.O)[0].getFirst().getValue().toString());
+ } catch (Exception e) {
+ //ignored
+ }
+ try {
+ MessageDigest md = MessageDigest.getInstance("SHA-1");
+ byte[] fingerprint = md.digest(certificate.getEncoded());
+ information.putString("sha1", prettifyFingerprintCert(bytesToHex(fingerprint)));
+ } catch (Exception e) {
+
+ }
+ return information;
+ } catch (CertificateEncodingException e) {
+ return information;
+ }
+ }
+
+ public static int encryptionTypeToText(int encryption) {
+ switch (encryption) {
+ case Message.ENCRYPTION_OTR:
+ return R.string.encryption_choice_otr;
+ case Message.ENCRYPTION_AXOLOTL:
+ return R.string.encryption_choice_omemo;
+ case Message.ENCRYPTION_NONE:
+ return R.string.encryption_choice_unencrypted;
+ default:
+ return R.string.encryption_choice_pgp;
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/utils/DNSHelper.java b/src/main/java/eu/siacs/conversations/utils/DNSHelper.java
new file mode 100644
index 00000000..9caa53f7
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/DNSHelper.java
@@ -0,0 +1,165 @@
+package eu.siacs.conversations.utils;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.RouteInfo;
+import android.os.Build;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.TreeSet;
+import java.util.regex.Pattern;
+
+import de.measite.minidns.Client;
+import de.measite.minidns.DNSMessage;
+import de.measite.minidns.Record;
+import de.measite.minidns.Record.CLASS;
+import de.measite.minidns.Record.TYPE;
+import de.measite.minidns.record.Data;
+import de.measite.minidns.record.SRV;
+import de.measite.minidns.util.NameUtil;
+import de.thedevstack.android.logcat.Logging;
+import de.thedevstack.conversationsplus.ConversationsPlusApplication;
+import de.thedevstack.conversationsplus.dto.SrvRecord;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+public class DNSHelper {
+ private static final String CLIENT_SRV_PREFIX = "_xmpp-client._tcp.";
+ private static final String SECURE_CLIENT_SRV_PREFIX = "_xmpps-client._tcp.";
+ 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");
+
+ protected static Client client = new Client();
+
+ static {
+ client.setTimeout(Config.PING_TIMEOUT * 1000);
+ }
+
+ /**
+ * Queries the SRV record for the server JID.
+ * This method uses all available Domain Name Servers.
+ * @param jid the server JID
+ * @return TreeSet with SrvRecords. If no SRV record is found for JID an empty TreeSet is returned.
+ */
+ public static final TreeSet<SrvRecord> querySrvRecord(Jid jid) {
+ String host = jid.getDomainpart();
+ TreeSet<SrvRecord> result = new TreeSet<>();
+
+ final List<InetAddress> dnsServers = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? getDnsServers() : getDnsServersPreLollipop();
+
+ if (dnsServers != null) {
+ for (InetAddress dnsServer : dnsServers) {
+ result = querySrvRecord(host, dnsServer);
+ if (!result.isEmpty()) {
+ break;
+ }
+ }
+ }
+
+ return result;
+ }
+
+ @TargetApi(21)
+ private static List<InetAddress> getDnsServers() {
+ List<InetAddress> servers = new ArrayList<>();
+ ConnectivityManager connectivityManager = (ConnectivityManager) ConversationsPlusApplication.getInstance().getSystemService(Context.CONNECTIVITY_SERVICE);
+ Network[] networks = connectivityManager == null ? null : connectivityManager.getAllNetworks();
+ if (networks == null) {
+ return getDnsServersPreLollipop();
+ }
+ for(int i = 0; i < networks.length; ++i) {
+ LinkProperties linkProperties = connectivityManager.getLinkProperties(networks[i]);
+ if (linkProperties != null) {
+ if (hasDefaultRoute(linkProperties)) {
+ servers.addAll(0, linkProperties.getDnsServers());
+ } else {
+ servers.addAll(linkProperties.getDnsServers());
+ }
+ }
+ }
+ if (servers.size() > 0) {
+ Logging.d("dns", "used lollipop variant to discover dns servers in " + networks.length + " networks");
+ }
+ return servers.size() > 0 ? servers : getDnsServersPreLollipop();
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ private static boolean hasDefaultRoute(LinkProperties linkProperties) {
+ for(RouteInfo route: linkProperties.getRoutes()) {
+ if (route.isDefaultRoute()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static List<InetAddress> getDnsServersPreLollipop() {
+ List<InetAddress> servers = new ArrayList<>();
+ String[] dns = client.findDNS();
+ for(int i = 0; i < dns.length; ++i) {
+ try {
+ servers.add(InetAddress.getByName(dns[i]));
+ } catch (UnknownHostException e) {
+ //ignore
+ }
+ }
+ return servers;
+ }
+
+ /**
+ * Queries the SRV record for an host from the given Domain Name Server.
+ * @param host the host to query for
+ * @param dnsServerAddress the DNS to query on
+ * @return TreeSet with SrvRecords.
+ */
+ private static final TreeSet<SrvRecord> querySrvRecord(String host, InetAddress dnsServerAddress) {
+ TreeSet<SrvRecord> result = new TreeSet<>();
+ querySrvRecord(host, dnsServerAddress, false, result);
+ querySrvRecord(host, dnsServerAddress, true, result);
+ return result;
+ }
+
+ private static final void querySrvRecord(String host, InetAddress dnsServerAddress, boolean tlsSrvRecord, TreeSet<SrvRecord> result) {
+ String qname = (tlsSrvRecord ? SECURE_CLIENT_SRV_PREFIX : CLIENT_SRV_PREFIX) + host;
+ String dnsServerHostAddress = dnsServerAddress.getHostAddress();
+ Logging.d("dns", "using dns server: " + dnsServerHostAddress + " to look up " + qname);
+ try {
+ DNSMessage message = client.query(qname, TYPE.SRV, CLASS.IN, dnsServerHostAddress);
+ if (null != message) {
+ Record[] rrset = message.getAnswers();
+ for (Record rr : rrset) {
+ Data d = rr.getPayload();
+ if (d instanceof SRV && NameUtil.idnEquals(qname, rr.getName())) {
+ SRV srv = (SRV) d;
+ SrvRecord srvRecord = new SrvRecord(srv.getPriority(), srv.getName(), srv.getPort(), tlsSrvRecord);
+ result.add(srvRecord);
+ }
+ }
+ } else {
+ Logging.e("dns", "No valid DNS message retrieved.");
+ }
+
+ } catch (IOException e) {
+ Logging.d("dns", "Error while retrieving SRV record '" + qname + "' for '" + host + "' from DNS '" + dnsServerHostAddress + "': " + e.getMessage());
+ }
+ }
+
+ public 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());
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java b/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java
new file mode 100644
index 00000000..4e3ec236
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java
@@ -0,0 +1,46 @@
+package eu.siacs.conversations.utils;
+
+import android.content.Context;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.lang.Thread.UncaughtExceptionHandler;
+
+public class ExceptionHandler implements UncaughtExceptionHandler {
+
+ private UncaughtExceptionHandler defaultHandler;
+ private Context context;
+
+ public ExceptionHandler(Context context) {
+ this.context = context;
+ this.defaultHandler = Thread.getDefaultUncaughtExceptionHandler();
+ }
+
+ @Override
+ public void uncaughtException(Thread thread, Throwable ex) {
+ Writer result = new StringWriter();
+ PrintWriter printWriter = new PrintWriter(result);
+ ex.printStackTrace(printWriter);
+ String stacktrace = result.toString();
+ printWriter.close();
+ try {
+ OutputStream os = context.openFileOutput("stacktrace.txt",
+ Context.MODE_PRIVATE);
+ os.write(stacktrace.getBytes());
+ os.flush();
+ os.close();
+ } catch (FileNotFoundException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (IOException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ this.defaultHandler.uncaughtException(thread, ex);
+ }
+
+}
diff --git a/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java b/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java
new file mode 100644
index 00000000..9c8db210
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java
@@ -0,0 +1,117 @@
+package eu.siacs.conversations.utils;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.text.format.DateUtils;
+
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.List;
+
+import de.thedevstack.android.logcat.Logging;
+import de.thedevstack.conversationsplus.ConversationsPlusPreferences;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.ui.ConversationActivity;
+import eu.siacs.conversations.xmpp.jid.InvalidJidException;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+public class ExceptionHelper {
+ public static void init(Context context) {
+ if (!(Thread.getDefaultUncaughtExceptionHandler() instanceof ExceptionHandler)) {
+ Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler(
+ context));
+ }
+ }
+
+ public static boolean checkForCrash(final ConversationActivity activity, final XmppConnectionService service) {
+ try {
+ boolean neverSend = ConversationsPlusPreferences.neverSend();
+ if (neverSend) {
+ return false;
+ }
+ List<Account> accounts = service.getAccounts();
+ Account account = null;
+ for (int i = 0; i < accounts.size(); ++i) {
+ if (!accounts.get(i).isOptionSet(Account.OPTION_DISABLED)) {
+ account = accounts.get(i);
+ break;
+ }
+ }
+ if (account == null) {
+ return false;
+ }
+ final Account finalAccount = account;
+ FileInputStream file = activity.openFileInput("stacktrace.txt");
+ InputStreamReader inputStreamReader = new InputStreamReader(file);
+ BufferedReader stacktrace = new BufferedReader(inputStreamReader);
+ final StringBuilder report = new StringBuilder();
+ PackageManager pm = activity.getPackageManager();
+ PackageInfo packageInfo = null;
+ try {
+ packageInfo = pm.getPackageInfo(activity.getPackageName(), 0);
+ report.append("Version: " + packageInfo.versionName + '\n');
+ report.append("Last Update: "
+ + DateUtils.formatDateTime(activity,
+ packageInfo.lastUpdateTime,
+ DateUtils.FORMAT_SHOW_TIME
+ | DateUtils.FORMAT_SHOW_DATE) + '\n');
+ } catch (NameNotFoundException e) {
+ return false;
+ }
+ String line;
+ while ((line = stacktrace.readLine()) != null) {
+ report.append(line);
+ report.append('\n');
+ }
+ file.close();
+ activity.deleteFile("stacktrace.txt");
+ AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+ builder.setTitle(activity.getString(R.string.crash_report_title));
+ builder.setMessage(activity.getText(R.string.crash_report_message));
+ builder.setPositiveButton(activity.getText(R.string.send_now),
+ new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+
+ Logging.d(Config.LOGTAG, "using account="
+ + finalAccount.getJid().toBareJid()
+ + " to send in stack trace");
+ Conversation conversation = null;
+ try {
+ conversation = service.findOrCreateConversation(finalAccount,
+ Jid.fromString(activity.getString(R.string.cplus_bugreport_jabberid)), false);
+ } catch (final InvalidJidException ignored) {
+ }
+ Message message = new Message(conversation, report
+ .toString(), Message.ENCRYPTION_NONE);
+ service.sendMessage(message);
+ }
+ });
+ builder.setNegativeButton(activity.getText(R.string.send_never),
+ new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ ConversationsPlusPreferences.applyNeverSend(true);
+ }
+ });
+ builder.create().show();
+ return true;
+ } catch (final IOException ignored) {
+ return false;
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/utils/ExifHelper.java b/src/main/java/eu/siacs/conversations/utils/ExifHelper.java
new file mode 100644
index 00000000..5e465e94
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/ExifHelper.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2011 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.
+ */
+
+package eu.siacs.conversations.utils;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import de.thedevstack.android.logcat.Logging;
+
+public class ExifHelper {
+ private static final String TAG = "CameraExif";
+
+ public static int getOrientation(InputStream is) {
+ if (is == null) {
+ return 0;
+ }
+
+ byte[] buf = new byte[8];
+ int length = 0;
+
+ // ISO/IEC 10918-1:1993(E)
+ while (read(is, buf, 2) && (buf[0] & 0xFF) == 0xFF) {
+ int marker = buf[1] & 0xFF;
+
+ // Check if the marker is a padding.
+ if (marker == 0xFF) {
+ continue;
+ }
+
+ // Check if the marker is SOI or TEM.
+ if (marker == 0xD8 || marker == 0x01) {
+ continue;
+ }
+ // Check if the marker is EOI or SOS.
+ if (marker == 0xD9 || marker == 0xDA) {
+ return 0;
+ }
+
+ // Get the length and check if it is reasonable.
+ if (!read(is, buf, 2)) {
+ return 0;
+ }
+ length = pack(buf, 0, 2, false);
+ if (length < 2) {
+ Logging.e(TAG, "Invalid length");
+ return 0;
+ }
+ length -= 2;
+
+ // Break if the marker is EXIF in APP1.
+ if (marker == 0xE1 && length >= 6) {
+ if (!read(is, buf, 6)) return 0;
+ length -= 6;
+ if (pack(buf, 0, 4, false) == 0x45786966 &&
+ pack(buf, 4, 2, false) == 0) {
+ break;
+ }
+ }
+
+ // Skip other markers.
+ try {
+ is.skip(length);
+ } catch (IOException ex) {
+ return 0;
+ }
+ length = 0;
+ }
+
+ // JEITA CP-3451 Exif Version 2.2
+ if (length > 8) {
+ int offset = 0;
+ byte[] jpeg = new byte[length];
+ if (!read(is, jpeg, length)) {
+ return 0;
+ }
+
+ // Identify the byte order.
+ int tag = pack(jpeg, offset, 4, false);
+ if (tag != 0x49492A00 && tag != 0x4D4D002A) {
+ Logging.e(TAG, "Invalid byte order");
+ return 0;
+ }
+ boolean littleEndian = (tag == 0x49492A00);
+
+ // Get the offset and check if it is reasonable.
+ int count = pack(jpeg, offset + 4, 4, littleEndian) + 2;
+ if (count < 10 || count > length) {
+ Logging.e(TAG, "Invalid offset");
+ return 0;
+ }
+ offset += count;
+ length -= count;
+
+ // Get the count and go through all the elements.
+ count = pack(jpeg, offset - 2, 2, littleEndian);
+ while (count-- > 0 && length >= 12) {
+ // Get the tag and check if it is orientation.
+ tag = pack(jpeg, offset, 2, littleEndian);
+ if (tag == 0x0112) {
+ // We do not really care about type and count, do we?
+ int orientation = pack(jpeg, offset + 8, 2, littleEndian);
+ switch (orientation) {
+ case 1:
+ return 0;
+ case 3:
+ return 180;
+ case 6:
+ return 90;
+ case 8:
+ return 270;
+ }
+ Logging.i(TAG, "Unsupported orientation");
+ return 0;
+ }
+ offset += 12;
+ length -= 12;
+ }
+ }
+
+ Logging.i(TAG, "Orientation not found");
+ return 0;
+ }
+
+ private static int pack(byte[] bytes, int offset, int length,
+ boolean littleEndian) {
+ int step = 1;
+ if (littleEndian) {
+ offset += length - 1;
+ step = -1;
+ }
+
+ int value = 0;
+ while (length-- > 0) {
+ value = (value << 8) | (bytes[offset] & 0xFF);
+ offset += step;
+ }
+ return value;
+ }
+
+ private static boolean read(InputStream is, byte[] buf, int length) {
+ try {
+ return is.read(buf, 0, length) == length;
+ } catch (IOException ex) {
+ return false;
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/utils/FileUtils.java b/src/main/java/eu/siacs/conversations/utils/FileUtils.java
new file mode 100644
index 00000000..3633025f
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/FileUtils.java
@@ -0,0 +1,261 @@
+package eu.siacs.conversations.utils;
+
+import android.annotation.SuppressLint;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Environment;
+import android.provider.DocumentsContract;
+import android.provider.MediaStore;
+import android.provider.OpenableColumns;
+
+import java.io.File;
+import java.net.URL;
+import java.util.List;
+
+import de.thedevstack.conversationsplus.ConversationsPlusApplication;
+
+import eu.siacs.conversations.entities.Transferable;
+
+public final class FileUtils {
+
+ /**
+ * Get a file path from a Uri. This will get the the path for Storage Access
+ * Framework Documents, as well as the _data field for the MediaStore and
+ * other file-based ContentProviders.
+ *
+ * @param uri The Uri to query.
+ * @author paulburke
+ */
+ @SuppressLint("NewApi")
+ public static String getPath(final Uri uri) {
+ if (uri == null) {
+ return null;
+ }
+
+ final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
+ final Context context = ConversationsPlusApplication.getAppContext();
+ // DocumentProvider
+ if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
+ // ExternalStorageProvider
+ if (isExternalStorageDocument(uri)) {
+ final String docId = DocumentsContract.getDocumentId(uri);
+ final String[] split = docId.split(":");
+ final String type = split[0];
+
+ if ("primary".equalsIgnoreCase(type)) {
+ return Environment.getExternalStorageDirectory() + "/" + split[1];
+ }
+
+ // TODO handle non-primary volumes
+ }
+ // DownloadsProvider
+ else if (isDownloadsDocument(uri)) {
+
+ final String id = DocumentsContract.getDocumentId(uri);
+ final Uri contentUri = ContentUris.withAppendedId(
+ Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
+
+ return getDataColumn(context, contentUri, null, null);
+ }
+ // MediaProvider
+ else if (isMediaDocument(uri)) {
+ final String docId = DocumentsContract.getDocumentId(uri);
+ final String[] split = docId.split(":");
+ final String type = split[0];
+
+ Uri contentUri = null;
+ if ("image".equals(type)) {
+ contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
+ } else if ("video".equals(type)) {
+ contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
+ } else if ("audio".equals(type)) {
+ contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
+ }
+
+ final String selection = "_id=?";
+ final String[] selectionArgs = new String[]{
+ split[1]
+ };
+
+ return getDataColumn(context, contentUri, selection, selectionArgs);
+ }
+ }
+ // MediaStore (and general)
+ else if (ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) {
+ String path = getDataColumn(context, uri, null, null);
+ if (path != null) {
+ File file = new File(path);
+ if (!file.canRead()) {
+ return null;
+ }
+ }
+ return path;
+ }
+ // File
+ else if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
+ return uri.getPath();
+ }
+
+ return null;
+ }
+
+ /**
+ * Get the value of the data column for this Uri. This is useful for
+ * MediaStore Uris, and other file-based ContentProviders.
+ *
+ * @param context The context.
+ * @param uri The Uri to query.
+ * @param selection (Optional) Filter used in the query.
+ * @param selectionArgs (Optional) Selection arguments used in the query.
+ * @return The value of the _data column, which is typically a file path.
+ */
+ private static String getDataColumn(Context context, Uri uri, String selection,
+ String[] selectionArgs) {
+
+ Cursor cursor = null;
+ final String column = "_data";
+ final String[] projection = {
+ column
+ };
+
+ try {
+ cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,null);
+ if (cursor != null && cursor.moveToFirst()) {
+ final int column_index = cursor.getColumnIndexOrThrow(column);
+ return cursor.getString(column_index);
+ }
+ } catch(Exception e) {
+ return null;
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ return null;
+ }
+
+
+ /**
+ * @param uri The Uri to check.
+ * @return Whether the Uri authority is ExternalStorageProvider.
+ */
+ public static boolean isExternalStorageDocument(Uri uri) {
+ return "com.android.externalstorage.documents".equals(uri.getAuthority());
+ }
+
+ /**
+ * @param uri The Uri to check.
+ * @return Whether the Uri authority is DownloadsProvider.
+ */
+ public static boolean isDownloadsDocument(Uri uri) {
+ return "com.android.providers.downloads.documents".equals(uri.getAuthority());
+ }
+
+ /**
+ * @param uri The Uri to check.
+ * @return Whether the Uri authority is MediaProvider.
+ */
+ public static boolean isMediaDocument(Uri uri) {
+ return "com.android.providers.media.documents".equals(uri.getAuthority());
+ }
+
+ public static String getRelevantExtension(URL url) {
+ if (url == null) {
+ return null;
+ }
+ String path = url.getPath();
+ return getRelevantExtension(path);
+ }
+
+ public static String getRelevantExtension(String path) {
+ if (path == null || path.isEmpty()) {
+ return null;
+ }
+
+ String filename = path.substring(path.lastIndexOf('/') + 1).toLowerCase();
+
+ final String lastPart = FileUtils.getLastExtension(filename);
+
+ if (!lastPart.isEmpty()) {
+ // we want the real file extension, not the crypto one
+ final String secondToLastPart = FileUtils.getSecondToLastExtension(filename);
+ if (!secondToLastPart.isEmpty() && Transferable.VALID_CRYPTO_EXTENSIONS.contains(lastPart)) {
+ return secondToLastPart;
+ } else {
+ return lastPart;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @param filename The filename to extract extension from
+ * @return last extension or empty string
+ */
+ public static String getLastExtension(final String filename) {
+ if (filename == null || filename.isEmpty()) {
+ return "";
+ }
+ final int lastDotPosition = filename.lastIndexOf('.');
+ final String lastPart = lastDotPosition != -1 ?
+ filename.substring(lastDotPosition + 1) : "";
+ return lastPart;
+ }
+
+ /**
+ * @param filename The filename to extract extension from
+ * @return second to last extension or empty string
+ */
+ public static String getSecondToLastExtension(final String filename) {
+ if (filename == null || filename.isEmpty()) {
+ return "";
+ }
+ final int lastDotPosition = filename.lastIndexOf('.');
+ final int secondToLastDotPosition = filename.lastIndexOf('.', lastDotPosition - 1);
+ final String secondToLastPart = secondToLastDotPosition != -1 ?
+ filename.substring(secondToLastDotPosition + 1, lastDotPosition) : "";
+ return secondToLastPart;
+ }
+
+ /**
+ * Retrieve file size from given uri
+ * @param context actual Context
+ * @param uri uri to file
+ * @return file size or -1 in case of error
+ */
+ private static long getFileSize(Context context, Uri uri) {
+ Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
+ if (cursor != null && cursor.moveToFirst()) {
+ return cursor.getLong(cursor.getColumnIndex(OpenableColumns.SIZE));
+ } else {
+ return -1;
+ }
+ }
+
+ /**
+ * Check for given list of uris if corresponding file sizes are all smaller than given maximum
+ * @param context actual Context
+ * @param uris list of uris
+ * @param max maximum file size
+ * @return true if all file sizes are smaller than max, false otherwise
+ */
+ public static boolean allFilesUnderSize(Context context, List<Uri> uris, long max) {
+ if (max <= 0) {
+ return true; //exception to be compatible with HTTP Upload < v0.2
+ }
+ for(Uri uri : uris) {
+ if (getFileSize(context, uri) > max) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private FileUtils() {
+ // Utility class - do not instantiate
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/utils/GeoHelper.java b/src/main/java/eu/siacs/conversations/utils/GeoHelper.java
new file mode 100644
index 00000000..74f91a98
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/GeoHelper.java
@@ -0,0 +1,77 @@
+package eu.siacs.conversations.utils;
+
+import android.content.Intent;
+import android.net.Uri;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+
+public class GeoHelper {
+ private static Pattern GEO_URI = Pattern.compile("geo:([\\-0-9.]+),([\\-0-9.]+)(?:,([\\-0-9.]+))?(?:\\?(.*))?", Pattern.CASE_INSENSITIVE);
+
+ public static boolean isGeoUri(String body) {
+ return body != null && GEO_URI.matcher(body).matches();
+ }
+
+ public static ArrayList<Intent> createGeoIntentsFromMessage(Message message) {
+ final ArrayList<Intent> intents = new ArrayList<>();
+ Matcher matcher = GEO_URI.matcher(message.getBody());
+ if (!matcher.matches()) {
+ return intents;
+ }
+ double latitude;
+ double longitude;
+ try {
+ latitude = Double.parseDouble(matcher.group(1));
+ if (latitude > 90.0 || latitude < -90.0) {
+ return intents;
+ }
+ longitude = Double.parseDouble(matcher.group(2));
+ if (longitude > 180.0 || longitude < -180.0) {
+ return intents;
+ }
+ } catch (NumberFormatException nfe) {
+ return intents;
+ }
+ final Conversation conversation = message.getConversation();
+ String label;
+ if (conversation.getMode() == Conversation.MODE_SINGLE && message.getStatus() == Message.STATUS_RECEIVED) {
+ try {
+ label = "(" + URLEncoder.encode(message.getConversation().getName(), "UTF-8") + ")";
+ } catch (UnsupportedEncodingException e) {
+ label = "";
+ }
+ } else {
+ label = "";
+ }
+
+ Intent locationPluginIntent = new Intent("eu.siacs.conversations.location.show");
+ locationPluginIntent.putExtra("latitude",latitude);
+ locationPluginIntent.putExtra("longitude",longitude);
+ if (conversation.getMode() == Conversation.MODE_SINGLE) {
+ if (message.getStatus() == Message.STATUS_RECEIVED) {
+ locationPluginIntent.putExtra("name",conversation.getName());
+ locationPluginIntent.putExtra("jid",message.getCounterpart().toString());
+ }
+ else {
+ locationPluginIntent.putExtra("jid",conversation.getAccount().getJid().toString());
+ }
+ }
+ intents.add(locationPluginIntent);
+
+ Intent geoIntent = new Intent(Intent.ACTION_VIEW);
+ geoIntent.setData(Uri.parse("geo:" + String.valueOf(latitude) + "," + String.valueOf(longitude) + "?q=" + String.valueOf(latitude) + "," + String.valueOf(longitude) + label));
+ intents.add(geoIntent);
+
+ Intent httpIntent = new Intent(Intent.ACTION_VIEW);
+ httpIntent.setData(Uri.parse("https://maps.google.com/maps?q=loc:"+String.valueOf(latitude) + "," + String.valueOf(longitude) +label));
+ intents.add(httpIntent);
+ return intents;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/utils/MimeUtils.java b/src/main/java/eu/siacs/conversations/utils/MimeUtils.java
new file mode 100644
index 00000000..d4544424
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/MimeUtils.java
@@ -0,0 +1,487 @@
+/*
+ * Copyright (C) 2010 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.
+ */
+package eu.siacs.conversations.utils;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+/**
+ * Utilities for dealing with MIME types.
+ * Used to implement java.net.URLConnection and android.webkit.MimeTypeMap.
+ */
+public final class MimeUtils {
+ private static final Map<String, String> mimeTypeToExtensionMap = new HashMap<String, String>();
+ private static final Map<String, String> extensionToMimeTypeMap = new HashMap<String, String>();
+ static {
+ // The following table is based on /etc/mime.types data minus
+ // chemical/* MIME types and MIME types that don't map to any
+ // file extensions. We also exclude top-level domain names to
+ // deal with cases like:
+ //
+ // mail.google.com/a/google.com
+ //
+ // and "active" MIME types (due to potential security issues).
+ // Note that this list is _not_ in alphabetical order and must not be sorted.
+ // The "most popular" extension must come first, so that it's the one returned
+ // by guessExtensionFromMimeType.
+ add("application/andrew-inset", "ez");
+ add("application/dsptype", "tsp");
+ add("application/hta", "hta");
+ add("application/mac-binhex40", "hqx");
+ add("application/mathematica", "nb");
+ add("application/msaccess", "mdb");
+ add("application/oda", "oda");
+ add("application/ogg", "ogg");
+ add("application/ogg", "oga");
+ add("application/pdf", "pdf");
+ add("application/pgp-keys", "key");
+ add("application/pgp-signature", "pgp");
+ add("application/pics-rules", "prf");
+ add("application/pkix-cert", "cer");
+ add("application/rar", "rar");
+ add("application/rdf+xml", "rdf");
+ add("application/rss+xml", "rss");
+ add("application/zip", "zip");
+ add("application/vnd.android.package-archive", "apk");
+ add("application/vnd.cinderella", "cdy");
+ add("application/vnd.ms-pki.stl", "stl");
+ add("application/vnd.oasis.opendocument.database", "odb");
+ add("application/vnd.oasis.opendocument.formula", "odf");
+ add("application/vnd.oasis.opendocument.graphics", "odg");
+ add("application/vnd.oasis.opendocument.graphics-template", "otg");
+ add("application/vnd.oasis.opendocument.image", "odi");
+ add("application/vnd.oasis.opendocument.spreadsheet", "ods");
+ add("application/vnd.oasis.opendocument.spreadsheet-template", "ots");
+ add("application/vnd.oasis.opendocument.text", "odt");
+ add("application/vnd.oasis.opendocument.text-master", "odm");
+ add("application/vnd.oasis.opendocument.text-template", "ott");
+ add("application/vnd.oasis.opendocument.text-web", "oth");
+ add("application/vnd.google-earth.kml+xml", "kml");
+ add("application/vnd.google-earth.kmz", "kmz");
+ add("application/msword", "doc");
+ add("application/msword", "dot");
+ add("application/vnd.openxmlformats-officedocument.wordprocessingml.document", "docx");
+ add("application/vnd.openxmlformats-officedocument.wordprocessingml.template", "dotx");
+ add("application/vnd.ms-excel", "xls");
+ add("application/vnd.ms-excel", "xlt");
+ add("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "xlsx");
+ add("application/vnd.openxmlformats-officedocument.spreadsheetml.template", "xltx");
+ add("application/vnd.ms-powerpoint", "ppt");
+ add("application/vnd.ms-powerpoint", "pot");
+ add("application/vnd.ms-powerpoint", "pps");
+ add("application/vnd.openxmlformats-officedocument.presentationml.presentation", "pptx");
+ add("application/vnd.openxmlformats-officedocument.presentationml.template", "potx");
+ add("application/vnd.openxmlformats-officedocument.presentationml.slideshow", "ppsx");
+ add("application/vnd.rim.cod", "cod");
+ add("application/vnd.smaf", "mmf");
+ add("application/vnd.stardivision.calc", "sdc");
+ add("application/vnd.stardivision.draw", "sda");
+ add("application/vnd.stardivision.impress", "sdd");
+ add("application/vnd.stardivision.impress", "sdp");
+ add("application/vnd.stardivision.math", "smf");
+ add("application/vnd.stardivision.writer", "sdw");
+ add("application/vnd.stardivision.writer", "vor");
+ add("application/vnd.stardivision.writer-global", "sgl");
+ add("application/vnd.sun.xml.calc", "sxc");
+ add("application/vnd.sun.xml.calc.template", "stc");
+ add("application/vnd.sun.xml.draw", "sxd");
+ add("application/vnd.sun.xml.draw.template", "std");
+ add("application/vnd.sun.xml.impress", "sxi");
+ add("application/vnd.sun.xml.impress.template", "sti");
+ add("application/vnd.sun.xml.math", "sxm");
+ add("application/vnd.sun.xml.writer", "sxw");
+ add("application/vnd.sun.xml.writer.global", "sxg");
+ add("application/vnd.sun.xml.writer.template", "stw");
+ add("application/vnd.visio", "vsd");
+ add("application/x-abiword", "abw");
+ add("application/x-apple-diskimage", "dmg");
+ add("application/x-bcpio", "bcpio");
+ add("application/x-bittorrent", "torrent");
+ add("application/x-cdf", "cdf");
+ add("application/x-cdlink", "vcd");
+ add("application/x-chess-pgn", "pgn");
+ add("application/x-cpio", "cpio");
+ add("application/x-debian-package", "deb");
+ add("application/x-debian-package", "udeb");
+ add("application/x-director", "dcr");
+ add("application/x-director", "dir");
+ add("application/x-director", "dxr");
+ add("application/x-dms", "dms");
+ add("application/x-doom", "wad");
+ add("application/x-dvi", "dvi");
+ add("application/x-font", "pfa");
+ add("application/x-font", "pfb");
+ add("application/x-font", "gsf");
+ add("application/x-font", "pcf");
+ add("application/x-font", "pcf.Z");
+ add("application/x-freemind", "mm");
+ // application/futuresplash isn't IANA, so application/x-futuresplash should come first.
+ add("application/x-futuresplash", "spl");
+ add("application/futuresplash", "spl");
+ add("application/x-gnumeric", "gnumeric");
+ add("application/x-go-sgf", "sgf");
+ add("application/x-graphing-calculator", "gcf");
+ add("application/x-gtar", "tgz");
+ add("application/x-gtar", "gtar");
+ add("application/x-gtar", "taz");
+ add("application/x-hdf", "hdf");
+ add("application/x-ica", "ica");
+ add("application/x-internet-signup", "ins");
+ add("application/x-internet-signup", "isp");
+ add("application/x-iphone", "iii");
+ add("application/x-iso9660-image", "iso");
+ add("application/x-jmol", "jmz");
+ add("application/x-kchart", "chrt");
+ add("application/x-killustrator", "kil");
+ add("application/x-koan", "skp");
+ add("application/x-koan", "skd");
+ add("application/x-koan", "skt");
+ add("application/x-koan", "skm");
+ add("application/x-kpresenter", "kpr");
+ add("application/x-kpresenter", "kpt");
+ add("application/x-kspread", "ksp");
+ add("application/x-kword", "kwd");
+ add("application/x-kword", "kwt");
+ add("application/x-latex", "latex");
+ add("application/x-lha", "lha");
+ add("application/x-lzh", "lzh");
+ add("application/x-lzx", "lzx");
+ add("application/x-maker", "frm");
+ add("application/x-maker", "maker");
+ add("application/x-maker", "frame");
+ add("application/x-maker", "fb");
+ add("application/x-maker", "book");
+ add("application/x-maker", "fbdoc");
+ add("application/x-mif", "mif");
+ add("application/x-ms-wmd", "wmd");
+ add("application/x-ms-wmz", "wmz");
+ add("application/x-msi", "msi");
+ add("application/x-ns-proxy-autoconfig", "pac");
+ add("application/x-nwc", "nwc");
+ add("application/x-object", "o");
+ add("application/x-oz-application", "oza");
+ add("application/x-pem-file", "pem");
+ add("application/x-pkcs12", "p12");
+ add("application/x-pkcs12", "pfx");
+ add("application/x-pkcs7-certreqresp", "p7r");
+ add("application/x-pkcs7-crl", "crl");
+ add("application/x-quicktimeplayer", "qtl");
+ add("application/x-shar", "shar");
+ add("application/x-shockwave-flash", "swf");
+ add("application/x-stuffit", "sit");
+ add("application/x-sv4cpio", "sv4cpio");
+ add("application/x-sv4crc", "sv4crc");
+ add("application/x-tar", "tar");
+ add("application/x-texinfo", "texinfo");
+ add("application/x-texinfo", "texi");
+ add("application/x-troff", "t");
+ add("application/x-troff", "roff");
+ add("application/x-troff-man", "man");
+ add("application/x-ustar", "ustar");
+ add("application/x-wais-source", "src");
+ add("application/x-wingz", "wz");
+ add("application/x-webarchive", "webarchive");
+ add("application/x-webarchive-xml", "webarchivexml");
+ add("application/x-x509-ca-cert", "crt");
+ add("application/x-x509-user-cert", "crt");
+ add("application/x-x509-server-cert", "crt");
+ add("application/x-xcf", "xcf");
+ add("application/x-xfig", "fig");
+ add("application/xhtml+xml", "xhtml");
+ add("audio/3gpp", "3gpp");
+ add("audio/aac", "aac");
+ add("audio/aac-adts", "aac");
+ add("audio/amr", "amr");
+ add("audio/amr-wb", "awb");
+ add("audio/basic", "snd");
+ add("audio/flac", "flac");
+ add("application/x-flac", "flac");
+ add("audio/imelody", "imy");
+ add("audio/midi", "mid");
+ add("audio/midi", "midi");
+ add("audio/midi", "ota");
+ add("audio/midi", "kar");
+ add("audio/midi", "rtttl");
+ add("audio/midi", "xmf");
+ add("audio/mobile-xmf", "mxmf");
+ // add ".mp3" first so it will be the default for guessExtensionFromMimeType
+ add("audio/mpeg", "mp3");
+ add("audio/mpeg", "mpga");
+ add("audio/mpeg", "mpega");
+ add("audio/mpeg", "mp2");
+ add("audio/mpeg", "m4a");
+ add("audio/mpegurl", "m3u");
+ add("audio/prs.sid", "sid");
+ add("audio/x-aiff", "aif");
+ add("audio/x-aiff", "aiff");
+ add("audio/x-aiff", "aifc");
+ add("audio/x-gsm", "gsm");
+ add("audio/x-matroska", "mka");
+ add("audio/x-mpegurl", "m3u");
+ add("audio/x-ms-wma", "wma");
+ add("audio/x-ms-wax", "wax");
+ add("audio/x-pn-realaudio", "ra");
+ add("audio/x-pn-realaudio", "rm");
+ add("audio/x-pn-realaudio", "ram");
+ add("audio/x-realaudio", "ra");
+ add("audio/x-scpls", "pls");
+ add("audio/x-sd2", "sd2");
+ add("audio/x-wav", "wav");
+ // image/bmp isn't IANA, so image/x-ms-bmp should come first.
+ add("image/x-ms-bmp", "bmp");
+ add("image/bmp", "bmp");
+ add("image/gif", "gif");
+ // image/ico isn't IANA, so image/x-icon should come first.
+ add("image/x-icon", "ico");
+ add("image/ico", "cur");
+ add("image/ico", "ico");
+ add("image/ief", "ief");
+ // add ".jpg" first so it will be the default for guessExtensionFromMimeType
+ add("image/jpeg", "jpg");
+ add("image/jpeg", "jpeg");
+ add("image/jpeg", "jpe");
+ add("image/pcx", "pcx");
+ add("image/png", "png");
+ add("image/svg+xml", "svg");
+ add("image/svg+xml", "svgz");
+ add("image/tiff", "tiff");
+ add("image/tiff", "tif");
+ add("image/vnd.djvu", "djvu");
+ add("image/vnd.djvu", "djv");
+ add("image/vnd.wap.wbmp", "wbmp");
+ add("image/webp", "webp");
+ add("image/x-cmu-raster", "ras");
+ add("image/x-coreldraw", "cdr");
+ add("image/x-coreldrawpattern", "pat");
+ add("image/x-coreldrawtemplate", "cdt");
+ add("image/x-corelphotopaint", "cpt");
+ add("image/x-jg", "art");
+ add("image/x-jng", "jng");
+ add("image/x-photoshop", "psd");
+ add("image/x-portable-anymap", "pnm");
+ add("image/x-portable-bitmap", "pbm");
+ add("image/x-portable-graymap", "pgm");
+ add("image/x-portable-pixmap", "ppm");
+ add("image/x-rgb", "rgb");
+ add("image/x-xbitmap", "xbm");
+ add("image/x-xpixmap", "xpm");
+ add("image/x-xwindowdump", "xwd");
+ add("model/iges", "igs");
+ add("model/iges", "iges");
+ add("model/mesh", "msh");
+ add("model/mesh", "mesh");
+ add("model/mesh", "silo");
+ add("text/calendar", "ics");
+ add("text/calendar", "icz");
+ add("text/comma-separated-values", "csv");
+ add("text/css", "css");
+ add("text/html", "htm");
+ add("text/html", "html");
+ add("text/h323", "323");
+ add("text/iuls", "uls");
+ add("text/mathml", "mml");
+ // add ".txt" first so it will be the default for guessExtensionFromMimeType
+ add("text/plain", "txt");
+ add("text/plain", "asc");
+ add("text/plain", "text");
+ add("text/plain", "diff");
+ add("text/plain", "po"); // reserve "pot" for vnd.ms-powerpoint
+ add("text/richtext", "rtx");
+ add("text/rtf", "rtf");
+ add("text/text", "phps");
+ add("text/tab-separated-values", "tsv");
+ add("text/xml", "xml");
+ add("text/x-bibtex", "bib");
+ add("text/x-boo", "boo");
+ add("text/x-c++hdr", "hpp");
+ add("text/x-c++hdr", "h++");
+ add("text/x-c++hdr", "hxx");
+ add("text/x-c++hdr", "hh");
+ add("text/x-c++src", "cpp");
+ add("text/x-c++src", "c++");
+ add("text/x-c++src", "cc");
+ add("text/x-c++src", "cxx");
+ add("text/x-chdr", "h");
+ add("text/x-component", "htc");
+ add("text/x-csh", "csh");
+ add("text/x-csrc", "c");
+ add("text/x-dsrc", "d");
+ add("text/x-haskell", "hs");
+ add("text/x-java", "java");
+ add("text/x-literate-haskell", "lhs");
+ add("text/x-moc", "moc");
+ add("text/x-pascal", "p");
+ add("text/x-pascal", "pas");
+ add("text/x-pcs-gcd", "gcd");
+ add("text/x-setext", "etx");
+ add("text/x-tcl", "tcl");
+ add("text/x-tex", "tex");
+ add("text/x-tex", "ltx");
+ add("text/x-tex", "sty");
+ add("text/x-tex", "cls");
+ add("text/x-vcalendar", "vcs");
+ add("text/x-vcard", "vcf");
+ add("video/3gpp", "3gpp");
+ add("video/3gpp", "3gp");
+ add("video/3gpp2", "3gpp2");
+ add("video/3gpp2", "3g2");
+ add("video/avi", "avi");
+ add("video/dl", "dl");
+ add("video/dv", "dif");
+ add("video/dv", "dv");
+ add("video/fli", "fli");
+ add("video/m4v", "m4v");
+ add("video/mp2ts", "ts");
+ add("video/mpeg", "mpeg");
+ add("video/mpeg", "mpg");
+ add("video/mpeg", "mpe");
+ add("video/mp4", "mp4");
+ add("video/mpeg", "VOB");
+ add("video/quicktime", "qt");
+ add("video/quicktime", "mov");
+ add("video/vnd.mpegurl", "mxu");
+ add("video/webm", "webm");
+ add("video/x-la-asf", "lsf");
+ add("video/x-la-asf", "lsx");
+ add("video/x-matroska", "mkv");
+ add("video/x-mng", "mng");
+ add("video/x-ms-asf", "asf");
+ add("video/x-ms-asf", "asx");
+ add("video/x-ms-wm", "wm");
+ add("video/x-ms-wmv", "wmv");
+ add("video/x-ms-wmx", "wmx");
+ add("video/x-ms-wvx", "wvx");
+ add("video/x-sgi-movie", "movie");
+ add("video/x-webex", "wrf");
+ add("x-conference/x-cooltalk", "ice");
+ add("x-epoc/x-sisx-app", "sisx");
+ applyOverrides();
+ }
+ private static void add(String mimeType, String extension) {
+ // If we have an existing x -> y mapping, we do not want to
+ // override it with another mapping x -> y2.
+ // If a mime type maps to several extensions
+ // the first extension added is considered the most popular
+ // so we do not want to overwrite it later.
+ if (!mimeTypeToExtensionMap.containsKey(mimeType)) {
+ mimeTypeToExtensionMap.put(mimeType, extension);
+ }
+ if (!extensionToMimeTypeMap.containsKey(extension)) {
+ extensionToMimeTypeMap.put(extension, mimeType);
+ }
+ }
+ private static InputStream getContentTypesPropertiesStream() {
+ // User override?
+ String userTable = System.getProperty("content.types.user.table");
+ if (userTable != null) {
+ File f = new File(userTable);
+ if (f.exists()) {
+ try {
+ return new FileInputStream(f);
+ } catch (IOException ignored) {
+ }
+ }
+ }
+ // Standard location?
+ File f = new File(System.getProperty("java.home"), "lib" + File.separator + "content-types.properties");
+ if (f.exists()) {
+ try {
+ return new FileInputStream(f);
+ } catch (IOException ignored) {
+ }
+ }
+ return null;
+ }
+ /**
+ * This isn't what the RI does. The RI doesn't have hard-coded defaults, so supplying your
+ * own "content.types.user.table" means you don't get any of the built-ins, and the built-ins
+ * come from "$JAVA_HOME/lib/content-types.properties".
+ */
+ private static void applyOverrides() {
+ // Get the appropriate InputStream to read overrides from, if any.
+ InputStream stream = getContentTypesPropertiesStream();
+ if (stream == null) {
+ return;
+ }
+ try {
+ try {
+ // Read the properties file...
+ Properties overrides = new Properties();
+ overrides.load(stream);
+ // And translate its mapping to ours...
+ for (Map.Entry<Object, Object> entry : overrides.entrySet()) {
+ String extension = (String) entry.getKey();
+ String mimeType = (String) entry.getValue();
+ add(mimeType, extension);
+ }
+ } finally {
+ stream.close();
+ }
+ } catch (IOException ignored) {
+ }
+ }
+ private MimeUtils() {
+ }
+ /**
+ * Returns true if the given MIME type has an entry in the map.
+ * @param mimeType A MIME type (i.e. text/plain)
+ * @return True iff there is a mimeType entry in the map.
+ */
+ public static boolean hasMimeType(String mimeType) {
+ if (mimeType == null || mimeType.isEmpty()) {
+ return false;
+ }
+ return mimeTypeToExtensionMap.containsKey(mimeType);
+ }
+ /**
+ * Returns the MIME type for the given extension.
+ * @param extension A file extension without the leading '.'
+ * @return The MIME type for the given extension or null iff there is none.
+ */
+ public static String guessMimeTypeFromExtension(String extension) {
+ if (extension == null || extension.isEmpty()) {
+ return null;
+ }
+ return extensionToMimeTypeMap.get(extension.toLowerCase());
+ }
+ /**
+ * Returns true if the given extension has a registered MIME type.
+ * @param extension A file extension without the leading '.'
+ * @return True iff there is an extension entry in the map.
+ */
+ public static boolean hasExtension(String extension) {
+ if (extension == null || extension.isEmpty()) {
+ return false;
+ }
+ return extensionToMimeTypeMap.containsKey(extension);
+ }
+ /**
+ * Returns the registered extension for the given MIME type. Note that some
+ * MIME types map to multiple extensions. This call will return the most
+ * common extension for the given MIME type.
+ * @param mimeType A MIME type (i.e. text/plain)
+ * @return The extension for the given MIME type or null iff there is none.
+ */
+ public static String guessExtensionFromMimeType(String mimeType) {
+ if (mimeType == null || mimeType.isEmpty()) {
+ return null;
+ }
+ return mimeTypeToExtensionMap.get(mimeType);
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java b/src/main/java/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java
new file mode 100644
index 00000000..f18a4ed8
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java
@@ -0,0 +1,9 @@
+package eu.siacs.conversations.utils;
+
+import android.os.Bundle;
+
+import java.util.List;
+
+public interface OnPhoneContactsLoadedListener {
+ public void onPhoneContactsLoaded(List<Bundle> phoneContacts);
+}
diff --git a/src/main/java/eu/siacs/conversations/utils/PRNGFixes.java b/src/main/java/eu/siacs/conversations/utils/PRNGFixes.java
new file mode 100644
index 00000000..5faa1fa7
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/PRNGFixes.java
@@ -0,0 +1,328 @@
+package eu.siacs.conversations.utils;
+
+import android.os.Build;
+import android.os.Process;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.security.NoSuchAlgorithmException;
+import java.security.Provider;
+import java.security.SecureRandom;
+import java.security.SecureRandomSpi;
+import java.security.Security;
+
+import de.thedevstack.android.logcat.Logging;
+
+/**
+ * Fixes for the output of the default PRNG having low entropy.
+ *
+ * The fixes need to be applied via {@link #apply()} before any use of Java
+ * Cryptography Architecture primitives. A good place to invoke them is in the
+ * application's {@code onCreate}.
+ */
+public final class PRNGFixes {
+
+ private static final int VERSION_CODE_JELLY_BEAN = 16;
+ private static final int VERSION_CODE_JELLY_BEAN_MR2 = 18;
+ private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL = getBuildFingerprintAndDeviceSerial();
+
+ /** Hidden constructor to prevent instantiation. */
+ private PRNGFixes() {
+ }
+
+ /**
+ * Applies all fixes.
+ *
+ * @throws SecurityException
+ * if a fix is needed but could not be applied.
+ */
+ public static void apply() {
+ applyOpenSSLFix();
+ installLinuxPRNGSecureRandom();
+ }
+
+ /**
+ * Applies the fix for OpenSSL PRNG having low entropy. Does nothing if the
+ * fix is not needed.
+ *
+ * @throws SecurityException
+ * if the fix is needed but could not be applied.
+ */
+ private static void applyOpenSSLFix() throws SecurityException {
+ if ((Build.VERSION.SDK_INT < VERSION_CODE_JELLY_BEAN)
+ || (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2)) {
+ // No need to apply the fix
+ return;
+ }
+
+ try {
+ // Mix in the device- and invocation-specific seed.
+ Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto")
+ .getMethod("RAND_seed", byte[].class)
+ .invoke(null, generateSeed());
+
+ // Mix output of Linux PRNG into OpenSSL's PRNG
+ int bytesRead = (Integer) Class
+ .forName(
+ "org.apache.harmony.xnet.provider.jsse.NativeCrypto")
+ .getMethod("RAND_load_file", String.class, long.class)
+ .invoke(null, "/dev/urandom", 1024);
+ if (bytesRead != 1024) {
+ throw new IOException(
+ "Unexpected number of bytes read from Linux PRNG: "
+ + bytesRead);
+ }
+ } catch (Exception e) {
+ throw new SecurityException("Failed to seed OpenSSL PRNG", e);
+ }
+ }
+
+ /**
+ * Installs a Linux PRNG-backed {@code SecureRandom} implementation as the
+ * default. Does nothing if the implementation is already the default or if
+ * there is not need to install the implementation.
+ *
+ * @throws SecurityException
+ * if the fix is needed but could not be applied.
+ */
+ private static void installLinuxPRNGSecureRandom() throws SecurityException {
+ if (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2) {
+ // No need to apply the fix
+ return;
+ }
+
+ // Install a Linux PRNG-based SecureRandom implementation as the
+ // default, if not yet installed.
+ Provider[] secureRandomProviders = Security
+ .getProviders("SecureRandom.SHA1PRNG");
+ if ((secureRandomProviders == null)
+ || (secureRandomProviders.length < 1)
+ || (!LinuxPRNGSecureRandomProvider.class
+ .equals(secureRandomProviders[0].getClass()))) {
+ Security.insertProviderAt(new LinuxPRNGSecureRandomProvider(), 1);
+ }
+
+ // Assert that new SecureRandom() and
+ // SecureRandom.getInstance("SHA1PRNG") return a SecureRandom backed
+ // by the Linux PRNG-based SecureRandom implementation.
+ SecureRandom rng1 = new SecureRandom();
+ if (!LinuxPRNGSecureRandomProvider.class.equals(rng1.getProvider()
+ .getClass())) {
+ throw new SecurityException(
+ "new SecureRandom() backed by wrong Provider: "
+ + rng1.getProvider().getClass());
+ }
+
+ SecureRandom rng2;
+ try {
+ rng2 = SecureRandom.getInstance("SHA1PRNG");
+ } catch (NoSuchAlgorithmException e) {
+ throw new SecurityException("SHA1PRNG not available", e);
+ }
+ if (!LinuxPRNGSecureRandomProvider.class.equals(rng2.getProvider()
+ .getClass())) {
+ throw new SecurityException(
+ "SecureRandom.getInstance(\"SHA1PRNG\") backed by wrong"
+ + " Provider: " + rng2.getProvider().getClass());
+ }
+ }
+
+ /**
+ * {@code Provider} of {@code SecureRandom} engines which pass through all
+ * requests to the Linux PRNG.
+ */
+ private static class LinuxPRNGSecureRandomProvider extends Provider {
+
+ public LinuxPRNGSecureRandomProvider() {
+ super("LinuxPRNG", 1.0,
+ "A Linux-specific random number provider that uses"
+ + " /dev/urandom");
+ // Although /dev/urandom is not a SHA-1 PRNG, some apps
+ // explicitly request a SHA1PRNG SecureRandom and we thus need to
+ // prevent them from getting the default implementation whose output
+ // may have low entropy.
+ put("SecureRandom.SHA1PRNG", LinuxPRNGSecureRandom.class.getName());
+ put("SecureRandom.SHA1PRNG ImplementedIn", "Software");
+ }
+ }
+
+ /**
+ * {@link SecureRandomSpi} which passes all requests to the Linux PRNG (
+ * {@code /dev/urandom}).
+ */
+ public static class LinuxPRNGSecureRandom extends SecureRandomSpi {
+
+ /*
+ * IMPLEMENTATION NOTE: Requests to generate bytes and to mix in a seed
+ * are passed through to the Linux PRNG (/dev/urandom). Instances of
+ * this class seed themselves by mixing in the current time, PID, UID,
+ * build fingerprint, and hardware serial number (where available) into
+ * Linux PRNG.
+ *
+ * Concurrency: Read requests to the underlying Linux PRNG are
+ * serialized (on sLock) to ensure that multiple threads do not get
+ * duplicated PRNG output.
+ */
+
+ private static final File URANDOM_FILE = new File("/dev/urandom");
+
+ private static final Object sLock = new Object();
+
+ /**
+ * Input stream for reading from Linux PRNG or {@code null} if not yet
+ * opened.
+ *
+ * @GuardedBy("sLock")
+ */
+ private static DataInputStream sUrandomIn;
+
+ /**
+ * Output stream for writing to Linux PRNG or {@code null} if not yet
+ * opened.
+ *
+ * @GuardedBy("sLock")
+ */
+ private static OutputStream sUrandomOut;
+
+ /**
+ * Whether this engine instance has been seeded. This is needed because
+ * each instance needs to seed itself if the client does not explicitly
+ * seed it.
+ */
+ private boolean mSeeded;
+
+ @Override
+ protected void engineSetSeed(byte[] bytes) {
+ try {
+ OutputStream out;
+ synchronized (sLock) {
+ out = getUrandomOutputStream();
+ }
+ out.write(bytes);
+ out.flush();
+ } catch (IOException e) {
+ // On a small fraction of devices /dev/urandom is not writable.
+ // Log and ignore.
+ Logging.w(PRNGFixes.class.getSimpleName(),
+ "Failed to mix seed into " + URANDOM_FILE);
+ } finally {
+ mSeeded = true;
+ }
+ }
+
+ @Override
+ protected void engineNextBytes(byte[] bytes) {
+ if (!mSeeded) {
+ // Mix in the device- and invocation-specific seed.
+ engineSetSeed(generateSeed());
+ }
+
+ try {
+ DataInputStream in;
+ synchronized (sLock) {
+ in = getUrandomInputStream();
+ }
+ synchronized (in) {
+ in.readFully(bytes);
+ }
+ } catch (IOException e) {
+ throw new SecurityException("Failed to read from "
+ + URANDOM_FILE, e);
+ }
+ }
+
+ @Override
+ protected byte[] engineGenerateSeed(int size) {
+ byte[] seed = new byte[size];
+ engineNextBytes(seed);
+ return seed;
+ }
+
+ private DataInputStream getUrandomInputStream() {
+ synchronized (sLock) {
+ if (sUrandomIn == null) {
+ // NOTE: Consider inserting a BufferedInputStream between
+ // DataInputStream and FileInputStream if you need higher
+ // PRNG output performance and can live with future PRNG
+ // output being pulled into this process prematurely.
+ try {
+ sUrandomIn = new DataInputStream(new FileInputStream(
+ URANDOM_FILE));
+ } catch (IOException e) {
+ throw new SecurityException("Failed to open "
+ + URANDOM_FILE + " for reading", e);
+ }
+ }
+ return sUrandomIn;
+ }
+ }
+
+ private OutputStream getUrandomOutputStream() throws IOException {
+ synchronized (sLock) {
+ if (sUrandomOut == null) {
+ sUrandomOut = new FileOutputStream(URANDOM_FILE);
+ }
+ return sUrandomOut;
+ }
+ }
+ }
+
+ /**
+ * Generates a device- and invocation-specific seed to be mixed into the
+ * Linux PRNG.
+ */
+ private static byte[] generateSeed() {
+ try {
+ ByteArrayOutputStream seedBuffer = new ByteArrayOutputStream();
+ DataOutputStream seedBufferOut = new DataOutputStream(seedBuffer);
+ seedBufferOut.writeLong(System.currentTimeMillis());
+ seedBufferOut.writeLong(System.nanoTime());
+ seedBufferOut.writeInt(Process.myPid());
+ seedBufferOut.writeInt(Process.myUid());
+ seedBufferOut.write(BUILD_FINGERPRINT_AND_DEVICE_SERIAL);
+ seedBufferOut.close();
+ return seedBuffer.toByteArray();
+ } catch (IOException e) {
+ throw new SecurityException("Failed to generate seed", e);
+ }
+ }
+
+ /**
+ * Gets the hardware serial number of this device.
+ *
+ * @return serial number or {@code null} if not available.
+ */
+ private static String getDeviceSerialNumber() {
+ // We're using the Reflection API because Build.SERIAL is only available
+ // since API Level 9 (Gingerbread, Android 2.3).
+ try {
+ return (String) Build.class.getField("SERIAL").get(null);
+ } catch (Exception ignored) {
+ return null;
+ }
+ }
+
+ private static byte[] getBuildFingerprintAndDeviceSerial() {
+ StringBuilder result = new StringBuilder();
+ String fingerprint = Build.FINGERPRINT;
+ if (fingerprint != null) {
+ result.append(fingerprint);
+ }
+ String serial = getDeviceSerialNumber();
+ if (serial != null) {
+ result.append(serial);
+ }
+ try {
+ return result.toString().getBytes("UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException("UTF-8 encoding not supported");
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java b/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java
new file mode 100644
index 00000000..6c1b4bef
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java
@@ -0,0 +1,136 @@
+package eu.siacs.conversations.utils;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.Loader;
+import android.content.Loader.OnLoadCompleteListener;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Profile;
+
+import java.util.List;
+import java.util.concurrent.RejectedExecutionException;
+
+public class PhoneHelper {
+
+ public static void loadPhoneContacts(Context context, final List<Bundle> phoneContacts, final OnPhoneContactsLoadedListener listener) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
+ && context.checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
+ listener.onPhoneContactsLoaded(phoneContacts);
+ return;
+ }
+ final String[] PROJECTION = new String[]{ContactsContract.Data._ID,
+ ContactsContract.Data.DISPLAY_NAME,
+ ContactsContract.Data.PHOTO_URI,
+ ContactsContract.Data.LOOKUP_KEY,
+ ContactsContract.CommonDataKinds.Im.DATA};
+
+ final String SELECTION = "(" + ContactsContract.Data.MIMETYPE + "=\""
+ + ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE
+ + "\") AND (" + ContactsContract.CommonDataKinds.Im.PROTOCOL
+ + "=\"" + ContactsContract.CommonDataKinds.Im.PROTOCOL_JABBER
+ + "\")";
+
+ CursorLoader mCursorLoader = new NotThrowCursorLoader(context,
+ ContactsContract.Data.CONTENT_URI, PROJECTION, SELECTION, null,
+ null);
+ mCursorLoader.registerListener(0, new OnLoadCompleteListener<Cursor>() {
+
+ @Override
+ public void onLoadComplete(Loader<Cursor> arg0, Cursor cursor) {
+ if (cursor != null) {
+ while (cursor.moveToNext()) {
+ Bundle contact = new Bundle();
+ contact.putInt("phoneid", cursor.getInt(cursor
+ .getColumnIndex(ContactsContract.Data._ID)));
+ contact.putString(
+ "displayname",
+ cursor.getString(cursor
+ .getColumnIndex(ContactsContract.Data.DISPLAY_NAME)));
+ contact.putString("photouri", cursor.getString(cursor
+ .getColumnIndex(ContactsContract.Data.PHOTO_URI)));
+ contact.putString("lookup", cursor.getString(cursor
+ .getColumnIndex(ContactsContract.Data.LOOKUP_KEY)));
+
+ contact.putString(
+ "jid",
+ cursor.getString(cursor
+ .getColumnIndex(ContactsContract.CommonDataKinds.Im.DATA)));
+ phoneContacts.add(contact);
+ }
+ cursor.close();
+ }
+
+ if (listener != null) {
+ listener.onPhoneContactsLoaded(phoneContacts);
+ }
+ }
+ });
+ try {
+ mCursorLoader.startLoading();
+ } catch (RejectedExecutionException e) {
+ if (listener != null) {
+ listener.onPhoneContactsLoaded(phoneContacts);
+ }
+ }
+ }
+
+ private static class NotThrowCursorLoader extends CursorLoader {
+
+ public NotThrowCursorLoader(Context c, Uri u, String[] p, String s, String[] sa, String so) {
+ super(c, u, p, s, sa, so);
+ }
+
+ @Override
+ public Cursor loadInBackground() {
+
+ try {
+ return (super.loadInBackground());
+ } catch (SecurityException e) {
+ return(null);
+ }
+ }
+
+ }
+
+ public static Uri getSefliUri(Context context) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
+ && context.checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
+ return null;
+ }
+ String[] mProjection = new String[]{Profile._ID, Profile.PHOTO_URI};
+ Cursor mProfileCursor = context.getContentResolver().query(
+ Profile.CONTENT_URI, mProjection, null, null, null);
+
+ if (mProfileCursor == null || mProfileCursor.getCount() == 0) {
+ return null;
+ } else {
+ mProfileCursor.moveToFirst();
+ String uri = mProfileCursor.getString(1);
+ mProfileCursor.close();
+ if (uri == null) {
+ return null;
+ } else {
+ return Uri.parse(uri);
+ }
+ }
+ }
+
+ public static String getVersionName(Context context) {
+ final String packageName = context == null ? null : context.getPackageName();
+ if (packageName != null) {
+ try {
+ return context.getPackageManager().getPackageInfo(packageName, 0).versionName;
+ } catch (final PackageManager.NameNotFoundException | RuntimeException e) {
+ return "unknown";
+ }
+ } else {
+ return "unknown";
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/utils/SSLSocketHelper.java b/src/main/java/eu/siacs/conversations/utils/SSLSocketHelper.java
new file mode 100644
index 00000000..3a8c1c0a
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/SSLSocketHelper.java
@@ -0,0 +1,73 @@
+package eu.siacs.conversations.utils;
+
+import android.os.Build;
+
+import java.lang.reflect.Method;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.LinkedList;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+
+public class SSLSocketHelper {
+
+ public static void setSecurity(final SSLSocket sslSocket) throws NoSuchAlgorithmException {
+ final String[] supportProtocols;
+ final Collection<String> supportedProtocols = new LinkedList<>(
+ Arrays.asList(sslSocket.getSupportedProtocols()));
+ supportedProtocols.remove("SSLv3");
+ supportProtocols = supportedProtocols.toArray(new String[supportedProtocols.size()]);
+
+ sslSocket.setEnabledProtocols(supportProtocols);
+
+ final String[] cipherSuites = CryptoHelper.getOrderedCipherSuites(
+ sslSocket.getSupportedCipherSuites());
+ if (cipherSuites.length > 0) {
+ sslSocket.setEnabledCipherSuites(cipherSuites);
+ }
+ }
+
+ public static void setSNIHost(final SSLSocketFactory factory, final SSLSocket socket, final String hostname) {
+ if (factory instanceof android.net.SSLCertificateSocketFactory && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ ((android.net.SSLCertificateSocketFactory) factory).setHostname(socket, hostname);
+ } else {
+ try {
+ socket.getClass().getMethod("setHostname", String.class).invoke(socket, hostname);
+ } catch (Throwable e) {
+ // ignore any error, we just can't set the hostname...
+ }
+ }
+ }
+
+ public static void setAlpnProtocol(final SSLSocketFactory factory, final SSLSocket socket, final String protocol) {
+ try {
+ if (factory instanceof android.net.SSLCertificateSocketFactory && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
+ // can't call directly because of @hide?
+ //((android.net.SSLCertificateSocketFactory)factory).setAlpnProtocols(new byte[][]{protocol.getBytes("UTF-8")});
+ android.net.SSLCertificateSocketFactory.class.getMethod("setAlpnProtocols", byte[][].class).invoke(socket, new Object[]{new byte[][]{protocol.getBytes("UTF-8")}});
+ } else {
+ final Method method = socket.getClass().getMethod("setAlpnProtocols", byte[].class);
+ // the concatenation of 8-bit, length prefixed protocol names, just one in our case...
+ // http://tools.ietf.org/html/draft-agl-tls-nextprotoneg-04#page-4
+ final byte[] protocolUTF8Bytes = protocol.getBytes("UTF-8");
+ final byte[] lengthPrefixedProtocols = new byte[protocolUTF8Bytes.length + 1];
+ lengthPrefixedProtocols[0] = (byte) protocol.length(); // cannot be over 255 anyhow
+ System.arraycopy(protocolUTF8Bytes, 0, lengthPrefixedProtocols, 1, protocolUTF8Bytes.length);
+ method.invoke(socket, new Object[]{lengthPrefixedProtocols});
+ }
+ } catch (Throwable e) {
+ // ignore any error, we just can't set the alpn protocol...
+ }
+ }
+
+ public static SSLContext getSSLContext() throws NoSuchAlgorithmException {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ return SSLContext.getInstance("TLSv1.2");
+ } else {
+ return SSLContext.getInstance("TLS");
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/utils/SerialSingleThreadExecutor.java b/src/main/java/eu/siacs/conversations/utils/SerialSingleThreadExecutor.java
new file mode 100644
index 00000000..bfb4668d
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/SerialSingleThreadExecutor.java
@@ -0,0 +1,34 @@
+package eu.siacs.conversations.utils;
+
+import java.util.ArrayDeque;
+import java.util.Queue;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+public class SerialSingleThreadExecutor implements Executor {
+
+ final Executor executor = Executors.newSingleThreadExecutor();
+ final Queue<Runnable> tasks = new ArrayDeque();
+ Runnable active;
+
+ public synchronized void execute(final Runnable r) {
+ tasks.offer(new Runnable() {
+ public void run() {
+ try {
+ r.run();
+ } finally {
+ scheduleNext();
+ }
+ }
+ });
+ if (active == null) {
+ scheduleNext();
+ }
+ }
+
+ protected synchronized void scheduleNext() {
+ if ((active = tasks.poll()) != null) {
+ executor.execute(active);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/eu/siacs/conversations/utils/SocksSocketFactory.java b/src/main/java/eu/siacs/conversations/utils/SocksSocketFactory.java
new file mode 100644
index 00000000..04cfa2eb
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/SocksSocketFactory.java
@@ -0,0 +1,53 @@
+package eu.siacs.conversations.utils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.nio.ByteBuffer;
+
+import eu.siacs.conversations.Config;
+
+public class SocksSocketFactory {
+
+ public static void createSocksConnection(Socket socket, String destination, int port) throws IOException {
+ InputStream proxyIs = socket.getInputStream();
+ OutputStream proxyOs = socket.getOutputStream();
+ proxyOs.write(new byte[]{0x05, 0x01, 0x00});
+ byte[] response = new byte[2];
+ proxyIs.read(response);
+ byte[] dest = destination.getBytes();
+ ByteBuffer request = ByteBuffer.allocate(7 + dest.length);
+ request.put(new byte[]{0x05, 0x01, 0x00, 0x03});
+ request.put((byte) dest.length);
+ request.put(dest);
+ request.putShort((short) port);
+ proxyOs.write(request.array());
+ response = new byte[7 + dest.length];
+ proxyIs.read(response);
+ if (response[1] != 0x00) {
+ throw new SocksConnectionException();
+ }
+ }
+
+ public static Socket createSocket(InetSocketAddress address, String destination, int port) throws IOException {
+ Socket socket = new Socket();
+ try {
+ socket.connect(address, Config.CONNECT_TIMEOUT * 1000);
+ } catch (IOException e) {
+ throw new SocksProxyNotFoundException();
+ }
+ createSocksConnection(socket, destination, port);
+ return socket;
+ }
+
+ static class SocksConnectionException extends IOException {
+
+ }
+
+ public static class SocksProxyNotFoundException extends IOException {
+
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/utils/UIHelper.java b/src/main/java/eu/siacs/conversations/utils/UIHelper.java
new file mode 100644
index 00000000..d9cdd34c
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/UIHelper.java
@@ -0,0 +1,323 @@
+package eu.siacs.conversations.utils;
+
+import android.content.Context;
+import android.text.format.DateFormat;
+import android.text.format.DateUtils;
+import android.util.Pair;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Locale;
+
+import de.thedevstack.conversationsplus.ConversationsPlusColors;
+
+import de.thedevstack.conversationsplus.entities.FileParams;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.Presence;
+import eu.siacs.conversations.entities.Transferable;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+public class UIHelper {
+
+ private static final ArrayList<String> LOCATION_QUESTIONS = new ArrayList<>(Arrays.asList(
+ "where are you", //en
+ "where are you now", //en
+ "where are you right now", //en
+ "whats your 20", //en
+ "what is your 20", //en
+ "what's your 20", //en
+ "whats your twenty", //en
+ "what is your twenty", //en
+ "what's your twenty", //en
+ "wo bist du", //de
+ "wo bist du jetzt", //de
+ "wo bist du gerade", //de
+ "wo seid ihr", //de
+ "wo seid ihr jetzt", //de
+ "wo seid ihr gerade", //de
+ "dónde estás", //es
+ "donde estas" //es
+ ));
+
+ private static final int SHORT_DATE_FLAGS = DateUtils.FORMAT_SHOW_DATE
+ | DateUtils.FORMAT_NO_YEAR | DateUtils.FORMAT_ABBREV_ALL;
+ private static final int FULL_DATE_FLAGS = DateUtils.FORMAT_SHOW_TIME
+ | DateUtils.FORMAT_ABBREV_ALL | DateUtils.FORMAT_SHOW_DATE;
+
+ public static String readableTimeDifference(Context context, long time) {
+ return readableTimeDifference(context, time, false);
+ }
+
+ public static String readableTimeDifferenceFull(Context context, long time) {
+ return readableTimeDifference(context, time, true);
+ }
+
+ private static String readableTimeDifference(Context context, long time,
+ boolean fullDate) {
+ if (time == 0) {
+ return context.getString(R.string.just_now);
+ }
+ Date date = new Date(time);
+ long difference = (System.currentTimeMillis() - time) / 1000;
+ if (difference < 60) {
+ return context.getString(R.string.just_now);
+ } else if (difference < 60 * 2) {
+ return context.getString(R.string.minute_ago);
+ } else if (difference < 60 * 15) {
+ return context.getString(R.string.minutes_ago,
+ Math.round(difference / 60.0));
+ } else if (today(date)) {
+ java.text.DateFormat df = DateFormat.getTimeFormat(context);
+ return df.format(date);
+ } else {
+ if (fullDate) {
+ return DateUtils.formatDateTime(context, date.getTime(),
+ FULL_DATE_FLAGS);
+ } else {
+ return DateUtils.formatDateTime(context, date.getTime(),
+ SHORT_DATE_FLAGS);
+ }
+ }
+ }
+
+ private static boolean today(Date date) {
+ return sameDay(date,new Date(System.currentTimeMillis()));
+ }
+
+ public static boolean sameDay(long timestamp1, long timestamp2) {
+ return sameDay(new Date(timestamp1),new Date(timestamp2));
+ }
+
+ private static boolean sameDay(Date a, Date b) {
+ Calendar cal1 = Calendar.getInstance();
+ Calendar cal2 = Calendar.getInstance();
+ cal1.setTime(a);
+ cal2.setTime(b);
+ return cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR)
+ && cal1.get(Calendar.DAY_OF_YEAR) == cal2
+ .get(Calendar.DAY_OF_YEAR);
+ }
+
+ public static String lastseen(Context context, long time) {
+ if (time == 0) {
+ return context.getString(R.string.never_seen);
+ }
+ long difference = (System.currentTimeMillis() - time) / 1000;
+ if (difference < 60) {
+ return context.getString(R.string.last_seen_now);
+ } else if (difference < 60 * 2) {
+ return context.getString(R.string.last_seen_min);
+ } else if (difference < 60 * 60) {
+ return context.getString(R.string.last_seen_mins,
+ Math.round(difference / 60.0));
+ } else if (difference < 60 * 60 * 2) {
+ return context.getString(R.string.last_seen_hour);
+ } else if (difference < 60 * 60 * 24) {
+ return context.getString(R.string.last_seen_hours,
+ Math.round(difference / (60.0 * 60.0)));
+ } else if (difference < 60 * 60 * 48) {
+ return context.getString(R.string.last_seen_day);
+ } else {
+ return context.getString(R.string.last_seen_days,
+ Math.round(difference / (60.0 * 60.0 * 24.0)));
+ }
+ }
+
+ public static int getColorForName(String name) {
+ if (name == null || name.isEmpty()) {
+ return 0xFF202020;
+ }
+ int colors[] = {0xFFe91e63, 0xFF9c27b0, 0xFF673ab7, 0xFF3f51b5,
+ 0xFF5677fc, 0xFF03a9f4, 0xFF00bcd4, 0xFF009688, 0xFFff5722,
+ 0xFF795548, 0xFF607d8b};
+ return colors[(int) ((name.hashCode() & 0xffffffffl) % colors.length)];
+ }
+
+ public static Pair<String,Boolean> getMessagePreview(final Context context, final Message message) {
+ final Transferable d = message.getTransferable();
+ if (d != null ) {
+ switch (d.getStatus()) {
+ case Transferable.STATUS_CHECKING:
+ return new Pair<>(context.getString(R.string.checking_x,
+ getFileDescriptionString(context,message)),true);
+ case Transferable.STATUS_DOWNLOADING:
+ return new Pair<>(context.getString(R.string.receiving_x_file,
+ getFileDescriptionString(context,message),
+ d.getProgress()),true);
+ case Transferable.STATUS_OFFER:
+ case Transferable.STATUS_OFFER_CHECK_FILESIZE:
+ return new Pair<>(context.getString(R.string.x_file_offered_for_download,
+ getFileDescriptionString(context,message)),true);
+ case Transferable.STATUS_DELETED:
+ return new Pair<>(context.getString(R.string.file_deleted),true);
+ case Transferable.STATUS_FAILED:
+ return new Pair<>(context.getString(R.string.file_transmission_failed),true);
+ case Transferable.STATUS_UPLOADING:
+ if (message.getStatus() == Message.STATUS_OFFERED) {
+ return new Pair<>(context.getString(R.string.offering_x_file,
+ getFileDescriptionString(context, message)), true);
+ } else {
+ return new Pair<>(context.getString(R.string.sending_x_file,
+ getFileDescriptionString(context, message)), true);
+ }
+ default:
+ return new Pair<>("",false);
+ }
+ } else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
+ return new Pair<>(context.getString(R.string.pgp_message),true);
+ } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) {
+ return new Pair<>(context.getString(R.string.decryption_failed), true);
+ } else if (message.getType() == Message.TYPE_FILE || message.getType() == Message.TYPE_IMAGE) {
+ if (message.getStatus() == Message.STATUS_RECEIVED) {
+ return new Pair<>(context.getString(R.string.received_x_file,
+ getFileDescriptionString(context, message)), true);
+ } else {
+ return new Pair<>(getFileDescriptionString(context,message),true);
+ }
+ } else {
+ if (message.hasMeCommand()) {
+ return new Pair<>(message.getBodyReplacedMeCommand(UIHelper.getMessageDisplayName(message)), false);
+ } else if (GeoHelper.isGeoUri(message.getBody())) {
+ if (message.getStatus() == Message.STATUS_RECEIVED) {
+ return new Pair<>(context.getString(R.string.received_location), true);
+ } else {
+ return new Pair<>(context.getString(R.string.location), true);
+ }
+ } else if (message.treatAsDownloadable() == Message.Decision.MUST) {
+ return new Pair<>(context.getString(R.string.x_file_offered_for_download,
+ getFileDescriptionString(context,message)),true);
+ } else{
+ return new Pair<>(message.getBody(), false);
+ }
+ }
+ }
+
+ public static String getFileDescriptionString(final Context context, final Message message) {
+ if (message.getType() == Message.TYPE_IMAGE) {
+ return context.getString(R.string.image);
+ }
+ final String mime = message.getMimeType();
+ if (mime == null) {
+ return context.getString(R.string.file);
+ } else if (mime.startsWith("audio/")) {
+ return context.getString(R.string.audio);
+ } else if(mime.startsWith("video/")) {
+ return context.getString(R.string.video);
+ } else if (mime.startsWith("image/")) {
+ return context.getString(R.string.image);
+ } else if (mime.contains("pdf")) {
+ return context.getString(R.string.pdf_document);
+ } else if (mime.contains("application/vnd.android.package-archive")) {
+ return context.getString(R.string.apk);
+ } else if (mime.contains("vcard")) {
+ return context.getString(R.string.vcard);
+ } else {
+ String filename = getDisplayFilename(message);
+ if (null == filename) {
+ return context.getString(R.string.file);
+ } else {
+ return filename;
+ }
+ }
+ }
+
+ public static String getDisplayFilename(final Message message) {
+ String originalFilename = (null != message.getFileParams()) ? message.getFileParams().getOriginalFilename() : null;
+ return (null != originalFilename) ? originalFilename : message.getRelativeFilePath();
+ }
+
+ public static String getMessageDisplayName(final Message message) {
+ final Conversation conversation = message.getConversation();
+ if (message.getStatus() == Message.STATUS_RECEIVED) {
+ final Contact contact = message.getContact();
+ if (conversation.getMode() == Conversation.MODE_MULTI) {
+ if (contact != null) {
+ return contact.getDisplayName();
+ } else {
+ return getDisplayedMucCounterpart(message.getCounterpart());
+ }
+ } else {
+ return contact != null ? contact.getDisplayName() : "";
+ }
+ } else {
+ if (conversation.getMode() == Conversation.MODE_MULTI) {
+ return conversation.getMucOptions().getSelf().getName();
+ } else {
+ final Jid jid = conversation.getAccount().getJid();
+ return jid.hasLocalpart() ? jid.getLocalpart() : jid.toDomainJid().toString();
+ }
+ }
+ }
+
+ public static int getStatusColor(Presence.Status status) {
+ switch (status) {
+ case ONLINE:
+ return ConversationsPlusColors.online();
+ case CHAT:
+ return ConversationsPlusColors.chat();
+ case AWAY:
+ return ConversationsPlusColors.away();
+ case XA:
+ return ConversationsPlusColors.xa();
+ case DND:
+ return ConversationsPlusColors.dnd();
+ }
+ return ConversationsPlusColors.offline();
+ }
+
+ private static String getDisplayedMucCounterpart(final Jid counterpart) {
+ if (counterpart==null) {
+ return "";
+ } else if (!counterpart.isBareJid()) {
+ return counterpart.getResourcepart();
+ } else {
+ return counterpart.toString();
+ }
+ }
+
+ public static boolean receivedLocationQuestion(Message message) {
+ if (message == null
+ || message.getStatus() != Message.STATUS_RECEIVED
+ || message.getType() != Message.TYPE_TEXT) {
+ return false;
+ }
+ String body = message.getBody() == null ? null : message.getBody().toLowerCase(Locale.getDefault());
+ body = body.replace("?","").replace("¿","");
+ return LOCATION_QUESTIONS.contains(body);
+ }
+
+ public static String getHumanReadableFileSize(long size) {
+ String filesize;
+ if (size > (1.5 * 1024 * 1024)) {
+ filesize = size / (1024 * 1024)+ " MiB";
+ } else if (size > 0) {
+ filesize = size / 1024 + " KiB";
+ } else {
+ filesize = null;
+ }
+
+ return filesize;
+ }
+
+ public static String getHumanReadableDetailedFileSize(long filesize) {
+ if (0 > filesize) {
+ return "?";
+ }
+ double size = Double.valueOf(filesize);
+ String[] sizes = {" bytes", " Kb", " Mb", " Gb", " Tb"};
+ int i = 0;
+ while (1023 < size) {
+ size /= 1024d;
+ ++i;
+ }
+ BigDecimal readableSize = new BigDecimal(size);
+ readableSize = readableSize.setScale(2, BigDecimal.ROUND_HALF_UP);
+ return readableSize.doubleValue() + sizes[i];
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/utils/XmlHelper.java b/src/main/java/eu/siacs/conversations/utils/XmlHelper.java
new file mode 100644
index 00000000..4dee07cf
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/XmlHelper.java
@@ -0,0 +1,12 @@
+package eu.siacs.conversations.utils;
+
+public class XmlHelper {
+ public static String encodeEntities(String content) {
+ content = content.replace("&", "&amp;");
+ content = content.replace("<", "&lt;");
+ content = content.replace(">", "&gt;");
+ content = content.replace("\"", "&quot;");
+ content = content.replace("'", "&apos;");
+ return content;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/utils/Xmlns.java b/src/main/java/eu/siacs/conversations/utils/Xmlns.java
new file mode 100644
index 00000000..ad30b3e6
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/Xmlns.java
@@ -0,0 +1,11 @@
+package eu.siacs.conversations.utils;
+
+import eu.siacs.conversations.Config;
+
+public final class Xmlns {
+ public static final String BLOCKING = "urn:xmpp:blocking";
+ public static final String ROSTER = "jabber:iq:roster";
+ public static final String REGISTER = "jabber:iq:register";
+ public static final String BYTE_STREAMS = "http://jabber.org/protocol/bytestreams";
+ public static final String HTTP_UPLOAD = "urn:xmpp:http:upload";
+}
diff --git a/src/main/java/eu/siacs/conversations/utils/XmppUri.java b/src/main/java/eu/siacs/conversations/utils/XmppUri.java
new file mode 100644
index 00000000..92c0241e
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/XmppUri.java
@@ -0,0 +1,85 @@
+package eu.siacs.conversations.utils;
+
+import android.net.Uri;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+
+import eu.siacs.conversations.xmpp.jid.InvalidJidException;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+public class XmppUri {
+
+ protected String jid;
+ protected boolean muc;
+ protected String fingerprint;
+
+ public XmppUri(String uri) {
+ try {
+ parse(Uri.parse(uri));
+ } catch (IllegalArgumentException e) {
+ try {
+ jid = Jid.fromString(uri).toBareJid().toString();
+ } catch (InvalidJidException e2) {
+ jid = null;
+ }
+ }
+ }
+
+ public XmppUri(Uri uri) {
+ parse(uri);
+ }
+
+ protected void parse(Uri uri) {
+ String scheme = uri.getScheme();
+ if ("xmpp".equalsIgnoreCase(scheme)) {
+ // sample: xmpp:jid@foo.com
+ muc = "join".equalsIgnoreCase(uri.getQuery());
+ if (uri.getAuthority() != null) {
+ jid = uri.getAuthority();
+ } else {
+ jid = uri.getSchemeSpecificPart().split("\\?")[0];
+ }
+ fingerprint = parseFingerprint(uri.getQuery());
+ } else if ("imto".equalsIgnoreCase(scheme)) {
+ // sample: imto://xmpp/jid@foo.com
+ try {
+ jid = URLDecoder.decode(uri.getEncodedPath(), "UTF-8").split("/")[1];
+ } catch (final UnsupportedEncodingException ignored) {
+ jid = null;
+ }
+ } else {
+ try {
+ jid = Jid.fromString(uri.toString()).toBareJid().toString();
+ } catch (final InvalidJidException ignored) {
+ jid = null;
+ }
+ }
+ }
+
+ protected String parseFingerprint(String query) {
+ if (query == null) {
+ return null;
+ } else {
+ final String NEEDLE = "otr-fingerprint=";
+ int index = query.indexOf(NEEDLE);
+ if (index >= 0 && query.length() >= (NEEDLE.length() + index + 40)) {
+ return query.substring(index + NEEDLE.length(), index + NEEDLE.length() + 40);
+ } else {
+ return null;
+ }
+ }
+ }
+
+ public Jid getJid() {
+ try {
+ return this.jid == null ? null :Jid.fromString(this.jid.toLowerCase());
+ } catch (InvalidJidException e) {
+ return null;
+ }
+ }
+
+ public String getFingerprint() {
+ return this.fingerprint;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xml/Element.java b/src/main/java/eu/siacs/conversations/xml/Element.java
new file mode 100644
index 00000000..9152c679
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xml/Element.java
@@ -0,0 +1,188 @@
+package eu.siacs.conversations.xml;
+
+import java.util.ArrayList;
+import java.util.Hashtable;
+import java.util.List;
+
+import de.thedevstack.android.logcat.Logging;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.utils.XmlHelper;
+import eu.siacs.conversations.xmpp.jid.InvalidJidException;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+public class Element {
+ private final String name;
+ private Hashtable<String, String> attributes = new Hashtable<>();
+ private String content;
+ protected List<Element> children = new ArrayList<>();
+
+ public Element(String name) {
+ this.name = name;
+ }
+
+ public Element(String name, String xmlns) {
+ this.name = name;
+ this.setAttribute("xmlns", xmlns);
+ }
+
+ public Element addChild(Element child) {
+ this.content = null;
+ children.add(child);
+ return child;
+ }
+
+ public Element addChild(String name) {
+ this.content = null;
+ Element child = new Element(name);
+ children.add(child);
+ return child;
+ }
+
+ public Element addChild(String name, String xmlns) {
+ this.content = null;
+ Element child = new Element(name);
+ child.setAttribute("xmlns", xmlns);
+ children.add(child);
+ return child;
+ }
+
+ public Element setContent(String content) {
+ this.content = content;
+ this.children.clear();
+ return this;
+ }
+
+ public Element findChild(String name) {
+ for (Element child : this.children) {
+ if (child.getName().equals(name)) {
+ return child;
+ }
+ }
+ return null;
+ }
+
+ public String findChildContent(String name) {
+ Element element = findChild(name);
+ return element == null ? null : element.getContent();
+ }
+
+ public Element findChild(String name, String xmlns) {
+ for (Element child : this.children) {
+ if (name.equals(child.getName()) && xmlns.equals(child.getAttribute("xmlns"))) {
+ return child;
+ }
+ }
+ return null;
+ }
+
+ public String findChildContent(String name, String xmlns) {
+ Element element = findChild(name,xmlns);
+ return element == null ? null : element.getContent();
+ }
+
+ public boolean hasChild(final String name) {
+ return findChild(name) != null;
+ }
+
+ public boolean hasChild(final String name, final String xmlns) {
+ return findChild(name, xmlns) != null;
+ }
+
+ public List<Element> getChildren() {
+ return this.children;
+ }
+
+ public Element setChildren(List<Element> children) {
+ this.children = children;
+ return this;
+ }
+
+ public final String getContent() {
+ return content;
+ }
+
+ public Element setAttribute(String name, String value) {
+ if (name != null && value != null) {
+ this.attributes.put(name, value);
+ }
+ return this;
+ }
+
+ public Element setAttributes(Hashtable<String, String> attributes) {
+ this.attributes = attributes;
+ return this;
+ }
+
+ public String getAttribute(String name) {
+ if (this.attributes.containsKey(name)) {
+ return this.attributes.get(name);
+ } else {
+ return null;
+ }
+ }
+
+ public Jid getAttributeAsJid(String name) {
+ final String jid = this.getAttribute(name);
+ if (jid != null && !jid.isEmpty()) {
+ try {
+ return Jid.fromString(jid);
+ } catch (final InvalidJidException e) {
+ Logging.e(Config.LOGTAG, "could not parse jid " + jid);
+ return null;
+ }
+ }
+ return null;
+ }
+
+ public Hashtable<String, String> getAttributes() {
+ return this.attributes;
+ }
+
+ public String toString() {
+ StringBuilder elementOutput = new StringBuilder();
+ if ((content == null) && (children.size() == 0)) {
+ Tag emptyTag = Tag.empty(name);
+ emptyTag.setAtttributes(this.attributes);
+ elementOutput.append(emptyTag.toString());
+ } else {
+ Tag startTag = Tag.start(name);
+ startTag.setAtttributes(this.attributes);
+ elementOutput.append(startTag);
+ if (content != null) {
+ elementOutput.append(XmlHelper.encodeEntities(content));
+ } else {
+ for (Element child : children) {
+ elementOutput.append(child.toString());
+ }
+ }
+ Tag endTag = Tag.end(name);
+ elementOutput.append(endTag);
+ }
+ return elementOutput.toString();
+ }
+
+ public final String getName() {
+ return name;
+ }
+
+ public void clearChildren() {
+ this.children.clear();
+ }
+
+ public void setAttribute(String name, long value) {
+ this.setAttribute(name, Long.toString(value));
+ }
+
+ public void setAttribute(String name, int value) {
+ this.setAttribute(name, Integer.toString(value));
+ }
+
+ public boolean getAttributeAsBoolean(String name) {
+ String attr = getAttribute(name);
+ return (attr != null && (attr.equalsIgnoreCase("true") || attr.equalsIgnoreCase("1")));
+ }
+
+ public String getNamespace() {
+ return getAttribute("xmlns");
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xml/Tag.java b/src/main/java/eu/siacs/conversations/xml/Tag.java
new file mode 100644
index 00000000..b9ef979f
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xml/Tag.java
@@ -0,0 +1,104 @@
+package eu.siacs.conversations.xml;
+
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import eu.siacs.conversations.utils.XmlHelper;
+
+public class Tag {
+ public static final int NO = -1;
+ public static final int START = 0;
+ public static final int END = 1;
+ public static final int EMPTY = 2;
+
+ protected int type;
+ protected String name;
+ protected Hashtable<String, String> attributes = new Hashtable<String, String>();
+
+ protected Tag(int type, String name) {
+ this.type = type;
+ this.name = name;
+ }
+
+ public static Tag no(String text) {
+ return new Tag(NO, text);
+ }
+
+ public static Tag start(String name) {
+ return new Tag(START, name);
+ }
+
+ public static Tag end(String name) {
+ return new Tag(END, name);
+ }
+
+ public static Tag empty(String name) {
+ return new Tag(EMPTY, name);
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getAttribute(String attrName) {
+ return this.attributes.get(attrName);
+ }
+
+ public Tag setAttribute(String attrName, String attrValue) {
+ this.attributes.put(attrName, attrValue);
+ return this;
+ }
+
+ public Tag setAtttributes(Hashtable<String, String> attributes) {
+ this.attributes = attributes;
+ return this;
+ }
+
+ public boolean isStart(String needle) {
+ if (needle == null)
+ return false;
+ return (this.type == START) && (needle.equals(this.name));
+ }
+
+ public boolean isEnd(String needle) {
+ if (needle == null)
+ return false;
+ return (this.type == END) && (needle.equals(this.name));
+ }
+
+ public boolean isNo() {
+ return (this.type == NO);
+ }
+
+ public String toString() {
+ StringBuilder tagOutput = new StringBuilder();
+ tagOutput.append('<');
+ if (type == END) {
+ tagOutput.append('/');
+ }
+ tagOutput.append(name);
+ if (type != END) {
+ Set<Entry<String, String>> attributeSet = attributes.entrySet();
+ Iterator<Entry<String, String>> it = attributeSet.iterator();
+ while (it.hasNext()) {
+ Entry<String, String> entry = it.next();
+ tagOutput.append(' ');
+ tagOutput.append(entry.getKey());
+ tagOutput.append("=\"");
+ tagOutput.append(XmlHelper.encodeEntities(entry.getValue()));
+ tagOutput.append('"');
+ }
+ }
+ if (type == EMPTY) {
+ tagOutput.append('/');
+ }
+ tagOutput.append('>');
+ return tagOutput.toString();
+ }
+
+ public Hashtable<String, String> getAttributes() {
+ return this.attributes;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xml/TagWriter.java b/src/main/java/eu/siacs/conversations/xml/TagWriter.java
new file mode 100644
index 00000000..f11c1846
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xml/TagWriter.java
@@ -0,0 +1,114 @@
+package eu.siacs.conversations.xml;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.util.concurrent.LinkedBlockingQueue;
+
+import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
+
+public class TagWriter {
+
+ private OutputStream plainOutputStream;
+ private OutputStreamWriter outputStream;
+ private boolean finshed = false;
+ private LinkedBlockingQueue<AbstractStanza> writeQueue = new LinkedBlockingQueue<AbstractStanza>();
+ private Thread asyncStanzaWriter = new Thread() {
+ private boolean shouldStop = false;
+
+ @Override
+ public void run() {
+ while (!shouldStop) {
+ if ((finshed) && (writeQueue.size() == 0)) {
+ return;
+ }
+ try {
+ AbstractStanza output = writeQueue.take();
+ if (outputStream == null) {
+ shouldStop = true;
+ } else {
+ outputStream.write(output.toString());
+ outputStream.flush();
+ }
+ } catch (IOException e) {
+ shouldStop = true;
+ } catch (InterruptedException e) {
+ shouldStop = true;
+ }
+ }
+ }
+ };
+
+ public TagWriter() {
+ }
+
+ public void setOutputStream(OutputStream out) throws IOException {
+ if (out == null) {
+ throw new IOException();
+ }
+ this.plainOutputStream = out;
+ this.outputStream = new OutputStreamWriter(out);
+ }
+
+ public OutputStream getOutputStream() throws IOException {
+ if (this.plainOutputStream == null) {
+ throw new IOException();
+ }
+ return this.plainOutputStream;
+ }
+
+ public TagWriter beginDocument() throws IOException {
+ if (outputStream == null) {
+ throw new IOException("output stream was null");
+ }
+ outputStream.write("<?xml version='1.0'?>");
+ outputStream.flush();
+ return this;
+ }
+
+ public TagWriter writeTag(Tag tag) throws IOException {
+ if (outputStream == null) {
+ throw new IOException("output stream was null");
+ }
+ outputStream.write(tag.toString());
+ outputStream.flush();
+ return this;
+ }
+
+ public TagWriter writeElement(Element element) throws IOException {
+ if (outputStream == null) {
+ throw new IOException("output stream was null");
+ }
+ outputStream.write(element.toString());
+ outputStream.flush();
+ return this;
+ }
+
+ public TagWriter writeStanzaAsync(AbstractStanza stanza) {
+ if (finshed) {
+ return this;
+ } else {
+ if (!asyncStanzaWriter.isAlive()) {
+ try {
+ asyncStanzaWriter.start();
+ } catch (IllegalThreadStateException e) {
+ // already started
+ }
+ }
+ writeQueue.add(stanza);
+ return this;
+ }
+ }
+
+ public void finish() {
+ this.finshed = true;
+ }
+
+ public boolean finished() {
+ return (this.writeQueue.size() == 0);
+ }
+
+ public boolean isActive() {
+ return outputStream != null;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xml/XmlReader.java b/src/main/java/eu/siacs/conversations/xml/XmlReader.java
new file mode 100644
index 00000000..74e65fcd
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xml/XmlReader.java
@@ -0,0 +1,141 @@
+package eu.siacs.conversations.xml;
+
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.util.Xml;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+import de.thedevstack.android.logcat.Logging;
+import eu.siacs.conversations.Config;
+
+public class XmlReader {
+ private XmlPullParser parser;
+ private PowerManager.WakeLock wakeLock;
+ private InputStream is;
+
+ public XmlReader(WakeLock wakeLock) {
+ this.parser = Xml.newPullParser();
+ try {
+ this.parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES,
+ true);
+ } catch (XmlPullParserException e) {
+ Logging.d(Config.LOGTAG, "error setting namespace feature on parser");
+ }
+ this.wakeLock = wakeLock;
+ }
+
+ public void setInputStream(InputStream inputStream) throws IOException {
+ if (inputStream == null) {
+ throw new IOException();
+ }
+ this.is = inputStream;
+ try {
+ parser.setInput(new InputStreamReader(this.is));
+ } catch (XmlPullParserException e) {
+ throw new IOException("error resetting parser");
+ }
+ }
+
+ public InputStream getInputStream() throws IOException {
+ if (this.is == null) {
+ throw new IOException();
+ }
+ return is;
+ }
+
+ public void reset() throws IOException {
+ if (this.is == null) {
+ throw new IOException();
+ }
+ try {
+ parser.setInput(new InputStreamReader(this.is));
+ } catch (XmlPullParserException e) {
+ throw new IOException("error resetting parser");
+ }
+ }
+
+ public Tag readTag() throws XmlPullParserException, IOException {
+ if (wakeLock.isHeld()) {
+ try {
+ wakeLock.release();
+ } catch (RuntimeException re) {
+ }
+ }
+ try {
+ while (this.is != null
+ && parser.next() != XmlPullParser.END_DOCUMENT) {
+ wakeLock.acquire();
+ if (parser.getEventType() == XmlPullParser.START_TAG) {
+ Tag tag = Tag.start(parser.getName());
+ for (int i = 0; i < parser.getAttributeCount(); ++i) {
+ tag.setAttribute(parser.getAttributeName(i),
+ parser.getAttributeValue(i));
+ }
+ String xmlns = parser.getNamespace();
+ if (xmlns != null) {
+ tag.setAttribute("xmlns", xmlns);
+ }
+ return tag;
+ } else if (parser.getEventType() == XmlPullParser.END_TAG) {
+ Tag tag = Tag.end(parser.getName());
+ return tag;
+ } else if (parser.getEventType() == XmlPullParser.TEXT) {
+ Tag tag = Tag.no(parser.getText());
+ return tag;
+ }
+ }
+ if (wakeLock.isHeld()) {
+ try {
+ wakeLock.release();
+ } catch (RuntimeException re) {
+ }
+ }
+ } catch (ArrayIndexOutOfBoundsException e) {
+ throw new IOException(
+ "xml parser mishandled ArrayIndexOufOfBounds", e);
+ } catch (StringIndexOutOfBoundsException e) {
+ throw new IOException(
+ "xml parser mishandled StringIndexOufOfBounds", e);
+ } catch (NullPointerException e) {
+ throw new IOException("xml parser mishandled NullPointerException",
+ e);
+ } catch (IndexOutOfBoundsException e) {
+ throw new IOException("xml parser mishandled IndexOutOfBound", e);
+ }
+ return null;
+ }
+
+ public Element readElement(Tag currentTag) throws XmlPullParserException,
+ IOException {
+ Element element = new Element(currentTag.getName());
+ element.setAttributes(currentTag.getAttributes());
+ Tag nextTag = this.readTag();
+ if (nextTag == null) {
+ throw new IOException("unterupted mid tag");
+ }
+ if (nextTag.isNo()) {
+ element.setContent(nextTag.getName());
+ nextTag = this.readTag();
+ if (nextTag == null) {
+ throw new IOException("unterupted mid tag");
+ }
+ }
+ while (!nextTag.isEnd(element.getName())) {
+ if (!nextTag.isNo()) {
+ Element child = this.readElement(nextTag);
+ element.addChild(child);
+ }
+ nextTag = this.readTag();
+ if (nextTag == null) {
+ throw new IOException("unterupted mid tag");
+ }
+ }
+ return element;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnAdvancedStreamFeaturesLoaded.java b/src/main/java/eu/siacs/conversations/xmpp/OnAdvancedStreamFeaturesLoaded.java
new file mode 100644
index 00000000..e45eba73
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/OnAdvancedStreamFeaturesLoaded.java
@@ -0,0 +1,7 @@
+package eu.siacs.conversations.xmpp;
+
+import eu.siacs.conversations.entities.Account;
+
+public interface OnAdvancedStreamFeaturesLoaded {
+ public void onAdvancedStreamFeaturesAvailable(final Account account);
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnBindListener.java b/src/main/java/eu/siacs/conversations/xmpp/OnBindListener.java
new file mode 100644
index 00000000..f09cf33d
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/OnBindListener.java
@@ -0,0 +1,7 @@
+package eu.siacs.conversations.xmpp;
+
+import eu.siacs.conversations.entities.Account;
+
+public interface OnBindListener {
+ public void onBind(Account account);
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnContactStatusChanged.java b/src/main/java/eu/siacs/conversations/xmpp/OnContactStatusChanged.java
new file mode 100644
index 00000000..20b17f02
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/OnContactStatusChanged.java
@@ -0,0 +1,7 @@
+package eu.siacs.conversations.xmpp;
+
+import eu.siacs.conversations.entities.Contact;
+
+public interface OnContactStatusChanged {
+ public void onContactStatusChanged(final Contact contact, final boolean online);
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnIqPacketReceived.java b/src/main/java/eu/siacs/conversations/xmpp/OnIqPacketReceived.java
new file mode 100644
index 00000000..a4cff986
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/OnIqPacketReceived.java
@@ -0,0 +1,8 @@
+package eu.siacs.conversations.xmpp;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+public interface OnIqPacketReceived extends PacketReceived {
+ public void onIqPacketReceived(Account account, IqPacket packet);
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnKeyStatusUpdated.java b/src/main/java/eu/siacs/conversations/xmpp/OnKeyStatusUpdated.java
new file mode 100644
index 00000000..e7fc582e
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/OnKeyStatusUpdated.java
@@ -0,0 +1,7 @@
+package eu.siacs.conversations.xmpp;
+
+import eu.siacs.conversations.crypto.axolotl.AxolotlService;
+
+public interface OnKeyStatusUpdated {
+ public void onKeyStatusUpdated(AxolotlService.FetchStatus report);
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnMessageAcknowledged.java b/src/main/java/eu/siacs/conversations/xmpp/OnMessageAcknowledged.java
new file mode 100644
index 00000000..5f670d93
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/OnMessageAcknowledged.java
@@ -0,0 +1,7 @@
+package eu.siacs.conversations.xmpp;
+
+import eu.siacs.conversations.entities.Account;
+
+public interface OnMessageAcknowledged {
+ public void onMessageAcknowledged(Account account, String id);
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnMessagePacketReceived.java b/src/main/java/eu/siacs/conversations/xmpp/OnMessagePacketReceived.java
new file mode 100644
index 00000000..325e945f
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/OnMessagePacketReceived.java
@@ -0,0 +1,8 @@
+package eu.siacs.conversations.xmpp;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
+
+public interface OnMessagePacketReceived extends PacketReceived {
+ public void onMessagePacketReceived(Account account, MessagePacket packet);
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnPresencePacketReceived.java b/src/main/java/eu/siacs/conversations/xmpp/OnPresencePacketReceived.java
new file mode 100644
index 00000000..95c1acfc
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/OnPresencePacketReceived.java
@@ -0,0 +1,8 @@
+package eu.siacs.conversations.xmpp;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
+
+public interface OnPresencePacketReceived extends PacketReceived {
+ public void onPresencePacketReceived(Account account, PresencePacket packet);
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnStatusChanged.java b/src/main/java/eu/siacs/conversations/xmpp/OnStatusChanged.java
new file mode 100644
index 00000000..ad1d98cb
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/OnStatusChanged.java
@@ -0,0 +1,7 @@
+package eu.siacs.conversations.xmpp;
+
+import eu.siacs.conversations.entities.Account;
+
+public interface OnStatusChanged {
+ public void onStatusChanged(Account account);
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnUpdateBlocklist.java b/src/main/java/eu/siacs/conversations/xmpp/OnUpdateBlocklist.java
new file mode 100644
index 00000000..92e72cfa
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/OnUpdateBlocklist.java
@@ -0,0 +1,13 @@
+package eu.siacs.conversations.xmpp;
+
+public interface OnUpdateBlocklist {
+ // Use an enum instead of a boolean to make sure we don't run into the boolean trap
+ // (`onUpdateBlocklist(true)' doesn't read well, and could be confusing).
+ public static enum Status {
+ BLOCKED,
+ UNBLOCKED
+ }
+
+ @SuppressWarnings("MethodNameSameAsClassName")
+ public void OnUpdateBlocklist(final Status status);
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/PacketReceived.java b/src/main/java/eu/siacs/conversations/xmpp/PacketReceived.java
new file mode 100644
index 00000000..d4502d73
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/PacketReceived.java
@@ -0,0 +1,5 @@
+package eu.siacs.conversations.xmpp;
+
+public abstract interface PacketReceived {
+
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java
new file mode 100644
index 00000000..a5d0321d
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java
@@ -0,0 +1,1574 @@
+package eu.siacs.conversations.xmpp;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.os.PowerManager.WakeLock;
+import android.os.SystemClock;
+import android.security.KeyChain;
+import android.util.Base64;
+import android.util.Log;
+import android.util.Pair;
+import android.util.SparseArray;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.math.BigInteger;
+import java.net.ConnectException;
+import java.net.IDN;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.net.URL;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.security.Principal;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.TreeSet;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.X509KeyManager;
+import javax.net.ssl.X509TrustManager;
+
+import de.duenndns.ssl.MemorizingTrustManager;
+import de.thedevstack.android.logcat.Logging;
+import de.thedevstack.conversationsplus.ConversationsPlusApplication;
+import de.thedevstack.conversationsplus.ConversationsPlusPreferences;
+import de.thedevstack.conversationsplus.dto.SrvRecord;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.crypto.XmppDomainVerifier;
+import eu.siacs.conversations.crypto.sasl.DigestMd5;
+import eu.siacs.conversations.crypto.sasl.External;
+import eu.siacs.conversations.crypto.sasl.Plain;
+import eu.siacs.conversations.crypto.sasl.SaslMechanism;
+import eu.siacs.conversations.crypto.sasl.ScramSha1;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.ServiceDiscoveryResult;
+import eu.siacs.conversations.generator.IqGenerator;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.utils.DNSHelper;
+import eu.siacs.conversations.utils.SSLSocketHelper;
+import eu.siacs.conversations.utils.SocksSocketFactory;
+import eu.siacs.conversations.utils.Xmlns;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xml.Tag;
+import eu.siacs.conversations.xml.TagWriter;
+import eu.siacs.conversations.xml.XmlReader;
+import eu.siacs.conversations.xmpp.forms.Data;
+import eu.siacs.conversations.xmpp.forms.Field;
+import eu.siacs.conversations.xmpp.jid.InvalidJidException;
+import eu.siacs.conversations.xmpp.jid.Jid;
+import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
+import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
+import eu.siacs.conversations.xmpp.stanzas.AbstractAcknowledgeableStanza;
+import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
+import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
+import eu.siacs.conversations.xmpp.stanzas.csi.ActivePacket;
+import eu.siacs.conversations.xmpp.stanzas.csi.InactivePacket;
+import eu.siacs.conversations.xmpp.stanzas.streammgmt.AckPacket;
+import eu.siacs.conversations.xmpp.stanzas.streammgmt.EnablePacket;
+import eu.siacs.conversations.xmpp.stanzas.streammgmt.RequestPacket;
+import eu.siacs.conversations.xmpp.stanzas.streammgmt.ResumePacket;
+
+public class XmppConnection implements Runnable {
+ private static final int DEFAULT_PORT = 5222;
+ private static final int PACKET_IQ = 0;
+ private static final int PACKET_MESSAGE = 1;
+ private static final int PACKET_PRESENCE = 2;
+ protected Account account;
+ private final WakeLock wakeLock;
+ private Socket socket;
+ private XmlReader tagReader;
+ private TagWriter tagWriter;
+ private final Features features = new Features(this);
+ private boolean needsBinding = true;
+ private boolean shouldAuthenticate = true;
+ private Element streamFeatures;
+ private final HashMap<Jid, ServiceDiscoveryResult> disco = new HashMap<>();
+
+ private String streamId = null;
+ private int smVersion = 3;
+ private final SparseArray<AbstractAcknowledgeableStanza> mStanzaQueue = new SparseArray<>();
+
+ private int stanzasReceived = 0;
+ private int stanzasSent = 0;
+ private long lastPacketReceived = 0;
+ private long lastPingSent = 0;
+ private long lastConnect = 0;
+ private long lastSessionStarted = 0;
+ private long lastDiscoStarted = 0;
+ private AtomicInteger mPendingServiceDiscoveries = new AtomicInteger(0);
+ private AtomicBoolean mIsServiceItemsDiscoveryPending = new AtomicBoolean(true);
+ private boolean mWaitForDisco = true;
+ private final ArrayList<String> mPendingServiceDiscoveriesIds = new ArrayList<>();
+ private boolean mInteractive = false;
+ private int attempt = 0;
+ private final Hashtable<String, Pair<IqPacket, OnIqPacketReceived>> packetCallbacks = new Hashtable<>();
+ private OnPresencePacketReceived presenceListener = null;
+ private OnJinglePacketReceived jingleListener = null;
+ private OnIqPacketReceived unregisteredIqListener = null;
+ private OnMessagePacketReceived messageListener = null;
+ private OnStatusChanged statusListener = null;
+ private OnBindListener bindListener = null;
+ private final ArrayList<OnAdvancedStreamFeaturesLoaded> advancedStreamFeaturesLoadedListeners = new ArrayList<>();
+ private OnMessageAcknowledged acknowledgedListener = null;
+ private XmppConnectionService mXmppConnectionService = null;
+
+ private SaslMechanism saslMechanism;
+
+ private X509KeyManager mKeyManager = new X509KeyManager() {
+ @Override
+ public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) {
+ return account.getPrivateKeyAlias();
+ }
+
+ @Override
+ public String chooseServerAlias(String s, Principal[] principals, Socket socket) {
+ return null;
+ }
+
+ @Override
+ public X509Certificate[] getCertificateChain(String alias) {
+ try {
+ return KeyChain.getCertificateChain(mXmppConnectionService, alias);
+ } catch (Exception e) {
+ return new X509Certificate[0];
+ }
+ }
+
+ @Override
+ public String[] getClientAliases(String s, Principal[] principals) {
+ return new String[0];
+ }
+
+ @Override
+ public String[] getServerAliases(String s, Principal[] principals) {
+ return new String[0];
+ }
+
+ @Override
+ public PrivateKey getPrivateKey(String alias) {
+ try {
+ return KeyChain.getPrivateKey(mXmppConnectionService, alias);
+ } catch (Exception e) {
+ return null;
+ }
+ }
+ };
+ private Identity mServerIdentity = Identity.UNKNOWN;
+
+ private OnIqPacketReceived createPacketReceiveHandler() {
+ return new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE.RESULT) {
+ account.setOption(Account.OPTION_REGISTER,
+ false);
+ changeStatus(Account.State.REGISTRATION_SUCCESSFUL);
+ } else if (packet.hasChild("error")
+ && (packet.findChild("error")
+ .hasChild("conflict"))) {
+ changeStatus(Account.State.REGISTRATION_CONFLICT);
+ } else {
+ changeStatus(Account.State.REGISTRATION_FAILED);
+ Log.d(Config.LOGTAG, packet.toString());
+ }
+ disconnect(true);
+ }
+ };
+ }
+
+ public XmppConnection(final Account account, final XmppConnectionService service) {
+ this.account = account;
+ this.wakeLock = ConversationsPlusApplication.createPartialWakeLock(account.getJid().toBareJid().toString());
+ tagWriter = new TagWriter();
+ mXmppConnectionService = service;
+ }
+
+ protected void changeStatus(final Account.State nextStatus) {
+ if (account.getStatus() != nextStatus) {
+ if ((nextStatus == Account.State.OFFLINE)
+ && (account.getStatus() != Account.State.CONNECTING)
+ && (account.getStatus() != Account.State.ONLINE)
+ && (account.getStatus() != Account.State.DISABLED)) {
+ return;
+ }
+ if (nextStatus == Account.State.ONLINE) {
+ this.attempt = 0;
+ }
+ account.setStatus(nextStatus);
+ if (statusListener != null) {
+ statusListener.onStatusChanged(account);
+ }
+ }
+ }
+
+ public void prepareNewConnection() {
+ this.lastConnect = SystemClock.elapsedRealtime();
+ this.lastPingSent = SystemClock.elapsedRealtime();
+ this.lastDiscoStarted = Long.MAX_VALUE;
+ this.changeStatus(Account.State.CONNECTING);
+ }
+
+ protected void connect() {
+ Logging.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": connecting");
+ features.encryptionEnabled = false;
+ 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 {
+ shouldAuthenticate = needsBinding = !account.isOptionSet(Account.OPTION_REGISTER);
+ tagReader = new XmlReader(wakeLock);
+ tagWriter = new TagWriter();
+ this.changeStatus(Account.State.CONNECTING);
+ final boolean extended = ConversationsPlusPreferences.showConnectionOptions();
+ if (extended && account.getHostname() != null && !account.getHostname().isEmpty()) {
+ socket = new Socket();
+ try {
+ socket.connect(new InetSocketAddress(account.getHostname(), account.getPort()), Config.SOCKET_TIMEOUT * 1000);
+ } catch (IOException e) {
+ throw new UnknownHostException();
+ }
+ startXmpp();
+ } else if (DNSHelper.isIp(account.getServer().toString())) {
+ socket = new Socket();
+ try {
+ socket.connect(new InetSocketAddress(account.getServer().toString(), DEFAULT_PORT), Config.SOCKET_TIMEOUT * 1000);
+ } catch (IOException e) {
+ throw new UnknownHostException();
+ }
+ startXmpp();
+ } else {
+ final TreeSet<SrvRecord> srvRecords = DNSHelper.querySrvRecord(account.getServer());
+ if (srvRecords.isEmpty()) {
+ socket = new Socket();
+ try {
+ socket.connect(new InetSocketAddress(account.getServer().getDomainpart(), DEFAULT_PORT), Config.SOCKET_TIMEOUT * 1000);
+ } catch (IOException e) {
+ throw new UnknownHostException();
+ }
+ startXmpp();
+ } else {
+ for (SrvRecord srvRecord : srvRecords) {
+ // if tls is true, encryption is implied and must not be started
+ features.encryptionEnabled = srvRecord.isUseTls();
+ TlsFactoryVerifier tlsFactoryVerifier = null;
+ if (features.encryptionEnabled) {
+ try {
+ tlsFactoryVerifier = getTlsFactoryVerifier();
+ socket = tlsFactoryVerifier.factory.createSocket();
+
+ if (socket == null) {
+ throw new IOException("could not initialize ssl socket");
+ }
+
+ SSLSocketHelper.setSecurity((SSLSocket) socket);
+ SSLSocketHelper.setSNIHost(tlsFactoryVerifier.factory, (SSLSocket) socket, account.getServer().getDomainpart());
+ SSLSocketHelper.setAlpnProtocol(tlsFactoryVerifier.factory, (SSLSocket) socket, "xmpp-client");
+ } catch (SecurityException e) {
+ throw e;
+ } catch (KeyManagementException e) {
+ Logging.e("connection-init", "Error while creating TLS verifier factory: " + e.getMessage(), e);
+ throw new SecurityException();
+ }
+ } else {
+ socket = new Socket();
+ }
+
+ socket.connect(new InetSocketAddress(srvRecord.getName(), srvRecord.getPort()), Config.SOCKET_TIMEOUT * 1000);
+
+ if (null != tlsFactoryVerifier && !tlsFactoryVerifier.verifier.verify(account.getServer().getDomainpart(), ((SSLSocket) socket).getSession())) {
+ Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": TLS certificate verification failed");
+ throw new SecurityException();
+ }
+
+ if (startXmpp()) {
+ break; // successfully connected to server that speaks xmpp
+ }
+ }
+ }
+ }
+ processStream();
+ } catch (final IncompatibleServerException e) {
+ this.changeStatus(Account.State.INCOMPATIBLE_SERVER);
+ } catch (final SecurityException e) {
+ this.changeStatus(Account.State.SECURITY_ERROR);
+ } catch (final UnauthorizedException e) {
+ this.changeStatus(Account.State.UNAUTHORIZED);
+ } catch (final UnknownHostException | ConnectException e) {
+ this.changeStatus(Account.State.SERVER_NOT_FOUND);
+ } catch (final IOException | XmlPullParserException | NoSuchAlgorithmException e) {
+ Logging.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage());
+ this.changeStatus(Account.State.OFFLINE);
+ this.attempt--; //don't count attempt when reconnecting instantly anyway
+ } finally {
+ forceCloseSocket();
+ if (wakeLock.isHeld()) {
+ try {
+ wakeLock.release();
+ } catch (final RuntimeException ignored) {
+ }
+ }
+ }
+ }
+
+ /**
+ * Starts xmpp protocol, call after connecting to socket
+ * @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() throws IOException, XmlPullParserException, NoSuchAlgorithmException {
+ tagWriter.setOutputStream(socket.getOutputStream());
+ tagReader.setInputStream(socket.getInputStream());
+ tagWriter.beginDocument();
+ sendStartStream();
+ Tag nextTag;
+ 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 final SSLSocketFactory factory;
+ private final HostnameVerifier verifier;
+
+ public TlsFactoryVerifier(final SSLSocketFactory factory, final HostnameVerifier verifier) throws IOException {
+ this.factory = factory;
+ this.verifier = verifier;
+ if (factory == null || verifier == null) {
+ throw new IOException("could not setup ssl");
+ }
+ }
+ }
+
+ private TlsFactoryVerifier getTlsFactoryVerifier() throws NoSuchAlgorithmException, KeyManagementException, IOException {
+ final SSLContext sc = SSLSocketHelper.getSSLContext();
+ MemorizingTrustManager trustManager = ConversationsPlusApplication.getMemorizingTrustManager();
+ KeyManager[] keyManager;
+ if (account.getPrivateKeyAlias() != null && account.getPassword().isEmpty()) {
+ keyManager = new KeyManager[]{mKeyManager};
+ } else {
+ keyManager = null;
+ }
+ sc.init(keyManager, new X509TrustManager[]{mInteractive ? trustManager : trustManager.getNonInteractive()}, ConversationsPlusApplication.getSecureRandom());
+ final SSLSocketFactory factory = sc.getSocketFactory();
+ final HostnameVerifier verifier;
+ if (mInteractive) {
+ verifier = trustManager.wrapHostnameVerifier(new XmppDomainVerifier());
+ } else {
+ verifier = trustManager.wrapHostnameVerifierNonInteractive(new XmppDomainVerifier());
+ }
+
+ return new TlsFactoryVerifier(factory, verifier);
+ }
+
+ @Override
+ public void run() {
+ forceCloseSocket();
+ connect();
+ }
+
+ private void processStream() throws XmlPullParserException, IOException, NoSuchAlgorithmException {
+ Tag nextTag = tagReader.readTag();
+ while (nextTag != null && !nextTag.isEnd("stream")) {
+ if (nextTag.isStart("error")) {
+ processStreamError(nextTag);
+ } else if (nextTag.isStart("features")) {
+ processStreamFeatures(nextTag);
+ } else if (nextTag.isStart("proceed")) {
+ switchOverToTls(nextTag);
+ } else if (nextTag.isStart("success")) {
+ final String challenge = tagReader.readElement(nextTag).getContent();
+ try {
+ saslMechanism.getResponse(challenge);
+ } catch (final SaslMechanism.AuthenticationException e) {
+ disconnect(true);
+ Log.e(Config.LOGTAG, String.valueOf(e));
+ }
+ Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": logged in");
+ account.setKey(Account.PINNED_MECHANISM_KEY,
+ String.valueOf(saslMechanism.getPriority()));
+ tagReader.reset();
+ sendStartStream();
+ final Tag tag = tagReader.readTag();
+ if (tag != null && tag.isStart("stream")) {
+ processStream();
+ } else {
+ throw new IOException("server didn't restart stream after successful auth");
+ }
+ break;
+ } else if (nextTag.isStart("failure")) {
+ throw new UnauthorizedException();
+ } else if (nextTag.isStart("challenge")) {
+ final String challenge = tagReader.readElement(nextTag).getContent();
+ final Element response = new Element("response");
+ response.setAttribute("xmlns",
+ "urn:ietf:params:xml:ns:xmpp-sasl");
+ try {
+ response.setContent(saslMechanism.getResponse(challenge));
+ } catch (final SaslMechanism.AuthenticationException e) {
+ // TODO: Send auth abort tag.
+ Log.e(Config.LOGTAG, e.toString());
+ }
+ tagWriter.writeElement(response);
+ } else if (nextTag.isStart("enabled")) {
+ final Element enabled = tagReader.readElement(nextTag);
+ if ("true".equals(enabled.getAttribute("resume"))) {
+ this.streamId = enabled.getAttribute("id");
+ Log.d(Config.LOGTAG, account.getJid().toBareJid().toString()
+ + ": stream managment(" + smVersion
+ + ") enabled (resumable)");
+ } else {
+ Log.d(Config.LOGTAG, account.getJid().toBareJid().toString()
+ + ": stream management(" + smVersion + ") enabled");
+ }
+ this.stanzasReceived = 0;
+ final RequestPacket r = new RequestPacket(smVersion);
+ tagWriter.writeStanzaAsync(r);
+ } else if (nextTag.isStart("resumed")) {
+ lastPacketReceived = SystemClock.elapsedRealtime();
+ final Element resumed = tagReader.readElement(nextTag);
+ final String h = resumed.getAttribute("h");
+ try {
+ final int serverCount = Integer.parseInt(h);
+ if (serverCount != stanzasSent) {
+ Log.d(Config.LOGTAG, account.getJid().toBareJid().toString()
+ + ": session resumed with lost packages");
+ stanzasSent = serverCount;
+ } else {
+ Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": session resumed");
+ }
+ acknowledgeStanzaUpTo(serverCount);
+ ArrayList<AbstractAcknowledgeableStanza> failedStanzas = new ArrayList<>();
+ for(int i = 0; i < this.mStanzaQueue.size(); ++i) {
+ failedStanzas.add(mStanzaQueue.valueAt(i));
+ }
+ mStanzaQueue.clear();
+ Log.d(Config.LOGTAG,"resending "+failedStanzas.size()+" stanzas");
+ for(AbstractAcknowledgeableStanza packet : failedStanzas) {
+ if (packet instanceof MessagePacket) {
+ MessagePacket message = (MessagePacket) packet;
+ mXmppConnectionService.markMessage(account,
+ message.getTo().toBareJid(),
+ message.getId(),
+ Message.STATUS_UNSEND);
+ }
+ sendPacket(packet);
+ }
+ } catch (final NumberFormatException ignored) {
+ }
+ Log.d(Config.LOGTAG, account.getJid().toBareJid()+ ": online with resource " + account.getResource());
+ changeStatus(Account.State.ONLINE);
+ } else if (nextTag.isStart("r")) {
+ tagReader.readElement(nextTag);
+ if (Config.EXTENDED_SM_LOGGING) {
+ Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": acknowledging stanza #" + this.stanzasReceived);
+ }
+ final AckPacket ack = new AckPacket(this.stanzasReceived, smVersion);
+ tagWriter.writeStanzaAsync(ack);
+ } else if (nextTag.isStart("a")) {
+ final Element ack = tagReader.readElement(nextTag);
+ lastPacketReceived = SystemClock.elapsedRealtime();
+ try {
+ final int serverSequence = Integer.parseInt(ack.getAttribute("h"));
+ acknowledgeStanzaUpTo(serverSequence);
+ } catch (NumberFormatException e) {
+ Log.d(Config.LOGTAG,account.getJid().toBareJid()+": server send ack without sequence number");
+ }
+ } else if (nextTag.isStart("failed")) {
+ tagReader.readElement(nextTag);
+ Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": resumption failed");
+ resetStreamId();
+ if (account.getStatus() != Account.State.ONLINE) {
+ sendBindRequest();
+ }
+ } else if (nextTag.isStart("iq")) {
+ processIq(nextTag);
+ } else if (nextTag.isStart("message")) {
+ processMessage(nextTag);
+ } else if (nextTag.isStart("presence")) {
+ processPresence(nextTag);
+ }
+ nextTag = tagReader.readTag();
+ }
+ }
+
+ private void acknowledgeStanzaUpTo(int serverCount) {
+ for (int i = 0; i < mStanzaQueue.size(); ++i) {
+ if (serverCount >= mStanzaQueue.keyAt(i)) {
+ if (Config.EXTENDED_SM_LOGGING) {
+ Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": server acknowledged stanza #" + mStanzaQueue.keyAt(i));
+ }
+ AbstractAcknowledgeableStanza stanza = mStanzaQueue.valueAt(i);
+ if (stanza instanceof MessagePacket && acknowledgedListener != null) {
+ MessagePacket packet = (MessagePacket) stanza;
+ acknowledgedListener.onMessageAcknowledged(account, packet.getId());
+ }
+ mStanzaQueue.removeAt(i);
+ i--;
+ }
+ }
+ }
+
+ private Element processPacket(final Tag currentTag, final int packetType)
+ throws XmlPullParserException, IOException {
+ Element element;
+ switch (packetType) {
+ case PACKET_IQ:
+ element = new IqPacket();
+ break;
+ case PACKET_MESSAGE:
+ element = new MessagePacket();
+ break;
+ case PACKET_PRESENCE:
+ element = new PresencePacket();
+ break;
+ default:
+ return null;
+ }
+ element.setAttributes(currentTag.getAttributes());
+ Tag nextTag = tagReader.readTag();
+ if (nextTag == null) {
+ throw new IOException("interrupted mid tag");
+ }
+ while (!nextTag.isEnd(element.getName())) {
+ if (!nextTag.isNo()) {
+ final Element child = tagReader.readElement(nextTag);
+ final String type = currentTag.getAttribute("type");
+ if (packetType == PACKET_IQ
+ && "jingle".equals(child.getName())
+ && ("set".equalsIgnoreCase(type) || "get"
+ .equalsIgnoreCase(type))) {
+ element = new JinglePacket();
+ element.setAttributes(currentTag.getAttributes());
+ }
+ element.addChild(child);
+ }
+ nextTag = tagReader.readTag();
+ if (nextTag == null) {
+ throw new IOException("interrupted mid tag");
+ }
+ }
+ if (stanzasReceived == Integer.MAX_VALUE) {
+ resetStreamId();
+ throw new IOException("time to restart the session. cant handle >2 billion pcks");
+ }
+ ++stanzasReceived;
+ lastPacketReceived = SystemClock.elapsedRealtime();
+ return element;
+ }
+
+ private void processIq(final Tag currentTag) throws XmlPullParserException, IOException {
+ final IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ);
+
+ if (packet.getId() == null) {
+ return; // an iq packet without id is definitely invalid
+ }
+
+ if (packet instanceof JinglePacket) {
+ if (this.jingleListener != null) {
+ this.jingleListener.onJinglePacketReceived(account,(JinglePacket) packet);
+ }
+ } else {
+ OnIqPacketReceived callback = null;
+ synchronized (this.packetCallbacks) {
+ if (packetCallbacks.containsKey(packet.getId())) {
+ final Pair<IqPacket, OnIqPacketReceived> packetCallbackDuple = packetCallbacks.get(packet.getId());
+ // Packets to the server should have responses from the server
+ if (packetCallbackDuple.first.toServer(account)) {
+ if (packet.fromServer(account) || mServerIdentity == Identity.FACEBOOK) {
+ callback = packetCallbackDuple.second;
+ packetCallbacks.remove(packet.getId());
+ } else {
+ Log.e(Config.LOGTAG, account.getJid().toBareJid().toString() + ": ignoring spoofed iq packet");
+ }
+ } else {
+ if (packet.getFrom().equals(packetCallbackDuple.first.getTo())) {
+ callback = packetCallbackDuple.second;
+ packetCallbacks.remove(packet.getId());
+ } else {
+ Logging.e(Config.LOGTAG, account.getJid().toBareJid().toString() + ": ignoring spoofed iq packet");
+ }
+ }
+ } else if (packet.getType() == IqPacket.TYPE.GET || packet.getType() == IqPacket.TYPE.SET) {
+ callback = this.unregisteredIqListener;
+ }
+ }
+ if (callback != null) {
+ callback.onIqPacketReceived(account,packet);
+ }
+ }
+ }
+
+ private void processMessage(final Tag currentTag) throws XmlPullParserException, IOException {
+ final MessagePacket packet = (MessagePacket) processPacket(currentTag,PACKET_MESSAGE);
+ this.messageListener.onMessagePacketReceived(account, packet);
+ }
+
+ private void processPresence(final Tag currentTag) throws XmlPullParserException, IOException {
+ PresencePacket packet = (PresencePacket) processPacket(currentTag, PACKET_PRESENCE);
+ this.presenceListener.onPresencePacketReceived(account, packet);
+ }
+
+ private void sendStartTLS() throws IOException {
+ final Tag startTLS = Tag.empty("starttls");
+ startTLS.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-tls");
+ tagWriter.writeTag(startTLS);
+ }
+
+
+
+ private void switchOverToTls(final Tag currentTag) throws XmlPullParserException, IOException {
+ tagReader.readTag();
+ try {
+ final TlsFactoryVerifier tlsFactoryVerifier = getTlsFactoryVerifier();
+ final InetAddress address = socket == null ? null : socket.getInetAddress();
+
+ if (address == null) {
+ throw new IOException("could not setup ssl");
+ }
+
+ final SSLSocket sslSocket = (SSLSocket) tlsFactoryVerifier.factory.createSocket(socket, address.getHostAddress(), socket.getPort(), true);
+
+ if (sslSocket == null) {
+ throw new IOException("could not initialize ssl socket");
+ }
+
+ SSLSocketHelper.setSecurity(sslSocket);
+
+ if (!tlsFactoryVerifier.verifier.verify(account.getServer().getDomainpart(), sslSocket.getSession())) {
+ Log.d(Config.LOGTAG,account.getJid().toBareJid()+": TLS certificate verification failed");
+ throw new SecurityException();
+ }
+ tagReader.setInputStream(sslSocket.getInputStream());
+ tagWriter.setOutputStream(sslSocket.getOutputStream());
+ sendStartStream();
+ Log.d(Config.LOGTAG, account.getJid().toBareJid()+ ": TLS connection established");
+ features.encryptionEnabled = true;
+ final Tag tag = tagReader.readTag();
+ if (tag != null && tag.isStart("stream")) {
+ processStream();
+ } else {
+ throw new IOException("server didn't restart stream after STARTTLS");
+ }
+ sslSocket.close();
+ } catch (final NoSuchAlgorithmException | KeyManagementException e1) {
+ Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": TLS certificate verification failed");
+ throw new SecurityException();
+ }
+ }
+
+ private void processStreamFeatures(final Tag currentTag)
+ throws XmlPullParserException, IOException {
+ this.streamFeatures = tagReader.readElement(currentTag);
+ if (this.streamFeatures.hasChild("starttls") && !features.encryptionEnabled) {
+ sendStartTLS();
+ } else if (this.streamFeatures.hasChild("register") && account.isOptionSet(Account.OPTION_REGISTER)) {
+ if (features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS) {
+ sendRegistryRequest();
+ } else {
+ throw new IncompatibleServerException();
+ }
+ } else if (!this.streamFeatures.hasChild("register")
+ && account.isOptionSet(Account.OPTION_REGISTER)) {
+ changeStatus(Account.State.REGISTRATION_NOT_SUPPORTED);
+ disconnect(true);
+ } else if (this.streamFeatures.hasChild("mechanisms")
+ && shouldAuthenticate
+ && (features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS)) {
+ authenticate();
+ } else if (this.streamFeatures.hasChild("sm", "urn:xmpp:sm:" + smVersion) && streamId != null) {
+ if (Config.EXTENDED_SM_LOGGING) {
+ Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": resuming after stanza #"+stanzasReceived);
+ }
+ final ResumePacket resume = new ResumePacket(this.streamId, stanzasReceived, smVersion);
+ this.tagWriter.writeStanzaAsync(resume);
+ } else if (needsBinding) {
+ if (this.streamFeatures.hasChild("bind")) {
+ sendBindRequest();
+ } else {
+ throw new IncompatibleServerException();
+ }
+ }
+ }
+
+ private void authenticate() throws IOException {
+ final List<String> mechanisms = extractMechanisms(streamFeatures
+ .findChild("mechanisms"));
+ final Element auth = new Element("auth");
+ auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
+ if (mechanisms.contains("EXTERNAL") && account.getPrivateKeyAlias() != null) {
+ saslMechanism = new External(tagWriter, account, ConversationsPlusApplication.getSecureRandom());
+ } else if (mechanisms.contains("SCRAM-SHA-1")) {
+ saslMechanism = new ScramSha1(tagWriter, account, ConversationsPlusApplication.getSecureRandom());
+ } else if (mechanisms.contains("PLAIN")) {
+ saslMechanism = new Plain(tagWriter, account);
+ } else if (mechanisms.contains("DIGEST-MD5")) {
+ saslMechanism = new DigestMd5(tagWriter, account, ConversationsPlusApplication.getSecureRandom());
+ }
+ if (saslMechanism != null) {
+ final JSONObject keys = account.getKeys();
+ try {
+ if (keys.has(Account.PINNED_MECHANISM_KEY) &&
+ keys.getInt(Account.PINNED_MECHANISM_KEY) > saslMechanism.getPriority()) {
+ Logging.e(Config.LOGTAG, "Auth failed. Authentication mechanism " + saslMechanism.getMechanism() +
+ " has lower priority (" + String.valueOf(saslMechanism.getPriority()) +
+ ") than pinned priority (" + keys.getInt(Account.PINNED_MECHANISM_KEY) +
+ "). Possible downgrade attack?");
+ throw new SecurityException();
+ }
+ } catch (final JSONException e) {
+ Logging.d(Config.LOGTAG, "Parse error while checking pinned auth mechanism");
+ }
+ Logging.d(Config.LOGTAG, account.getJid().toString() + ": Authenticating with " + saslMechanism.getMechanism());
+ auth.setAttribute("mechanism", saslMechanism.getMechanism());
+ if (!saslMechanism.getClientFirstMessage().isEmpty()) {
+ auth.setContent(saslMechanism.getClientFirstMessage());
+ }
+ tagWriter.writeElement(auth);
+ } else {
+ throw new IncompatibleServerException();
+ }
+ }
+
+ private List<String> extractMechanisms(final Element stream) {
+ final ArrayList<String> mechanisms = new ArrayList<>(stream
+ .getChildren().size());
+ for (final Element child : stream.getChildren()) {
+ mechanisms.add(child.getContent());
+ }
+ return mechanisms;
+ }
+
+ public void sendCaptchaRegistryRequest(String id, Data data) {
+ if (data == null) {
+ setAccountCreationFailed("");
+ } else {
+ IqPacket request = getIqGenerator().generateCreateAccountWithCaptcha(account, id, data);
+ sendIqPacket(request, createPacketReceiveHandler());
+ }
+ }
+
+ private void sendRegistryRequest() {
+ final IqPacket register = new IqPacket(IqPacket.TYPE.GET);
+ register.query("jabber:iq:register");
+ register.setTo(account.getServer());
+ sendIqPacket(register, new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(final Account account, final IqPacket packet) {
+ boolean failed = false;
+ if (packet.getType() == IqPacket.TYPE.RESULT
+ && packet.query().hasChild("username")
+ && (packet.query().hasChild("password"))) {
+ final IqPacket register = new IqPacket(IqPacket.TYPE.SET);
+ final Element username = new Element("username").setContent(account.getUsername());
+ final Element password = new Element("password").setContent(account.getPassword());
+ register.query("jabber:iq:register").addChild(username);
+ register.query().addChild(password);
+ sendIqPacket(register, createPacketReceiveHandler());
+ } else if (packet.getType() == IqPacket.TYPE.RESULT
+ && (packet.query().hasChild("x", "jabber:x:data"))) {
+ final Data data = Data.parse(packet.query().findChild("x", "jabber:x:data"));
+ final Element blob = packet.query().findChild("data", "urn:xmpp:bob");
+ final String id = packet.getId();
+
+ Bitmap captcha = null;
+ if (blob != null) {
+ try {
+ final String base64Blob = blob.getContent();
+ final byte[] strBlob = Base64.decode(base64Blob, Base64.DEFAULT);
+ InputStream stream = new ByteArrayInputStream(strBlob);
+ captcha = BitmapFactory.decodeStream(stream);
+ } catch (Exception e) {
+ //ignored
+ }
+ } else {
+ try {
+ Field url = data.getFieldByName("url");
+ String urlString = url.findChildContent("value");
+ URL uri = new URL(urlString);
+ captcha = BitmapFactory.decodeStream(uri.openConnection().getInputStream());
+ } catch (IOException e) {
+ Logging.e(Config.LOGTAG, e.toString());
+ }
+ }
+
+ if (captcha != null) {
+ failed = !mXmppConnectionService.displayCaptchaRequest(account, id, data, captcha);
+ }
+ } else {
+ failed = true;
+ }
+
+ if (failed) {
+ final Element instructions = packet.query().findChild("instructions");
+ setAccountCreationFailed((instructions != null) ? instructions.getContent() : "");
+ }
+ }
+ });
+ }
+
+ private void setAccountCreationFailed(String instructions) {
+ changeStatus(Account.State.REGISTRATION_FAILED);
+ disconnect(true);
+ Log.d(Config.LOGTAG, account.getJid().toBareJid()
+ + ": could not register. instructions are"
+ + instructions);
+ }
+
+ public void resetEverything() {
+ resetStreamId();
+ clearIqCallbacks();
+ mStanzaQueue.clear();
+ synchronized (this.disco) {
+ disco.clear();
+ }
+ }
+
+ private void sendBindRequest() {
+ while(!mXmppConnectionService.areMessagesInitialized() && socket != null && !socket.isClosed()) {
+ try {
+ Thread.sleep(500);
+ } catch (final InterruptedException ignored) {
+ }
+ }
+ needsBinding = false;
+ clearIqCallbacks();
+ final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
+ iq.addChild("bind", "urn:ietf:params:xml:ns:xmpp-bind")
+ .addChild("resource").setContent(account.getResource());
+ this.sendUnmodifiedIqPacket(iq, new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(final Account account, final IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE.TIMEOUT) {
+ return;
+ }
+ final Element bind = packet.findChild("bind");
+ if (bind != null && packet.getType() == IqPacket.TYPE.RESULT) {
+ final Element jid = bind.findChild("jid");
+ if (jid != null && jid.getContent() != null) {
+ try {
+ account.setResource(Jid.fromString(jid.getContent()).getResourcepart());
+ } catch (final InvalidJidException e) {
+ // TODO: Handle the case where an external JID is technically invalid?
+ }
+ if (streamFeatures.hasChild("session")) {
+ sendStartSession();
+ } else {
+ sendPostBindInitialization();
+ }
+ } else {
+ Logging.d(Config.LOGTAG, account.getJid() + ": disconnecting because of bind failure. (no jid)");
+ disconnect(true);
+ }
+ } else {
+ Logging.d(Config.LOGTAG, account.getJid() + ": disconnecting because of bind failure ("+packet.toString());
+ disconnect(true);
+ }
+ }
+ });
+ }
+
+ private void clearIqCallbacks() {
+ final IqPacket failurePacket = new IqPacket(IqPacket.TYPE.TIMEOUT);
+ final ArrayList<OnIqPacketReceived> callbacks = new ArrayList<>();
+ synchronized (this.packetCallbacks) {
+ if (this.packetCallbacks.size() == 0) {
+ return;
+ }
+ Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": clearing "+this.packetCallbacks.size()+" iq callbacks");
+ final Iterator<Pair<IqPacket, OnIqPacketReceived>> iterator = this.packetCallbacks.values().iterator();
+ while (iterator.hasNext()) {
+ Pair<IqPacket, OnIqPacketReceived> entry = iterator.next();
+ callbacks.add(entry.second);
+ iterator.remove();
+ }
+ }
+ for(OnIqPacketReceived callback : callbacks) {
+ callback.onIqPacketReceived(account,failurePacket);
+ }
+ Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": done clearing iq callbacks. " + this.packetCallbacks.size() + " left");
+ }
+
+ public void sendDiscoTimeout() {
+ final IqPacket failurePacket = new IqPacket(IqPacket.TYPE.ERROR); //don't use timeout
+ final ArrayList<OnIqPacketReceived> callbacks = new ArrayList<>();
+ synchronized (this.mPendingServiceDiscoveriesIds) {
+ for(String id : mPendingServiceDiscoveriesIds) {
+ synchronized (this.packetCallbacks) {
+ Pair<IqPacket, OnIqPacketReceived> pair = this.packetCallbacks.remove(id);
+ if (pair != null) {
+ callbacks.add(pair.second);
+ }
+ }
+ }
+ this.mPendingServiceDiscoveriesIds.clear();
+ }
+ if (callbacks.size() > 0) {
+ Log.d(Config.LOGTAG,account.getJid().toBareJid()+": sending disco timeout");
+ resetStreamId(); //we don't want to live with this for ever
+ }
+ for(OnIqPacketReceived callback : callbacks) {
+ callback.onIqPacketReceived(account,failurePacket);
+ }
+ }
+
+ private void sendStartSession() {
+ final IqPacket startSession = new IqPacket(IqPacket.TYPE.SET);
+ startSession.addChild("session", "urn:ietf:params:xml:ns:xmpp-session");
+ this.sendUnmodifiedIqPacket(startSession, new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE.RESULT) {
+ sendPostBindInitialization();
+ } else if (packet.getType() != IqPacket.TYPE.TIMEOUT) {
+ Logging.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not init sessions");
+ disconnect(true);
+ }
+ }
+ });
+ }
+
+ private void sendPostBindInitialization() {
+ smVersion = 0;
+ if (streamFeatures.hasChild("sm", "urn:xmpp:sm:3")) {
+ smVersion = 3;
+ } else if (streamFeatures.hasChild("sm", "urn:xmpp:sm:2")) {
+ smVersion = 2;
+ }
+ if (smVersion != 0) {
+ final EnablePacket enable = new EnablePacket(smVersion);
+ tagWriter.writeStanzaAsync(enable);
+ stanzasSent = 0;
+ mStanzaQueue.clear();
+ }
+ features.carbonsEnabled = false;
+ features.blockListRequested = false;
+ synchronized (this.disco) {
+ this.disco.clear();
+ }
+ mPendingServiceDiscoveries.set(0);
+ mIsServiceItemsDiscoveryPending.set(true);
+ mWaitForDisco = mServerIdentity != Identity.NIMBUZZ;
+ lastDiscoStarted = SystemClock.elapsedRealtime();
+ Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": starting service discovery");
+ mXmppConnectionService.scheduleWakeUpCall(Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode());
+ Element caps = streamFeatures.findChild("c");
+ final String hash = caps == null ? null : caps.getAttribute("hash");
+ final String ver = caps == null ? null : caps.getAttribute("ver");
+ ServiceDiscoveryResult discoveryResult = null;
+ if (hash != null && ver != null) {
+ discoveryResult = mXmppConnectionService.databaseBackend.findDiscoveryResult(hash, ver);
+ }
+ if (discoveryResult == null) {
+ sendServiceDiscoveryInfo(account.getServer());
+ } else {
+ Log.d(Config.LOGTAG,account.getJid().toBareJid()+": server caps came from cache");
+ disco.put(account.getServer(), discoveryResult);
+ }
+ sendServiceDiscoveryInfo(account.getJid().toBareJid());
+ sendServiceDiscoveryItems(account.getServer());
+ if (!mWaitForDisco) {
+ finalizeBind();
+ }
+ this.lastSessionStarted = SystemClock.elapsedRealtime();
+ }
+
+ private void sendServiceDiscoveryInfo(final Jid jid) {
+ mPendingServiceDiscoveries.incrementAndGet();
+ final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
+ iq.setTo(jid);
+ iq.query("http://jabber.org/protocol/disco#info");
+ String id = this.sendIqPacket(iq, new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(final Account account, final IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE.RESULT) {
+ boolean advancedStreamFeaturesLoaded;
+ synchronized (XmppConnection.this.disco) {
+ 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())) {
+ mXmppConnectionService.databaseBackend.insertDiscoveryResult(result);
+ }
+ disco.put(jid, result);
+ advancedStreamFeaturesLoaded = disco.containsKey(account.getServer())
+ && disco.containsKey(account.getJid().toBareJid());
+ }
+ if (advancedStreamFeaturesLoaded && (jid.equals(account.getServer()) || jid.equals(account.getJid().toBareJid()))) {
+ enableAdvancedStreamFeatures();
+ }
+ } else {
+ Logging.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not query disco info for " + jid.toString());
+ }
+ if (packet.getType() != IqPacket.TYPE.TIMEOUT) {
+ if (mPendingServiceDiscoveries.decrementAndGet() == 0
+ && !mIsServiceItemsDiscoveryPending.get()
+ && mWaitForDisco) {
+ finalizeBind();
+ }
+ }
+ }
+ });
+ synchronized (this.mPendingServiceDiscoveriesIds) {
+ this.mPendingServiceDiscoveriesIds.add(id);
+ }
+ }
+
+ private void finalizeBind() {
+ Logging.d(Config.LOGTAG, account.getJid().toBareJid() + ": online with resource " + account.getResource());
+ if (bindListener != null) {
+ bindListener.onBind(account);
+ }
+ changeStatus(Account.State.ONLINE);
+ }
+
+ private void enableAdvancedStreamFeatures() {
+ if (getFeatures().carbons() && !features.carbonsEnabled) {
+ sendEnableCarbons();
+ }
+ if (getFeatures().blocking() && !features.blockListRequested) {
+ Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": Requesting block list");
+ this.sendIqPacket(getIqGenerator().generateGetBlockList(), mXmppConnectionService.getIqParser());
+ }
+ for (final OnAdvancedStreamFeaturesLoaded listener : advancedStreamFeaturesLoadedListeners) {
+ listener.onAdvancedStreamFeaturesAvailable(account);
+ }
+ }
+
+ private void sendServiceDiscoveryItems(final Jid server) {
+ final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
+ iq.setTo(server.toDomainJid());
+ iq.query("http://jabber.org/protocol/disco#items");
+ String id = this.sendIqPacket(iq, new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(final Account account, final IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE.RESULT) {
+ final List<Element> elements = packet.query().getChildren();
+ for (final Element element : elements) {
+ if (element.getName().equals("item")) {
+ final Jid jid = element.getAttributeAsJid("jid");
+ if (jid != null && !jid.equals(account.getServer())) {
+ sendServiceDiscoveryInfo(jid);
+ }
+ }
+ }
+ } else {
+ Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not query disco items of " + server);
+ }
+ if (packet.getType() != IqPacket.TYPE.TIMEOUT) {
+ mIsServiceItemsDiscoveryPending.set(false);
+ if (mPendingServiceDiscoveries.get() == 0 && mWaitForDisco) {
+ finalizeBind();
+ }
+ }
+ }
+ });
+ synchronized (this.mPendingServiceDiscoveriesIds) {
+ this.mPendingServiceDiscoveriesIds.add(id);
+ }
+ }
+
+ private void sendEnableCarbons() {
+ final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
+ iq.addChild("enable", "urn:xmpp:carbons:2");
+ this.sendIqPacket(iq, new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(final Account account, final IqPacket packet) {
+ if (!packet.hasChild("error")) {
+ Logging.d(Config.LOGTAG, account.getJid().toBareJid()
+ + ": successfully enabled carbons");
+ features.carbonsEnabled = true;
+ } else {
+ Logging.d(Config.LOGTAG, account.getJid().toBareJid()
+ + ": error enableing carbons " + packet.toString());
+ }
+ }
+ });
+ }
+
+ private void processStreamError(final Tag currentTag)
+ throws XmlPullParserException, IOException {
+ final Element streamError = tagReader.readElement(currentTag);
+ if (streamError != null && streamError.hasChild("conflict")) {
+ final String resource = account.getResource().split("\\.")[0];
+ account.setResource(resource + "." + nextRandomId());
+ Logging.d(Config.LOGTAG,
+ account.getJid().toBareJid() + ": switching resource due to conflict ("
+ + account.getResource() + ")");
+ } else if (streamError != null) {
+ Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": stream error "+streamError.toString());
+ }
+ }
+
+ private void sendStartStream() throws IOException {
+ final Tag stream = Tag.start("stream:stream");
+ stream.setAttribute("to", account.getServer().toString());
+ stream.setAttribute("version", "1.0");
+ stream.setAttribute("xml:lang", "en");
+ stream.setAttribute("xmlns", "jabber:client");
+ stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams");
+ tagWriter.writeTag(stream);
+ }
+
+ private String nextRandomId() {
+ return new BigInteger(50, ConversationsPlusApplication.getSecureRandom()).toString(32);
+ }
+
+ public String sendIqPacket(final IqPacket packet, final OnIqPacketReceived callback) {
+ packet.setFrom(account.getJid());
+ return this.sendUnmodifiedIqPacket(packet, callback);
+ }
+
+ private synchronized String sendUnmodifiedIqPacket(final IqPacket packet, final OnIqPacketReceived callback) {
+ if (packet.getId() == null) {
+ final String id = nextRandomId();
+ packet.setAttribute("id", id);
+ }
+ if (callback != null) {
+ synchronized (this.packetCallbacks) {
+ packetCallbacks.put(packet.getId(), new Pair<>(packet, callback));
+ }
+ }
+ this.sendPacket(packet);
+ return packet.getId();
+ }
+
+ public void sendMessagePacket(final MessagePacket packet) {
+ this.sendPacket(packet);
+ }
+
+ public void sendPresencePacket(final PresencePacket packet) {
+ this.sendPacket(packet);
+ }
+
+ private synchronized void sendPacket(final AbstractStanza packet) {
+ if (stanzasSent == Integer.MAX_VALUE) {
+ resetStreamId();
+ disconnect(true);
+ return;
+ }
+ tagWriter.writeStanzaAsync(packet);
+ if (packet instanceof AbstractAcknowledgeableStanza) {
+ AbstractAcknowledgeableStanza stanza = (AbstractAcknowledgeableStanza) packet;
+ ++stanzasSent;
+ this.mStanzaQueue.put(stanzasSent, stanza);
+ if (stanza instanceof MessagePacket && stanza.getId() != null && getFeatures().sm()) {
+ if (Config.EXTENDED_SM_LOGGING) {
+ Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": requesting ack for message stanza #" + stanzasSent);
+ }
+ tagWriter.writeStanzaAsync(new RequestPacket(this.smVersion));
+ }
+ }
+ }
+
+ public void sendPing() {
+ if (!r()) {
+ final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
+ iq.setFrom(account.getJid());
+ iq.addChild("ping", "urn:xmpp:ping");
+ this.sendIqPacket(iq, null);
+ }
+ this.lastPingSent = SystemClock.elapsedRealtime();
+ }
+
+ public void setOnMessagePacketReceivedListener(
+ final OnMessagePacketReceived listener) {
+ this.messageListener = listener;
+ }
+
+ public void setOnUnregisteredIqPacketReceivedListener(
+ final OnIqPacketReceived listener) {
+ this.unregisteredIqListener = listener;
+ }
+
+ public void setOnPresencePacketReceivedListener(
+ final OnPresencePacketReceived listener) {
+ this.presenceListener = listener;
+ }
+
+ public void setOnJinglePacketReceivedListener(
+ final OnJinglePacketReceived listener) {
+ this.jingleListener = listener;
+ }
+
+ public void setOnStatusChangedListener(final OnStatusChanged listener) {
+ this.statusListener = listener;
+ }
+
+ public void setOnBindListener(final OnBindListener listener) {
+ this.bindListener = listener;
+ }
+
+ public void setOnMessageAcknowledgeListener(final OnMessageAcknowledged listener) {
+ this.acknowledgedListener = listener;
+ }
+
+ public void addOnAdvancedStreamFeaturesAvailableListener(final OnAdvancedStreamFeaturesLoaded listener) {
+ if (!this.advancedStreamFeaturesLoadedListeners.contains(listener)) {
+ this.advancedStreamFeaturesLoadedListeners.add(listener);
+ }
+ }
+
+ public void waitForPush() {
+ if (tagWriter.isActive()) {
+ tagWriter.finish();
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ while(!tagWriter.finished()) {
+ Thread.sleep(10);
+ }
+ socket.close();
+ Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": closed tcp without closing stream");
+ } catch (IOException e) {
+ e.printStackTrace();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ }).start();
+ } else {
+ forceCloseSocket();
+ Logging.d(Config.LOGTAG, account.getJid().toBareJid() + ": closed tcp without closing stream (no waiting)");
+ }
+ }
+
+ private void forceCloseSocket() {
+ if (socket != null) {
+ try {
+ socket.close();
+ } catch (IOException e) {
+ Logging.d(Config.LOGTAG,account.getJid().toBareJid().toString()+": exception during force close ("+e.getMessage()+")");
+ }
+ }
+ }
+
+ public void disconnect(final boolean force) {
+ Logging.d(Config.LOGTAG, account.getJid().toBareJid() + ": disconnecting force="+Boolean.valueOf(force));
+ if (force) {
+ forceCloseSocket();
+ return;
+ } else {
+ if (tagWriter.isActive()) {
+ tagWriter.finish();
+ try {
+ int i = 0;
+ boolean warned = false;
+ while (!tagWriter.finished() && socket.isConnected() && i <= 10) {
+ if (!warned) {
+ Log.d(Config.LOGTAG, account.getJid().toBareJid()+": waiting for tag writer to finish");
+ warned = true;
+ }
+ Thread.sleep(200);
+ i++;
+ }
+ if (warned) {
+ Log.d(Config.LOGTAG,account.getJid().toBareJid()+": tag writer has finished");
+ }
+ Log.d(Config.LOGTAG,account.getJid().toBareJid()+": closing stream");
+ tagWriter.writeTag(Tag.end("stream:stream"));
+ } catch (final IOException e) {
+ Log.d(Config.LOGTAG,account.getJid().toBareJid()+": io exception during disconnect ("+e.getMessage()+")");
+ } catch (final InterruptedException e) {
+ Log.d(Config.LOGTAG, "interrupted");
+ }
+ }
+ }
+ }
+
+ public void resetStreamId() {
+ this.streamId = null;
+ }
+
+ private List<Entry<Jid, ServiceDiscoveryResult>> findDiscoItemsByFeature(final String feature) {
+ synchronized (this.disco) {
+ final List<Entry<Jid, ServiceDiscoveryResult>> items = new ArrayList<>();
+ for (final Entry<Jid, ServiceDiscoveryResult> cursor : this.disco.entrySet()) {
+ if (cursor.getValue().getFeatures().contains(feature)) {
+ items.add(cursor);
+ }
+ }
+ return items;
+ }
+ }
+
+ public Jid findDiscoItemByFeature(final String feature) {
+ final List<Entry<Jid, ServiceDiscoveryResult>> items = findDiscoItemsByFeature(feature);
+ if (items.size() >= 1) {
+ return items.get(0).getKey();
+ }
+ return null;
+ }
+
+ public boolean r() {
+ if (getFeatures().sm()) {
+ this.tagWriter.writeStanzaAsync(new RequestPacket(smVersion));
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public String getMucServer() {
+ synchronized (this.disco) {
+ for (final Entry<Jid, ServiceDiscoveryResult> cursor : disco.entrySet()) {
+ final ServiceDiscoveryResult value = cursor.getValue();
+ if (value.getFeatures().contains("http://jabber.org/protocol/muc")
+ && !value.getFeatures().contains("jabber:iq:gateway")
+ && !value.hasIdentity("conference", "irc")) {
+ return cursor.getKey().toString();
+ }
+ }
+ }
+ return null;
+ }
+
+ public int getTimeToNextAttempt() {
+ final int interval = (int) (25 * Math.pow(1.5, attempt));
+ final int secondsSinceLast = (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000);
+ return interval - secondsSinceLast;
+ }
+
+ public int getAttempt() {
+ return this.attempt;
+ }
+
+ public Features getFeatures() {
+ return this.features;
+ }
+
+ public long getLastSessionEstablished() {
+ final long diff;
+ if (this.lastSessionStarted == 0) {
+ diff = SystemClock.elapsedRealtime() - this.lastConnect;
+ } else {
+ diff = SystemClock.elapsedRealtime() - this.lastSessionStarted;
+ }
+ return System.currentTimeMillis() - diff;
+ }
+
+ public long getLastConnect() {
+ return this.lastConnect;
+ }
+
+ public long getLastPingSent() {
+ return this.lastPingSent;
+ }
+
+ public long getLastDiscoStarted() {
+ return this.lastDiscoStarted;
+ }
+ public long getLastPacketReceived() {
+ return this.lastPacketReceived;
+ }
+
+ public void sendActive() {
+ this.sendPacket(new ActivePacket());
+ }
+
+ public void sendInactive() {
+ this.sendPacket(new InactivePacket());
+ }
+
+ public void resetAttemptCount() {
+ this.attempt = 0;
+ this.lastConnect = 0;
+ }
+
+ public void setInteractive(boolean interactive) {
+ this.mInteractive = interactive;
+ }
+
+ public Identity getServerIdentity() {
+ return mServerIdentity;
+ }
+
+ private class UnauthorizedException extends IOException {
+
+ }
+
+ private class SecurityException extends IOException {
+
+ }
+
+ private class IncompatibleServerException extends IOException {
+
+ }
+
+ public enum Identity {
+ FACEBOOK,
+ SLACK,
+ EJABBERD,
+ PROSODY,
+ NIMBUZZ,
+ UNKNOWN
+ }
+
+ public class Features {
+ XmppConnection connection;
+ private boolean carbonsEnabled = false;
+ private boolean encryptionEnabled = false;
+ private boolean blockListRequested = false;
+
+ public Features(final XmppConnection connection) {
+ this.connection = connection;
+ }
+
+ private boolean hasDiscoFeature(final Jid server, final String feature) {
+ synchronized (XmppConnection.this.disco) {
+ return connection.disco.containsKey(server) &&
+ connection.disco.get(server).getFeatures().contains(feature);
+ }
+ }
+
+ public boolean carbons() {
+ return hasDiscoFeature(account.getServer(), "urn:xmpp:carbons:2");
+ }
+
+ public boolean blocking() {
+ return hasDiscoFeature(account.getServer(), Xmlns.BLOCKING);
+ }
+
+ public boolean register() {
+ return hasDiscoFeature(account.getServer(), Xmlns.REGISTER);
+ }
+
+ public boolean sm() {
+ return streamId != null
+ || (connection.streamFeatures != null && connection.streamFeatures.hasChild("sm"));
+ }
+
+ public boolean csi() {
+ return connection.streamFeatures != null && connection.streamFeatures.hasChild("csi", "urn:xmpp:csi:0");
+ }
+
+ public boolean pep() {
+ synchronized (XmppConnection.this.disco) {
+ ServiceDiscoveryResult info = disco.get(account.getJid().toBareJid());
+ return info != null && info.hasIdentity("pubsub", "pep");
+ }
+ }
+
+ public boolean pepPersistent() {
+ synchronized (XmppConnection.this.disco) {
+ ServiceDiscoveryResult info = disco.get(account.getJid().toBareJid());
+ return info != null && info.getFeatures().contains("http://jabber.org/protocol/pubsub#persistent-items");
+ }
+ }
+
+ public boolean mam() {
+ return hasDiscoFeature(account.getJid().toBareJid(), "urn:xmpp:mam:0")
+ || hasDiscoFeature(account.getServer(), "urn:xmpp:mam:0");
+ }
+
+ public boolean push() {
+ return hasDiscoFeature(account.getJid().toBareJid(), "urn:xmpp:push:0")
+ || hasDiscoFeature(account.getServer(), "urn:xmpp:push:0");
+ }
+
+ public boolean rosterVersioning() {
+ return connection.streamFeatures != null && connection.streamFeatures.hasChild("ver");
+ }
+
+ public void setBlockListRequested(boolean value) {
+ this.blockListRequested = value;
+ }
+
+ public boolean httpUpload(long filesize) {
+ if (Config.DISABLE_HTTP_UPLOAD) {
+ return false;
+ } else {
+ List<Entry<Jid, ServiceDiscoveryResult>> items = findDiscoItemsByFeature(Xmlns.HTTP_UPLOAD);
+ if (items.size() > 0) {
+ try {
+ long maxsize = Long.parseLong(items.get(0).getValue().getExtendedDiscoInformation(Xmlns.HTTP_UPLOAD, "max-file-size"));
+ return filesize <= maxsize;
+ } catch (Exception e) {
+ return true;
+ }
+ } else {
+ return false;
+ }
+ }
+ }
+
+ public long getMaxHttpUploadSize() {
+ List<Entry<Jid, ServiceDiscoveryResult>> items = findDiscoItemsByFeature(Xmlns.HTTP_UPLOAD);
+ if (items.size() > 0) {
+ try {
+ return Long.parseLong(items.get(0).getValue().getExtendedDiscoInformation(Xmlns.HTTP_UPLOAD, "max-file-size"));
+ } catch (Exception e) {
+ return -1;
+ }
+ } else {
+ return -1;
+ }
+ }
+ }
+
+ private IqGenerator getIqGenerator() {
+ return mXmppConnectionService.getIqGenerator();
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/chatstate/ChatState.java b/src/main/java/eu/siacs/conversations/xmpp/chatstate/ChatState.java
new file mode 100644
index 00000000..3e371562
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/chatstate/ChatState.java
@@ -0,0 +1,32 @@
+package eu.siacs.conversations.xmpp.chatstate;
+
+import eu.siacs.conversations.xml.Element;
+
+public enum ChatState {
+
+ ACTIVE, INACTIVE, GONE, COMPOSING, PAUSED;
+
+ public static ChatState parse(Element element) {
+ final String NAMESPACE = "http://jabber.org/protocol/chatstates";
+ if (element.hasChild("active",NAMESPACE)) {
+ return ACTIVE;
+ } else if (element.hasChild("inactive",NAMESPACE)) {
+ return INACTIVE;
+ } else if (element.hasChild("composing",NAMESPACE)) {
+ return COMPOSING;
+ } else if (element.hasChild("gone",NAMESPACE)) {
+ return GONE;
+ } else if (element.hasChild("paused",NAMESPACE)) {
+ return PAUSED;
+ } else {
+ return null;
+ }
+ }
+
+ public static Element toElement(ChatState state) {
+ final String NAMESPACE = "http://jabber.org/protocol/chatstates";
+ final Element element = new Element(state.toString().toLowerCase());
+ element.setAttribute("xmlns",NAMESPACE);
+ return element;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/forms/Data.java b/src/main/java/eu/siacs/conversations/xmpp/forms/Data.java
new file mode 100644
index 00000000..8dabcb5b
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/forms/Data.java
@@ -0,0 +1,99 @@
+package eu.siacs.conversations.xmpp.forms;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+import eu.siacs.conversations.xml.Element;
+
+public class Data extends Element {
+
+ private static final String FORM_TYPE = "FORM_TYPE";
+
+ public Data() {
+ super("x");
+ this.setAttribute("xmlns","jabber:x:data");
+ }
+
+ public List<Field> getFields() {
+ ArrayList<Field> fields = new ArrayList<Field>();
+ for(Element child : getChildren()) {
+ if (child.getName().equals("field")
+ && !FORM_TYPE.equals(child.getAttribute("var"))) {
+ fields.add(Field.parse(child));
+ }
+ }
+ return fields;
+ }
+
+ public Field getFieldByName(String needle) {
+ for(Element child : getChildren()) {
+ if (child.getName().equals("field")
+ && needle.equals(child.getAttribute("var"))) {
+ return Field.parse(child);
+ }
+ }
+ return null;
+ }
+
+ public void put(String name, String value) {
+ Field field = getFieldByName(name);
+ if (field == null) {
+ field = new Field(name);
+ this.addChild(field);
+ }
+ field.setValue(value);
+ }
+
+ public void put(String name, Collection<String> values) {
+ Field field = getFieldByName(name);
+ if (field == null) {
+ field = new Field(name);
+ this.addChild(field);
+ }
+ field.setValues(values);
+ }
+
+ public void submit() {
+ this.setAttribute("type","submit");
+ removeUnnecessaryChildren();
+ for(Field field : getFields()) {
+ field.removeNonValueChildren();
+ }
+ }
+
+ private void removeUnnecessaryChildren() {
+ for(Iterator<Element> iterator = this.children.iterator(); iterator.hasNext();) {
+ Element element = iterator.next();
+ if (!element.getName().equals("field") && !element.getName().equals("title")) {
+ iterator.remove();
+ }
+ }
+ }
+
+ public static Data parse(Element element) {
+ Data data = new Data();
+ data.setAttributes(element.getAttributes());
+ data.setChildren(element.getChildren());
+ return data;
+ }
+
+ public void setFormType(String formType) {
+ this.put(FORM_TYPE, formType);
+ }
+
+ public String getFormType() {
+ String type = getValue(FORM_TYPE);
+ return type == null ? "" : type;
+ }
+
+ public String getValue(String name) {
+ Field field = this.getFieldByName(name);
+ return field == null ? null : field.getValue();
+ }
+
+ public String getTitle() {
+ return findChildContent("title");
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/forms/Field.java b/src/main/java/eu/siacs/conversations/xmpp/forms/Field.java
new file mode 100644
index 00000000..020b34b9
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/forms/Field.java
@@ -0,0 +1,81 @@
+package eu.siacs.conversations.xmpp.forms;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+import eu.siacs.conversations.xml.Element;
+
+public class Field extends Element {
+
+ public Field(String name) {
+ super("field");
+ this.setAttribute("var",name);
+ }
+
+ private Field() {
+ super("field");
+ }
+
+ public String getFieldName() {
+ return this.getAttribute("var");
+ }
+
+ public void setValue(String value) {
+ this.children.clear();
+ this.addChild("value").setContent(value);
+ }
+
+ public void setValues(Collection<String> values) {
+ this.children.clear();
+ for(String value : values) {
+ this.addChild("value").setContent(value);
+ }
+ }
+
+ public void removeNonValueChildren() {
+ for(Iterator<Element> iterator = this.children.iterator(); iterator.hasNext();) {
+ Element element = iterator.next();
+ if (!element.getName().equals("value")) {
+ iterator.remove();
+ }
+ }
+ }
+
+ public static Field parse(Element element) {
+ Field field = new Field();
+ field.setAttributes(element.getAttributes());
+ field.setChildren(element.getChildren());
+ return field;
+ }
+
+ public String getValue() {
+ return findChildContent("value");
+ }
+
+ public List<String> getValues() {
+ List<String> values = new ArrayList<>();
+ for(Element child : getChildren()) {
+ if ("value".equals(child.getName())) {
+ String content = child.getContent();
+ if (content != null) {
+ values.add(content);
+ }
+ }
+ }
+ return values;
+ }
+
+ public String getLabel() {
+ return getAttribute("label");
+ }
+
+ public String getType() {
+ return getAttribute("type");
+ }
+
+ public boolean isRequired() {
+ return hasChild("required");
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jid/InvalidJidException.java b/src/main/java/eu/siacs/conversations/xmpp/jid/InvalidJidException.java
new file mode 100644
index 00000000..164e8849
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jid/InvalidJidException.java
@@ -0,0 +1,49 @@
+package eu.siacs.conversations.xmpp.jid;
+
+public class InvalidJidException extends Exception {
+
+ // This is probably not the "Java way", but the "Java way" means we'd have a ton of extra tiny,
+ // annoying classes floating around. I like this.
+ public final static String INVALID_LENGTH = "JID must be between 0 and 3071 characters";
+ public final static String INVALID_PART_LENGTH = "JID part must be between 0 and 1023 characters";
+ public final static String INVALID_CHARACTER = "JID contains an invalid character";
+ public final static String STRINGPREP_FAIL = "The STRINGPREP operation has failed for the given JID";
+ public final static String IS_NULL = "JID can not be NULL";
+
+ /**
+ * Constructs a new {@code Exception} that includes the current stack trace.
+ */
+ public InvalidJidException() {
+ }
+
+ /**
+ * Constructs a new {@code Exception} with the current stack trace and the
+ * specified detail message.
+ *
+ * @param detailMessage the detail message for this exception.
+ */
+ public InvalidJidException(final String detailMessage) {
+ super(detailMessage);
+ }
+
+ /**
+ * Constructs a new {@code Exception} with the current stack trace, the
+ * specified detail message and the specified cause.
+ *
+ * @param detailMessage the detail message for this exception.
+ * @param throwable the cause of this exception.
+ */
+ public InvalidJidException(final String detailMessage, final Throwable throwable) {
+ super(detailMessage, throwable);
+ }
+
+ /**
+ * Constructs a new {@code Exception} with the current stack trace and the
+ * specified cause.
+ *
+ * @param throwable the cause of this exception.
+ */
+ public InvalidJidException(final Throwable throwable) {
+ super(throwable);
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jid/Jid.java b/src/main/java/eu/siacs/conversations/xmpp/jid/Jid.java
new file mode 100644
index 00000000..a15abe14
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jid/Jid.java
@@ -0,0 +1,226 @@
+package eu.siacs.conversations.xmpp.jid;
+
+import android.util.LruCache;
+
+import net.java.otr4j.session.SessionID;
+
+import java.net.IDN;
+
+import eu.siacs.conversations.Config;
+import gnu.inet.encoding.Stringprep;
+import gnu.inet.encoding.StringprepException;
+
+/**
+ * The `Jid' class provides an immutable representation of a JID.
+ */
+public final class Jid {
+
+ private static LruCache<String,Jid> cache = new LruCache<>(1024);
+
+ private final String localpart;
+ private final String domainpart;
+ private final String resourcepart;
+
+ // 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).
+ private final String displayjid;
+
+ public String getLocalpart() {
+ return localpart;
+ }
+
+ public String getDomainpart() {
+ return IDN.toUnicode(domainpart);
+ }
+
+ public String getResourcepart() {
+ return resourcepart;
+ }
+
+ public static Jid fromSessionID(final SessionID id) throws InvalidJidException{
+ if (id.getUserID().isEmpty()) {
+ return Jid.fromString(id.getAccountID());
+ } else {
+ return Jid.fromString(id.getAccountID()+"/"+id.getUserID());
+ }
+ }
+
+ public static Jid fromString(final String jid) throws InvalidJidException {
+ return Jid.fromString(jid, false);
+ }
+
+ public static Jid fromString(final String jid, final boolean safe) throws InvalidJidException {
+ return new Jid(jid, safe);
+ }
+
+ public static Jid fromParts(final String localpart,
+ final String domainpart,
+ final String resourcepart) throws InvalidJidException {
+ String out;
+ if (localpart == null || localpart.isEmpty()) {
+ out = domainpart;
+ } else {
+ out = localpart + "@" + domainpart;
+ }
+ if (resourcepart != null && !resourcepart.isEmpty()) {
+ out = out + "/" + resourcepart;
+ }
+ return new Jid(out, false);
+ }
+
+ private Jid(final String jid, final boolean safe) throws InvalidJidException {
+ if (jid == null) throw new InvalidJidException(InvalidJidException.IS_NULL);
+
+ Jid fromCache = Jid.cache.get(jid);
+ if (fromCache != null) {
+ displayjid = fromCache.displayjid;
+ localpart = fromCache.localpart;
+ domainpart = fromCache.domainpart;
+ resourcepart = fromCache.resourcepart;
+ return;
+ }
+
+ // Hackish Android way to count the number of chars in a string... should work everywhere.
+ final int atCount = jid.length() - jid.replace("@", "").length();
+ final int slashCount = jid.length() - jid.replace("/", "").length();
+
+ // Throw an error if there's anything obvious wrong with the JID...
+ if (jid.isEmpty() || jid.length() > 3071) {
+ throw new InvalidJidException(InvalidJidException.INVALID_LENGTH);
+ }
+
+ // Go ahead and check if the localpart or resourcepart is empty.
+ if (jid.startsWith("@") || (jid.endsWith("@") && slashCount == 0) || jid.startsWith("/") || (jid.endsWith("/") && slashCount < 2)) {
+ throw new InvalidJidException(InvalidJidException.INVALID_CHARACTER);
+ }
+
+ String finaljid;
+
+ final int domainpartStart;
+ final int atLoc = jid.indexOf("@");
+ final int slashLoc = jid.indexOf("/");
+ // If there is no "@" in the JID (eg. "example.net" or "example.net/resource")
+ // or there are one or more "@" signs but they're all in the resourcepart (eg. "example.net/@/rp@"):
+ if (atCount == 0 || (atCount > 0 && slashLoc != -1 && atLoc > slashLoc)) {
+ localpart = "";
+ finaljid = "";
+ domainpartStart = 0;
+ } else {
+ final String lp = jid.substring(0, atLoc);
+ try {
+ localpart = Config.DISABLE_STRING_PREP || safe ? lp : Stringprep.nodeprep(lp);
+ } catch (final StringprepException e) {
+ throw new InvalidJidException(InvalidJidException.STRINGPREP_FAIL, e);
+ }
+ if (localpart.isEmpty() || localpart.length() > 1023) {
+ throw new InvalidJidException(InvalidJidException.INVALID_PART_LENGTH);
+ }
+ domainpartStart = atLoc + 1;
+ finaljid = lp + "@";
+ }
+
+ final String dp;
+ if (slashCount > 0) {
+ final String rp = jid.substring(slashLoc + 1, jid.length());
+ try {
+ resourcepart = Config.DISABLE_STRING_PREP || safe ? rp : Stringprep.resourceprep(rp);
+ } catch (final StringprepException e) {
+ throw new InvalidJidException(InvalidJidException.STRINGPREP_FAIL, e);
+ }
+ if (resourcepart.isEmpty() || resourcepart.length() > 1023) {
+ throw new InvalidJidException(InvalidJidException.INVALID_PART_LENGTH);
+ }
+ try {
+ dp = IDN.toUnicode(Stringprep.nameprep(jid.substring(domainpartStart, slashLoc)), IDN.USE_STD3_ASCII_RULES);
+ } catch (final StringprepException e) {
+ throw new InvalidJidException(InvalidJidException.STRINGPREP_FAIL, e);
+ }
+ finaljid = finaljid + dp + "/" + rp;
+ } else {
+ resourcepart = "";
+ try{
+ dp = IDN.toUnicode(Stringprep.nameprep(jid.substring(domainpartStart, jid.length())), IDN.USE_STD3_ASCII_RULES);
+ } catch (final StringprepException e) {
+ throw new InvalidJidException(InvalidJidException.STRINGPREP_FAIL, e);
+ }
+ finaljid = finaljid + dp;
+ }
+
+ // Remove trailing "." before storing the domain part.
+ if (dp.endsWith(".")) {
+ try {
+ domainpart = IDN.toASCII(dp.substring(0, dp.length() - 1), IDN.USE_STD3_ASCII_RULES);
+ } catch (final IllegalArgumentException e) {
+ throw new InvalidJidException(e);
+ }
+ } else {
+ try {
+ domainpart = IDN.toASCII(dp, IDN.USE_STD3_ASCII_RULES);
+ } catch (final IllegalArgumentException e) {
+ throw new InvalidJidException(e);
+ }
+ }
+
+ // TODO: Find a proper domain validation library; validate individual parts, separators, etc.
+ if (domainpart.isEmpty() || domainpart.length() > 1023) {
+ throw new InvalidJidException(InvalidJidException.INVALID_PART_LENGTH);
+ }
+
+ Jid.cache.put(jid, this);
+
+ this.displayjid = finaljid;
+ }
+
+ public Jid toBareJid() {
+ try {
+ return resourcepart.isEmpty() ? this : fromParts(localpart, domainpart, "");
+ } catch (final InvalidJidException e) {
+ // This should never happen.
+ throw new AssertionError("Jid " + this.toString() + " invalid");
+ }
+ }
+
+ public Jid toDomainJid() {
+ try {
+ return resourcepart.isEmpty() && localpart.isEmpty() ? this : fromString(getDomainpart());
+ } catch (final InvalidJidException e) {
+ // This should never happen.
+ throw new AssertionError("Jid " + this.toString() + " invalid");
+ }
+ }
+
+ @Override
+ public String toString() {
+ return displayjid;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ final Jid jid = (Jid) o;
+
+ return jid.hashCode() == this.hashCode();
+ }
+
+ @Override
+ public int hashCode() {
+ int result = localpart.hashCode();
+ result = 31 * result + domainpart.hashCode();
+ result = 31 * result + resourcepart.hashCode();
+ return result;
+ }
+
+ public boolean hasLocalpart() {
+ return !localpart.isEmpty();
+ }
+
+ public boolean isBareJid() {
+ return this.resourcepart.isEmpty();
+ }
+
+ public boolean isDomainJid() {
+ return !this.hasLocalpart();
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java
new file mode 100644
index 00000000..dcadb92f
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java
@@ -0,0 +1,147 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+public class JingleCandidate {
+
+ public static int TYPE_UNKNOWN;
+ public static int TYPE_DIRECT = 0;
+ public static int TYPE_PROXY = 1;
+
+ private boolean ours;
+ private boolean usedByCounterpart = false;
+ private String cid;
+ private String host;
+ private int port;
+ private int type;
+ private Jid jid;
+ private int priority;
+
+ public JingleCandidate(String cid, boolean ours) {
+ this.ours = ours;
+ this.cid = cid;
+ }
+
+ public String getCid() {
+ return cid;
+ }
+
+ public void setHost(String host) {
+ this.host = host;
+ }
+
+ public String getHost() {
+ return this.host;
+ }
+
+ public void setJid(final Jid jid) {
+ this.jid = jid;
+ }
+
+ public Jid getJid() {
+ return this.jid;
+ }
+
+ public void setPort(int port) {
+ this.port = port;
+ }
+
+ public int getPort() {
+ return this.port;
+ }
+
+ public void setType(int type) {
+ this.type = type;
+ }
+
+ public void setType(String type) {
+ switch (type) {
+ case "proxy":
+ this.type = TYPE_PROXY;
+ break;
+ case "direct":
+ this.type = TYPE_DIRECT;
+ break;
+ default:
+ this.type = TYPE_UNKNOWN;
+ break;
+ }
+ }
+
+ public void setPriority(int i) {
+ this.priority = i;
+ }
+
+ public int getPriority() {
+ return this.priority;
+ }
+
+ public boolean equals(JingleCandidate other) {
+ return this.getCid().equals(other.getCid());
+ }
+
+ public boolean equalValues(JingleCandidate other) {
+ return other != null && other.getHost().equals(this.getHost()) && (other.getPort() == this.getPort());
+ }
+
+ public boolean isOurs() {
+ return ours;
+ }
+
+ public int getType() {
+ return this.type;
+ }
+
+ public static List<JingleCandidate> parse(List<Element> canditates) {
+ List<JingleCandidate> parsedCandidates = new ArrayList<>();
+ for (Element c : canditates) {
+ parsedCandidates.add(JingleCandidate.parse(c));
+ }
+ return parsedCandidates;
+ }
+
+ public static JingleCandidate parse(Element candidate) {
+ JingleCandidate parsedCandidate = new JingleCandidate(
+ candidate.getAttribute("cid"), false);
+ parsedCandidate.setHost(candidate.getAttribute("host"));
+ parsedCandidate.setJid(candidate.getAttributeAsJid("jid"));
+ parsedCandidate.setType(candidate.getAttribute("type"));
+ parsedCandidate.setPriority(Integer.parseInt(candidate
+ .getAttribute("priority")));
+ parsedCandidate
+ .setPort(Integer.parseInt(candidate.getAttribute("port")));
+ return parsedCandidate;
+ }
+
+ public Element toElement() {
+ Element element = new Element("candidate");
+ element.setAttribute("cid", this.getCid());
+ element.setAttribute("host", this.getHost());
+ element.setAttribute("port", Integer.toString(this.getPort()));
+ element.setAttribute("jid", this.getJid().toString());
+ element.setAttribute("priority", Integer.toString(this.getPriority()));
+ if (this.getType() == TYPE_DIRECT) {
+ element.setAttribute("type", "direct");
+ } else if (this.getType() == TYPE_PROXY) {
+ element.setAttribute("type", "proxy");
+ }
+ return element;
+ }
+
+ public void flagAsUsedByCounterpart() {
+ this.usedByCounterpart = true;
+ }
+
+ public boolean isUsedByCounterpart() {
+ return this.usedByCounterpart;
+ }
+
+ public String toString() {
+ return this.getHost() + ":" + this.getPort() + " (prio="
+ + this.getPriority() + ")";
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java
new file mode 100644
index 00000000..fca0597f
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java
@@ -0,0 +1,1025 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import android.util.Pair;
+
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map.Entry;
+import java.util.concurrent.ConcurrentHashMap;
+
+import de.thedevstack.android.logcat.Logging;
+import de.thedevstack.conversationsplus.ConversationsPlusPreferences;
+import de.thedevstack.conversationsplus.utils.MessageUtil;
+import de.thedevstack.conversationsplus.utils.StreamUtil;
+import de.thedevstack.conversationsplus.utils.XmppConnectionServiceAccessor;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.crypto.axolotl.AxolotlService;
+import eu.siacs.conversations.crypto.axolotl.OnMessageCreatedCallback;
+import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.Transferable;
+import eu.siacs.conversations.entities.TransferablePlaceholder;
+import eu.siacs.conversations.persistance.FileBackend;
+import eu.siacs.conversations.services.AbstractConnectionManager;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.utils.FileUtils;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.OnIqPacketReceived;
+import eu.siacs.conversations.xmpp.jid.Jid;
+import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
+import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
+import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+public class JingleConnection implements Transferable {
+
+ private JingleConnectionManager mJingleConnectionManager;
+ private XmppConnectionService mXmppConnectionService;
+
+ protected static final int JINGLE_STATUS_INITIATED = 0;
+ protected static final int JINGLE_STATUS_ACCEPTED = 1;
+ protected static final int JINGLE_STATUS_FINISHED = 4;
+ protected static final int JINGLE_STATUS_TRANSMITTING = 5;
+ protected static final int JINGLE_STATUS_FAILED = 99;
+
+ private int ibbBlockSize = 8192;
+
+ private int mJingleStatus = -1;
+ private int mStatus = Transferable.STATUS_UNKNOWN;
+ private Message message;
+ private String sessionId;
+ private Account account;
+ private Jid initiator;
+ private Jid responder;
+ private List<JingleCandidate> candidates = new ArrayList<>();
+ private ConcurrentHashMap<String, JingleSocks5Transport> connections = new ConcurrentHashMap<>();
+
+ private String transportId;
+ private Element fileOffer;
+ private DownloadableFile file = null;
+
+ private String contentName;
+ private String contentCreator;
+
+ private int mProgress = 0;
+
+ private boolean receivedCandidate = false;
+ private boolean sentCandidate = false;
+
+ private boolean acceptedAutomatically = false;
+
+ private XmppAxolotlMessage mXmppAxolotlMessage;
+
+ private JingleTransport transport = null;
+
+ private OutputStream mFileOutputStream;
+ private InputStream mFileInputStream;
+
+ private OnIqPacketReceived responseListener = new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ if (packet.getType() != IqPacket.TYPE.RESULT) {
+ fail();
+ }
+ }
+ };
+
+ final OnFileTransmissionStatusChanged onFileTransmissionSatusChanged = new OnFileTransmissionStatusChanged() {
+
+ @Override
+ public void onFileTransmitted(DownloadableFile file) {
+ if (responder.equals(account.getJid())) {
+ sendSuccess();
+ MessageUtil.updateFileParams(message);
+ mXmppConnectionService.databaseBackend.createMessage(message);
+ MessageUtil.markMessage(message,Message.STATUS_RECEIVED);
+ if (acceptedAutomatically) {
+ message.markUnread();
+ JingleConnection.this.mXmppConnectionService.getNotificationService().push(message);
+ }
+ } else {
+ if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
+ file.delete();
+ }
+ }
+ Logging.d(Config.LOGTAG,"successfully transmitted file:" + file.getAbsolutePath()+" ("+file.getSha1Sum()+")");
+ if (message.getEncryption() != Message.ENCRYPTION_PGP) {
+ FileBackend.updateMediaScanner(file, mXmppConnectionService);
+ } else {
+ account.getPgpDecryptionService().add(message);
+ }
+ }
+
+ @Override
+ public void onFileTransferAborted() {
+ JingleConnection.this.sendCancel();
+ JingleConnection.this.fail();
+ }
+ };
+
+ public InputStream getFileInputStream() {
+ return this.mFileInputStream;
+ }
+
+ public OutputStream getFileOutputStream() {
+ return this.mFileOutputStream;
+ }
+
+ private OnProxyActivated onProxyActivated = new OnProxyActivated() {
+
+ @Override
+ public void success() {
+ if (initiator.equals(account.getJid())) {
+ Logging.d(Config.LOGTAG, "we were initiating. sending file");
+ transport.send(file, onFileTransmissionSatusChanged);
+ } else {
+ transport.receive(file, onFileTransmissionSatusChanged);
+ Logging.d(Config.LOGTAG, "we were responding. receiving file");
+ }
+ }
+
+ @Override
+ public void failed() {
+ Logging.d(Config.LOGTAG, "proxy activation failed");
+ }
+ };
+
+ public JingleConnection(JingleConnectionManager mJingleConnectionManager) {
+ this.mJingleConnectionManager = mJingleConnectionManager;
+ this.mXmppConnectionService = XmppConnectionServiceAccessor.xmppConnectionService;
+ }
+
+ public String getSessionId() {
+ return this.sessionId;
+ }
+
+ public Account getAccount() {
+ return this.account;
+ }
+
+ public Jid getCounterPart() {
+ return this.message.getCounterpart();
+ }
+
+ public void deliverPacket(JinglePacket packet) {
+ boolean returnResult = true;
+ if (packet.isAction("session-terminate")) {
+ Reason reason = packet.getReason();
+ if (reason != null) {
+ if (reason.hasChild("cancel")) {
+ this.fail();
+ } else if (reason.hasChild("success")) {
+ this.receiveSuccess();
+ } else {
+ this.fail();
+ }
+ } else {
+ this.fail();
+ }
+ } else if (packet.isAction("session-accept")) {
+ returnResult = receiveAccept(packet);
+ } else if (packet.isAction("transport-info")) {
+ returnResult = receiveTransportInfo(packet);
+ } else if (packet.isAction("transport-replace")) {
+ if (packet.getJingleContent().hasIbbTransport()) {
+ returnResult = this.receiveFallbackToIbb(packet);
+ } else {
+ returnResult = false;
+ Logging.d(Config.LOGTAG, "trying to fallback to something unknown"
+ + packet.toString());
+ }
+ } else if (packet.isAction("transport-accept")) {
+ returnResult = this.receiveTransportAccept(packet);
+ } else {
+ Logging.d(Config.LOGTAG, "packet arrived in connection. action was "
+ + packet.getAction());
+ returnResult = false;
+ }
+ IqPacket response;
+ if (returnResult) {
+ response = packet.generateResponse(IqPacket.TYPE.RESULT);
+
+ } else {
+ response = packet.generateResponse(IqPacket.TYPE.ERROR);
+ }
+ mXmppConnectionService.sendIqPacket(account,response,null);
+ }
+
+ public void init(final Message message) {
+ if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
+ Conversation conversation = message.getConversation();
+ conversation.getAccount().getAxolotlService().prepareKeyTransportMessage(conversation, new OnMessageCreatedCallback() {
+ @Override
+ public void run(XmppAxolotlMessage xmppAxolotlMessage) {
+ if (xmppAxolotlMessage != null) {
+ init(message, xmppAxolotlMessage);
+ } else {
+ fail();
+ }
+ }
+ });
+ } else {
+ init(message, null);
+ }
+ }
+
+ private void init(Message message, XmppAxolotlMessage xmppAxolotlMessage) {
+ this.mXmppAxolotlMessage = xmppAxolotlMessage;
+ this.contentCreator = "initiator";
+ this.contentName = this.mJingleConnectionManager.nextRandomId();
+ this.message = message;
+ this.message.setTransferable(this);
+ this.mStatus = Transferable.STATUS_UPLOADING;
+ this.account = message.getConversation().getAccount();
+ this.initiator = this.account.getJid();
+ this.responder = this.message.getCounterpart();
+ this.sessionId = this.mJingleConnectionManager.nextRandomId();
+ if (this.candidates.size() > 0) {
+ this.sendInitRequest();
+ } else {
+ this.mJingleConnectionManager.getPrimaryCandidate(account,
+ new OnPrimaryCandidateFound() {
+
+ @Override
+ public void onPrimaryCandidateFound(boolean success,
+ final JingleCandidate candidate) {
+ if (success) {
+ final JingleSocks5Transport socksConnection = new JingleSocks5Transport(
+ JingleConnection.this, candidate);
+ connections.put(candidate.getCid(),
+ socksConnection);
+ socksConnection
+ .connect(new OnTransportConnected() {
+
+ @Override
+ public void failed() {
+ Logging.d(Config.LOGTAG,
+ "connection to our own primary candidete failed");
+ sendInitRequest();
+ }
+
+ @Override
+ public void established() {
+ Logging.d(Config.LOGTAG,
+ "succesfully connected to our own primary candidate");
+ mergeCandidate(candidate);
+ sendInitRequest();
+ }
+ });
+ mergeCandidate(candidate);
+ } else {
+ Logging.d(Config.LOGTAG,
+ "no primary candidate of our own was found");
+ sendInitRequest();
+ }
+ }
+ });
+ }
+
+ }
+
+ public void init(Account account, JinglePacket packet) {
+ this.mJingleStatus = JINGLE_STATUS_INITIATED;
+ Conversation conversation = this.mXmppConnectionService
+ .findOrCreateConversation(account,
+ packet.getFrom().toBareJid(), false);
+ this.message = new Message(conversation, "", Message.ENCRYPTION_NONE);
+ this.message.setStatus(Message.STATUS_RECEIVED);
+ this.mStatus = Transferable.STATUS_OFFER;
+ this.message.setTransferable(this);
+ final Jid from = packet.getFrom();
+ this.message.setCounterpart(from);
+ this.account = account;
+ this.initiator = packet.getFrom();
+ this.responder = this.account.getJid();
+ this.sessionId = packet.getSessionId();
+ Content content = packet.getJingleContent();
+ this.contentCreator = content.getAttribute("creator");
+ this.contentName = content.getAttribute("name");
+ this.transportId = content.getTransportId();
+ this.mergeCandidates(JingleCandidate.parse(content.socks5transport().getChildren()));
+ this.fileOffer = packet.getJingleContent().getFileOffer();
+
+ mXmppConnectionService.sendIqPacket(account,packet.generateResponse(IqPacket.TYPE.RESULT),null);
+
+ if (fileOffer != null) {
+ Element encrypted = fileOffer.findChild("encrypted", AxolotlService.PEP_PREFIX);
+ if (encrypted != null) {
+ this.mXmppAxolotlMessage = XmppAxolotlMessage.fromElement(encrypted, packet.getFrom().toBareJid());
+ }
+ Element fileSize = fileOffer.findChild("size");
+ Element fileNameElement = fileOffer.findChild("name");
+ if (fileNameElement != null) {
+ String filename = fileNameElement.getContent()
+ .toLowerCase(Locale.US).toLowerCase();
+ final String lastPart = FileUtils.getLastExtension(filename);
+ final String secondToLastPart = FileUtils.getSecondToLastExtension(filename);
+ if (!lastPart.isEmpty()) {
+ if (VALID_IMAGE_EXTENSIONS.contains(lastPart)) {
+ message.setType(Message.TYPE_IMAGE);
+ message.setRelativeFilePath(message.getUuid()+"."+lastPart);
+ } else if (VALID_CRYPTO_EXTENSIONS.contains(lastPart)) {
+ if (!secondToLastPart.isEmpty()) {
+ if (VALID_IMAGE_EXTENSIONS.contains(secondToLastPart)) {
+ message.setType(Message.TYPE_IMAGE);
+ message.setRelativeFilePath(message.getUuid()+"."+secondToLastPart);
+ } else {
+ message.setType(Message.TYPE_FILE);
+ message.setRelativeFilePath(message.getUuid() + "_"
+ + filename.substring(0, filename.length() - (secondToLastPart.length() + 1)));
+ }
+ if (lastPart.equals("otr")) {
+ message.setEncryption(Message.ENCRYPTION_OTR);
+ } else {
+ message.setEncryption(Message.ENCRYPTION_PGP);
+ }
+ }
+ } else {
+ message.setType(Message.TYPE_FILE);
+ message.setRelativeFilePath(message.getUuid() + "_" + filename);
+ }
+ } else {
+ message.setType(Message.TYPE_FILE);
+ message.setRelativeFilePath(message.getUuid() + "_" + filename);
+ }
+
+ long size = Long.parseLong(fileSize.getContent());
+ message.setBody(Long.toString(size));
+ conversation.add(message);
+ mXmppConnectionService.updateConversationUi();
+ if (mJingleConnectionManager.hasStoragePermission()
+ && size <= ConversationsPlusPreferences.autoAcceptFileSize()
+ && mXmppConnectionService.isDownloadAllowedInConnection()) {
+ Logging.d(Config.LOGTAG, "auto accepting file from "+ packet.getFrom());
+ this.acceptedAutomatically = true;
+ this.sendAccept();
+ } else {
+ message.markUnread();
+ Logging.d(Config.LOGTAG,
+ "not auto accepting new file offer with size: "
+ + size
+ + " allowed size:"
+ + ConversationsPlusPreferences.autoAcceptFileSize());
+ this.mXmppConnectionService.getNotificationService().push(message);
+ }
+ this.file = FileBackend.getFile(message, false);
+ if (mXmppAxolotlMessage != null) {
+ XmppAxolotlMessage.XmppAxolotlKeyTransportMessage transportMessage = account.getAxolotlService().processReceivingKeyTransportMessage(mXmppAxolotlMessage);
+ if (transportMessage != null) {
+ message.setEncryption(Message.ENCRYPTION_AXOLOTL);
+ this.file.setKey(transportMessage.getKey());
+ this.file.setIv(transportMessage.getIv());
+ message.setFingerprint(transportMessage.getFingerprint());
+ } else {
+ Logging.d(Config.LOGTAG,"could not process KeyTransportMessage");
+ }
+ } else if (message.getEncryption() == Message.ENCRYPTION_OTR) {
+ byte[] key = conversation.getSymmetricKey();
+ if (key == null) {
+ this.sendCancel();
+ this.fail();
+ return;
+ } else {
+ this.file.setKeyAndIv(key);
+ }
+ }
+ this.mFileOutputStream = AbstractConnectionManager.createOutputStream(this.file,message.getEncryption() == Message.ENCRYPTION_AXOLOTL);
+ if (message.getEncryption() == Message.ENCRYPTION_OTR && Config.REPORT_WRONG_FILESIZE_IN_OTR_JINGLE) {
+ this.file.setExpectedSize((size / 16 + 1) * 16);
+ } else {
+ this.file.setExpectedSize(size);
+ }
+ Logging.d(Config.LOGTAG, "receiving file: expecting size of " + this.file.getExpectedSize());
+ } else {
+ this.sendCancel();
+ this.fail();
+ }
+ } else {
+ this.sendCancel();
+ this.fail();
+ }
+ }
+
+ private void sendInitRequest() {
+ JinglePacket packet = this.bootstrapPacket("session-initiate");
+ Content content = new Content(this.contentCreator, this.contentName);
+ if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) {
+ content.setTransportId(this.transportId);
+ this.file = FileBackend.getFile(message, false);
+ Pair<InputStream,Integer> pair;
+ try {
+ if (message.getEncryption() == Message.ENCRYPTION_OTR) {
+ Conversation conversation = this.message.getConversation();
+ if (!this.mXmppConnectionService.renewSymmetricKey(conversation)) {
+ Logging.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not set symmetric key");
+ cancel();
+ }
+ this.file.setKeyAndIv(conversation.getSymmetricKey());
+ pair = AbstractConnectionManager.createInputStream(this.file, false);
+ this.file.setExpectedSize(pair.second);
+ content.setFileOffer(this.file, true);
+ } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
+ this.file.setKey(mXmppAxolotlMessage.getInnerKey());
+ this.file.setIv(mXmppAxolotlMessage.getIV());
+ pair = AbstractConnectionManager.createInputStream(this.file, true);
+ this.file.setExpectedSize(pair.second);
+ content.setFileOffer(this.file, false).addChild(mXmppAxolotlMessage.toElement());
+ } else {
+ pair = AbstractConnectionManager.createInputStream(this.file, false);
+ this.file.setExpectedSize(pair.second);
+ content.setFileOffer(this.file, false);
+ }
+ } catch (FileNotFoundException e) {
+ cancel();
+ return;
+ }
+ this.mFileInputStream = pair.first;
+ this.transportId = this.mJingleConnectionManager.nextRandomId();
+ content.setTransportId(this.transportId);
+ content.socks5transport().setChildren(getCandidatesAsElements());
+ packet.setContent(content);
+ this.sendJinglePacket(packet,new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE.RESULT) {
+ Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": other party received offer");
+ mJingleStatus = JINGLE_STATUS_INITIATED;
+ MessageUtil.markMessage(message, Message.STATUS_OFFERED);
+ } else {
+ fail();
+ }
+ }
+ });
+
+ }
+ }
+
+ private List<Element> getCandidatesAsElements() {
+ List<Element> elements = new ArrayList<>();
+ for (JingleCandidate c : this.candidates) {
+ if (c.isOurs()) {
+ elements.add(c.toElement());
+ }
+ }
+ return elements;
+ }
+
+ private void sendAccept() {
+ mJingleStatus = JINGLE_STATUS_ACCEPTED;
+ this.mStatus = Transferable.STATUS_DOWNLOADING;
+ mXmppConnectionService.updateConversationUi();
+ this.mJingleConnectionManager.getPrimaryCandidate(this.account, new OnPrimaryCandidateFound() {
+ @Override
+ public void onPrimaryCandidateFound(boolean success, final JingleCandidate candidate) {
+ final JinglePacket packet = bootstrapPacket("session-accept");
+ final Content content = new Content(contentCreator,contentName);
+ content.setFileOffer(fileOffer);
+ content.setTransportId(transportId);
+ if (success && candidate != null && !equalCandidateExists(candidate)) {
+ final JingleSocks5Transport socksConnection = new JingleSocks5Transport(
+ JingleConnection.this,
+ candidate);
+ connections.put(candidate.getCid(), socksConnection);
+ socksConnection.connect(new OnTransportConnected() {
+
+ @Override
+ public void failed() {
+ Logging.d(Config.LOGTAG,"connection to our own primary candidate failed");
+ content.socks5transport().setChildren(getCandidatesAsElements());
+ packet.setContent(content);
+ sendJinglePacket(packet);
+ connectNextCandidate();
+ }
+
+ @Override
+ public void established() {
+ Logging.d(Config.LOGTAG, "connected to primary candidate");
+ mergeCandidate(candidate);
+ content.socks5transport().setChildren(getCandidatesAsElements());
+ packet.setContent(content);
+ sendJinglePacket(packet);
+ connectNextCandidate();
+ }
+ });
+ } else {
+ Logging.d(Config.LOGTAG,"did not find a primary candidate for ourself");
+ content.socks5transport().setChildren(getCandidatesAsElements());
+ packet.setContent(content);
+ sendJinglePacket(packet);
+ connectNextCandidate();
+ }
+ }
+ });
+ }
+
+ private JinglePacket bootstrapPacket(String action) {
+ JinglePacket packet = new JinglePacket();
+ packet.setAction(action);
+ packet.setFrom(account.getJid());
+ packet.setTo(this.message.getCounterpart());
+ packet.setSessionId(this.sessionId);
+ packet.setInitiator(this.initiator);
+ return packet;
+ }
+
+ private void sendJinglePacket(JinglePacket packet) {
+ mXmppConnectionService.sendIqPacket(account,packet,responseListener);
+ }
+
+ private void sendJinglePacket(JinglePacket packet, OnIqPacketReceived callback) {
+ mXmppConnectionService.sendIqPacket(account,packet,callback);
+ }
+
+ private boolean receiveAccept(JinglePacket packet) {
+ Content content = packet.getJingleContent();
+ mergeCandidates(JingleCandidate.parse(content.socks5transport()
+ .getChildren()));
+ this.mJingleStatus = JINGLE_STATUS_ACCEPTED;
+ MessageUtil.markMessage(message, Message.STATUS_UNSEND);
+ this.connectNextCandidate();
+ return true;
+ }
+
+ private boolean receiveTransportInfo(JinglePacket packet) {
+ Content content = packet.getJingleContent();
+ if (content.hasSocks5Transport()) {
+ if (content.socks5transport().hasChild("activated")) {
+ if ((this.transport != null) && (this.transport instanceof JingleSocks5Transport)) {
+ onProxyActivated.success();
+ } else {
+ String cid = content.socks5transport().findChild("activated").getAttribute("cid");
+ Logging.d(Config.LOGTAG, "received proxy activated (" + cid
+ + ")prior to choosing our own transport");
+ JingleSocks5Transport connection = this.connections.get(cid);
+ if (connection != null) {
+ connection.setActivated(true);
+ } else {
+ Logging.d(Config.LOGTAG, "activated connection not found");
+ this.sendCancel();
+ this.fail();
+ }
+ }
+ return true;
+ } else if (content.socks5transport().hasChild("proxy-error")) {
+ onProxyActivated.failed();
+ return true;
+ } else if (content.socks5transport().hasChild("candidate-error")) {
+ Logging.d(Config.LOGTAG, "received candidate error");
+ this.receivedCandidate = true;
+ if ((mJingleStatus == JINGLE_STATUS_ACCEPTED)
+ && (this.sentCandidate)) {
+ this.connect();
+ }
+ return true;
+ } else if (content.socks5transport().hasChild("candidate-used")) {
+ String cid = content.socks5transport()
+ .findChild("candidate-used").getAttribute("cid");
+ if (cid != null) {
+ Logging.d(Config.LOGTAG, "candidate used by counterpart:" + cid);
+ JingleCandidate candidate = getCandidate(cid);
+ candidate.flagAsUsedByCounterpart();
+ this.receivedCandidate = true;
+ if ((mJingleStatus == JINGLE_STATUS_ACCEPTED)
+ && (this.sentCandidate)) {
+ this.connect();
+ } else {
+ Logging.d(Config.LOGTAG,
+ "ignoring because file is already in transmission or we havent sent our candidate yet");
+ }
+ return true;
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ } else {
+ return true;
+ }
+ }
+
+ private void connect() {
+ final JingleSocks5Transport connection = chooseConnection();
+ this.transport = connection;
+ if (connection == null) {
+ Logging.d(Config.LOGTAG, "could not find suitable candidate");
+ this.disconnectSocks5Connections();
+ if (this.initiator.equals(account.getJid())) {
+ this.sendFallbackToIbb();
+ }
+ } else {
+ this.mJingleStatus = JINGLE_STATUS_TRANSMITTING;
+ if (connection.needsActivation()) {
+ if (connection.getCandidate().isOurs()) {
+ Logging.d(Config.LOGTAG, "candidate "
+ + connection.getCandidate().getCid()
+ + " was our proxy. going to activate");
+ IqPacket activation = new IqPacket(IqPacket.TYPE.SET);
+ activation.setTo(connection.getCandidate().getJid());
+ activation.query("http://jabber.org/protocol/bytestreams")
+ .setAttribute("sid", this.getSessionId());
+ activation.query().addChild("activate")
+ .setContent(this.getCounterPart().toString());
+ mXmppConnectionService.sendIqPacket(account,activation,
+ new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account,
+ IqPacket packet) {
+ if (packet.getType() != IqPacket.TYPE.RESULT) {
+ onProxyActivated.failed();
+ } else {
+ onProxyActivated.success();
+ sendProxyActivated(connection.getCandidate().getCid());
+ }
+ }
+ });
+ } else {
+ Logging.d(Config.LOGTAG,
+ "candidate "
+ + connection.getCandidate().getCid()
+ + " was a proxy. waiting for other party to activate");
+ }
+ } else {
+ if (initiator.equals(account.getJid())) {
+ Logging.d(Config.LOGTAG, "we were initiating. sending file");
+ connection.send(file, onFileTransmissionSatusChanged);
+ } else {
+ Logging.d(Config.LOGTAG, "we were responding. receiving file");
+ connection.receive(file, onFileTransmissionSatusChanged);
+ }
+ }
+ }
+ }
+
+ private JingleSocks5Transport chooseConnection() {
+ JingleSocks5Transport connection = null;
+ for (Entry<String, JingleSocks5Transport> cursor : connections
+ .entrySet()) {
+ JingleSocks5Transport currentConnection = cursor.getValue();
+ // Logging.d(Config.LOGTAG,"comparing candidate: "+currentConnection.getCandidate().toString());
+ if (currentConnection.isEstablished()
+ && (currentConnection.getCandidate().isUsedByCounterpart() || (!currentConnection
+ .getCandidate().isOurs()))) {
+ // Logging.d(Config.LOGTAG,"is usable");
+ if (connection == null) {
+ connection = currentConnection;
+ } else {
+ if (connection.getCandidate().getPriority() < currentConnection
+ .getCandidate().getPriority()) {
+ connection = currentConnection;
+ } else if (connection.getCandidate().getPriority() == currentConnection
+ .getCandidate().getPriority()) {
+ // Logging.d(Config.LOGTAG,"found two candidates with same priority");
+ if (initiator.equals(account.getJid())) {
+ if (currentConnection.getCandidate().isOurs()) {
+ connection = currentConnection;
+ }
+ } else {
+ if (!currentConnection.getCandidate().isOurs()) {
+ connection = currentConnection;
+ }
+ }
+ }
+ }
+ }
+ }
+ return connection;
+ }
+
+ private void sendSuccess() {
+ JinglePacket packet = bootstrapPacket("session-terminate");
+ Reason reason = new Reason();
+ reason.addChild("success");
+ packet.setReason(reason);
+ this.sendJinglePacket(packet);
+ this.disconnectSocks5Connections();
+ this.mJingleStatus = JINGLE_STATUS_FINISHED;
+ this.message.setStatus(Message.STATUS_RECEIVED);
+ this.message.setTransferable(null);
+ this.mXmppConnectionService.updateMessage(message);
+ this.mJingleConnectionManager.finishConnection(this);
+ }
+
+ private void sendFallbackToIbb() {
+ Logging.d(Config.LOGTAG, "sending fallback to ibb");
+ JinglePacket packet = this.bootstrapPacket("transport-replace");
+ Content content = new Content(this.contentCreator, this.contentName);
+ this.transportId = this.mJingleConnectionManager.nextRandomId();
+ content.setTransportId(this.transportId);
+ content.ibbTransport().setAttribute("block-size",
+ Integer.toString(this.ibbBlockSize));
+ packet.setContent(content);
+ this.sendJinglePacket(packet);
+ }
+
+ private boolean receiveFallbackToIbb(JinglePacket packet) {
+ Logging.d(Config.LOGTAG, "receiving fallack to ibb");
+ String receivedBlockSize = packet.getJingleContent().ibbTransport()
+ .getAttribute("block-size");
+ if (receivedBlockSize != null) {
+ int bs = Integer.parseInt(receivedBlockSize);
+ if (bs > this.ibbBlockSize) {
+ this.ibbBlockSize = bs;
+ }
+ }
+ this.transportId = packet.getJingleContent().getTransportId();
+ this.transport = new JingleInbandTransport(this, this.transportId, this.ibbBlockSize);
+ this.transport.receive(file, onFileTransmissionSatusChanged);
+ JinglePacket answer = bootstrapPacket("transport-accept");
+ Content content = new Content("initiator", "a-file-offer");
+ content.setTransportId(this.transportId);
+ content.ibbTransport().setAttribute("block-size",this.ibbBlockSize);
+ answer.setContent(content);
+ this.sendJinglePacket(answer);
+ return true;
+ }
+
+ private boolean receiveTransportAccept(JinglePacket packet) {
+ if (packet.getJingleContent().hasIbbTransport()) {
+ String receivedBlockSize = packet.getJingleContent().ibbTransport()
+ .getAttribute("block-size");
+ if (receivedBlockSize != null) {
+ int bs = Integer.parseInt(receivedBlockSize);
+ if (bs > this.ibbBlockSize) {
+ this.ibbBlockSize = bs;
+ }
+ }
+ this.transport = new JingleInbandTransport(this, this.transportId, this.ibbBlockSize);
+ this.transport.connect(new OnTransportConnected() {
+
+ @Override
+ public void failed() {
+ Logging.d(Config.LOGTAG, "ibb open failed");
+ }
+
+ @Override
+ public void established() {
+ JingleConnection.this.transport.send(file,
+ onFileTransmissionSatusChanged);
+ }
+ });
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ private void receiveSuccess() {
+ this.mJingleStatus = JINGLE_STATUS_FINISHED;
+ MessageUtil.markMessage(this.message,Message.STATUS_SEND_RECEIVED);
+ this.disconnectSocks5Connections();
+ if (this.transport != null && this.transport instanceof JingleInbandTransport) {
+ this.transport.disconnect();
+ }
+ this.message.setTransferable(null);
+ this.mJingleConnectionManager.finishConnection(this);
+ }
+
+ public void cancel() {
+ this.disconnectSocks5Connections();
+ if (this.transport != null && this.transport instanceof JingleInbandTransport) {
+ this.transport.disconnect();
+ }
+ this.sendCancel();
+ this.mJingleConnectionManager.finishConnection(this);
+ if (this.responder.equals(account.getJid())) {
+ this.message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_FAILED));
+ if (this.file!=null) {
+ file.delete();
+ }
+ this.mXmppConnectionService.updateConversationUi();
+ } else {
+ MessageUtil.markMessage(this.message, Message.STATUS_SEND_FAILED);
+ this.message.setTransferable(null);
+ }
+ }
+
+ private void fail() {
+ this.mJingleStatus = JINGLE_STATUS_FAILED;
+ this.disconnectSocks5Connections();
+ if (this.transport != null && this.transport instanceof JingleInbandTransport) {
+ this.transport.disconnect();
+ }
+ StreamUtil.close(mFileInputStream);
+ StreamUtil.close(mFileOutputStream);
+ if (this.message != null) {
+ if (this.responder.equals(account.getJid())) {
+ this.message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_FAILED));
+ if (this.file!=null) {
+ file.delete();
+ }
+ this.mXmppConnectionService.updateConversationUi();
+ } else {
+ MessageUtil.markMessage(this.message, Message.STATUS_SEND_FAILED);
+ this.message.setTransferable(null);
+ }
+ }
+ this.mJingleConnectionManager.finishConnection(this);
+ }
+
+ private void sendCancel() {
+ JinglePacket packet = bootstrapPacket("session-terminate");
+ Reason reason = new Reason();
+ reason.addChild("cancel");
+ packet.setReason(reason);
+ this.sendJinglePacket(packet);
+ }
+
+ private void connectNextCandidate() {
+ for (JingleCandidate candidate : this.candidates) {
+ if ((!connections.containsKey(candidate.getCid()) && (!candidate
+ .isOurs()))) {
+ this.connectWithCandidate(candidate);
+ return;
+ }
+ }
+ this.sendCandidateError();
+ }
+
+ private void connectWithCandidate(final JingleCandidate candidate) {
+ final JingleSocks5Transport socksConnection = new JingleSocks5Transport(
+ this, candidate);
+ connections.put(candidate.getCid(), socksConnection);
+ socksConnection.connect(new OnTransportConnected() {
+
+ @Override
+ public void failed() {
+ Logging.d(Config.LOGTAG,
+ "connection failed with " + candidate.getHost() + ":"
+ + candidate.getPort());
+ connectNextCandidate();
+ }
+
+ @Override
+ public void established() {
+ Logging.d(Config.LOGTAG,
+ "established connection with " + candidate.getHost()
+ + ":" + candidate.getPort());
+ sendCandidateUsed(candidate.getCid());
+ }
+ });
+ }
+
+ private void disconnectSocks5Connections() {
+ Iterator<Entry<String, JingleSocks5Transport>> it = this.connections
+ .entrySet().iterator();
+ while (it.hasNext()) {
+ Entry<String, JingleSocks5Transport> pairs = it.next();
+ pairs.getValue().disconnect();
+ it.remove();
+ }
+ }
+
+ private void sendProxyActivated(String cid) {
+ JinglePacket packet = bootstrapPacket("transport-info");
+ Content content = new Content(this.contentCreator, this.contentName);
+ content.setTransportId(this.transportId);
+ content.socks5transport().addChild("activated")
+ .setAttribute("cid", cid);
+ packet.setContent(content);
+ this.sendJinglePacket(packet);
+ }
+
+ private void sendCandidateUsed(final String cid) {
+ JinglePacket packet = bootstrapPacket("transport-info");
+ Content content = new Content(this.contentCreator, this.contentName);
+ content.setTransportId(this.transportId);
+ content.socks5transport().addChild("candidate-used")
+ .setAttribute("cid", cid);
+ packet.setContent(content);
+ this.sentCandidate = true;
+ if ((receivedCandidate) && (mJingleStatus == JINGLE_STATUS_ACCEPTED)) {
+ connect();
+ }
+ this.sendJinglePacket(packet);
+ }
+
+ private void sendCandidateError() {
+ JinglePacket packet = bootstrapPacket("transport-info");
+ Content content = new Content(this.contentCreator, this.contentName);
+ content.setTransportId(this.transportId);
+ content.socks5transport().addChild("candidate-error");
+ packet.setContent(content);
+ this.sentCandidate = true;
+ if ((receivedCandidate) && (mJingleStatus == JINGLE_STATUS_ACCEPTED)) {
+ connect();
+ }
+ this.sendJinglePacket(packet);
+ }
+
+ public Jid getInitiator() {
+ return this.initiator;
+ }
+
+ public Jid getResponder() {
+ return this.responder;
+ }
+
+ public int getJingleStatus() {
+ return this.mJingleStatus;
+ }
+
+ private boolean equalCandidateExists(JingleCandidate candidate) {
+ for (JingleCandidate c : this.candidates) {
+ if (c.equalValues(candidate)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void mergeCandidate(JingleCandidate candidate) {
+ for (JingleCandidate c : this.candidates) {
+ if (c.equals(candidate)) {
+ return;
+ }
+ }
+ this.candidates.add(candidate);
+ }
+
+ private void mergeCandidates(List<JingleCandidate> candidates) {
+ for (JingleCandidate c : candidates) {
+ mergeCandidate(c);
+ }
+ }
+
+ private JingleCandidate getCandidate(String cid) {
+ for (JingleCandidate c : this.candidates) {
+ if (c.getCid().equals(cid)) {
+ return c;
+ }
+ }
+ return null;
+ }
+
+ public void updateProgress(int i) {
+ this.mProgress = i;
+ mXmppConnectionService.updateConversationUi();
+ }
+
+ interface OnProxyActivated {
+ public void success();
+
+ public void failed();
+ }
+
+ public boolean hasTransportId(String sid) {
+ return sid.equals(this.transportId);
+ }
+
+ public JingleTransport getTransport() {
+ return this.transport;
+ }
+
+ public boolean start() {
+ if (account.getStatus() == Account.State.ONLINE) {
+ if (mJingleStatus == JINGLE_STATUS_INITIATED) {
+ new Thread(new Runnable() {
+
+ @Override
+ public void run() {
+ sendAccept();
+ }
+ }).start();
+ }
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public int getStatus() {
+ return this.mStatus;
+ }
+
+ @Override
+ public long getFileSize() {
+ if (this.file != null) {
+ return this.file.getExpectedSize();
+ } else {
+ return 0;
+ }
+ }
+
+ @Override
+ public int getProgress() {
+ return this.mProgress;
+ }
+
+ public AbstractConnectionManager getConnectionManager() {
+ return this.mJingleConnectionManager;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java
new file mode 100644
index 00000000..19ac354d
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java
@@ -0,0 +1,170 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import android.annotation.SuppressLint;
+
+import java.math.BigInteger;
+import java.security.SecureRandom;
+import java.util.HashMap;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import de.thedevstack.android.logcat.Logging;
+
+import de.thedevstack.conversationsplus.utils.MessageUtil;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.Transferable;
+import eu.siacs.conversations.services.AbstractConnectionManager;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.utils.Xmlns;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.OnIqPacketReceived;
+import eu.siacs.conversations.xmpp.jid.Jid;
+import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+public class JingleConnectionManager extends AbstractConnectionManager {
+ private List<JingleConnection> connections = new CopyOnWriteArrayList<>();
+
+ private HashMap<Jid, JingleCandidate> primaryCandidates = new HashMap<>();
+
+ @SuppressLint("TrulyRandom")
+ private SecureRandom random = new SecureRandom();
+
+ public void deliverPacket(Account account, JinglePacket packet) {
+ if (packet.isAction("session-initiate")) {
+ JingleConnection connection = new JingleConnection(this);
+ connection.init(account, packet);
+ connections.add(connection);
+ } else {
+ for (JingleConnection connection : connections) {
+ if (connection.getAccount() == account
+ && connection.getSessionId().equals(
+ packet.getSessionId())
+ && connection.getCounterPart().equals(packet.getFrom())) {
+ connection.deliverPacket(packet);
+ return;
+ }
+ }
+ IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR);
+ Element error = response.addChild("error");
+ error.setAttribute("type", "cancel");
+ error.addChild("item-not-found",
+ "urn:ietf:params:xml:ns:xmpp-stanzas");
+ error.addChild("unknown-session", "urn:xmpp:jingle:errors:1");
+ account.getXmppConnection().sendIqPacket(response, null);
+ }
+ }
+
+ public JingleConnection createNewConnection(Message message) {
+ Transferable old = message.getTransferable();
+ if (old != null) {
+ old.cancel();
+ }
+ JingleConnection connection = new JingleConnection(this);
+ MessageUtil.markMessage(message,Message.STATUS_WAITING);
+ connection.init(message);
+ this.connections.add(connection);
+ return connection;
+ }
+
+ public JingleConnection createNewConnection(final JinglePacket packet) {
+ JingleConnection connection = new JingleConnection(this);
+ this.connections.add(connection);
+ return connection;
+ }
+
+ public void finishConnection(JingleConnection connection) {
+ this.connections.remove(connection);
+ }
+
+ public void getPrimaryCandidate(Account account,
+ final OnPrimaryCandidateFound listener) {
+ if (Config.DISABLE_PROXY_LOOKUP) {
+ listener.onPrimaryCandidateFound(false, null);
+ return;
+ }
+ if (!this.primaryCandidates.containsKey(account.getJid().toBareJid())) {
+ final Jid proxy = account.getXmppConnection().findDiscoItemByFeature(Xmlns.BYTE_STREAMS);
+ if (proxy != null) {
+ IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
+ iq.setTo(proxy);
+ iq.query(Xmlns.BYTE_STREAMS);
+ account.getXmppConnection().sendIqPacket(iq,new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ Element streamhost = packet.query().findChild("streamhost",Xmlns.BYTE_STREAMS);
+ final String host = streamhost == null ? null : streamhost.getAttribute("host");
+ final String port = streamhost == null ? null : streamhost.getAttribute("port");
+ if (host != null && port != null) {
+ try {
+ JingleCandidate candidate = new JingleCandidate(nextRandomId(), true);
+ candidate.setHost(host);
+ candidate.setPort(Integer.parseInt(port));
+ candidate.setType(JingleCandidate.TYPE_PROXY);
+ candidate.setJid(proxy);
+ candidate.setPriority(655360 + 65535);
+ primaryCandidates.put(account.getJid().toBareJid(),candidate);
+ listener.onPrimaryCandidateFound(true,candidate);
+ } catch (final NumberFormatException e) {
+ listener.onPrimaryCandidateFound(false,null);
+ return;
+ }
+ } else {
+ listener.onPrimaryCandidateFound(false,null);
+ }
+ }
+ });
+ } else {
+ listener.onPrimaryCandidateFound(false, null);
+ }
+
+ } else {
+ listener.onPrimaryCandidateFound(true,
+ this.primaryCandidates.get(account.getJid().toBareJid()));
+ }
+ }
+
+ public String nextRandomId() {
+ return new BigInteger(50, random).toString(32);
+ }
+
+ public void deliverIbbPacket(Account account, IqPacket packet) {
+ String sid = null;
+ Element payload = null;
+ if (packet.hasChild("open", "http://jabber.org/protocol/ibb")) {
+ payload = packet.findChild("open", "http://jabber.org/protocol/ibb");
+ sid = payload.getAttribute("sid");
+ } else if (packet.hasChild("data", "http://jabber.org/protocol/ibb")) {
+ payload = packet.findChild("data", "http://jabber.org/protocol/ibb");
+ sid = payload.getAttribute("sid");
+ }
+ if (sid != null) {
+ for (JingleConnection connection : connections) {
+ if (connection.getAccount() == account
+ && connection.hasTransportId(sid)) {
+ JingleTransport transport = connection.getTransport();
+ if (transport instanceof JingleInbandTransport) {
+ JingleInbandTransport inbandTransport = (JingleInbandTransport) transport;
+ inbandTransport.deliverPayload(packet, payload);
+ return;
+ }
+ }
+ }
+ Logging.d(Config.LOGTAG,"couldn't deliver payload: " + payload.toString());
+ } else {
+ Logging.d(Config.LOGTAG, "no sid found in incoming ibb packet");
+ }
+ }
+
+ public void cancelInTransmission() {
+ for (JingleConnection connection : this.connections) {
+ if (connection.getJingleStatus() == JingleConnection.JINGLE_STATUS_TRANSMITTING) {
+ connection.cancel();
+ }
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java
new file mode 100644
index 00000000..3800b94f
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java
@@ -0,0 +1,239 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import android.util.Base64;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+import de.thedevstack.android.logcat.Logging;
+import de.thedevstack.conversationsplus.utils.StreamUtil;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.OnIqPacketReceived;
+import eu.siacs.conversations.xmpp.jid.Jid;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+public class JingleInbandTransport extends JingleTransport {
+
+ private Account account;
+ private Jid counterpart;
+ private int blockSize;
+ private int seq = 0;
+ private String sessionId;
+
+ private boolean established = false;
+
+ private boolean connected = true;
+
+ private DownloadableFile file;
+ private JingleConnection connection;
+
+ private InputStream fileInputStream = null;
+ private OutputStream fileOutputStream = null;
+ private long remainingSize = 0;
+ private long fileSize = 0;
+ private MessageDigest digest;
+
+ private OnFileTransmissionStatusChanged onFileTransmissionStatusChanged;
+
+ private OnIqPacketReceived onAckReceived = new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ if (connected && packet.getType() == IqPacket.TYPE.RESULT) {
+ sendNextBlock();
+ }
+ }
+ };
+
+ public JingleInbandTransport(final JingleConnection connection, final String sid, final int blocksize) {
+ this.connection = connection;
+ this.account = connection.getAccount();
+ this.counterpart = connection.getCounterPart();
+ this.blockSize = blocksize;
+ this.sessionId = sid;
+ }
+
+ public void connect(final OnTransportConnected callback) {
+ IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
+ iq.setTo(this.counterpart);
+ Element open = iq.addChild("open", "http://jabber.org/protocol/ibb");
+ open.setAttribute("sid", this.sessionId);
+ open.setAttribute("stanza", "iq");
+ open.setAttribute("block-size", Integer.toString(this.blockSize));
+ this.connected = true;
+ this.account.getXmppConnection().sendIqPacket(iq,
+ new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account,
+ IqPacket packet) {
+ if (packet.getType() != IqPacket.TYPE.RESULT) {
+ callback.failed();
+ } else {
+ callback.established();
+ }
+ }
+ });
+ }
+
+ @Override
+ public void receive(DownloadableFile file,
+ OnFileTransmissionStatusChanged callback) {
+ this.onFileTransmissionStatusChanged = callback;
+ this.file = file;
+ try {
+ this.digest = MessageDigest.getInstance("SHA-1");
+ digest.reset();
+ file.getParentFile().mkdirs();
+ file.createNewFile();
+ this.fileOutputStream = connection.getFileOutputStream();
+ if (this.fileOutputStream == null) {
+ Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": could not create output stream");
+ callback.onFileTransferAborted();
+ return;
+ }
+ this.remainingSize = this.fileSize = file.getExpectedSize();
+ } catch (final NoSuchAlgorithmException | IOException e) {
+ Logging.d(Config.LOGTAG,account.getJid().toBareJid()+" "+e.getMessage());
+ callback.onFileTransferAborted();
+ }
+ }
+
+ @Override
+ public void send(DownloadableFile file,
+ OnFileTransmissionStatusChanged callback) {
+ this.onFileTransmissionStatusChanged = callback;
+ this.file = file;
+ try {
+ this.remainingSize = this.file.getExpectedSize();
+ this.fileSize = this.remainingSize;
+ this.digest = MessageDigest.getInstance("SHA-1");
+ this.digest.reset();
+ fileInputStream = connection.getFileInputStream();
+ if (fileInputStream == null) {
+ Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": could no create input stream");
+ callback.onFileTransferAborted();
+ return;
+ }
+ if (this.connected) {
+ this.sendNextBlock();
+ }
+ } catch (NoSuchAlgorithmException e) {
+ callback.onFileTransferAborted();
+ Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": "+e.getMessage());
+ }
+ }
+
+ @Override
+ public void disconnect() {
+ this.connected = false;
+ if (this.fileOutputStream != null) {
+ try {
+ this.fileOutputStream.close();
+ } catch (IOException e) {
+
+ }
+ }
+ if (this.fileInputStream != null) {
+ try {
+ this.fileInputStream.close();
+ } catch (IOException e) {
+
+ }
+ }
+ }
+
+ private void sendNextBlock() {
+ byte[] buffer = new byte[this.blockSize];
+ try {
+ int count = fileInputStream.read(buffer);
+ if (count == -1) {
+ file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest()));
+ this.onFileTransmissionStatusChanged.onFileTransmitted(file);
+ fileInputStream.close();
+ return;
+ } else if (count != buffer.length) {
+ int rem = fileInputStream.read(buffer,count,buffer.length-count);
+ if (rem > 0) {
+ count += rem;
+ }
+ }
+ this.remainingSize -= count;
+ this.digest.update(buffer,0,count);
+ String base64 = Base64.encodeToString(buffer,0,count, Base64.NO_WRAP);
+ IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
+ iq.setTo(this.counterpart);
+ Element data = iq.addChild("data", "http://jabber.org/protocol/ibb");
+ data.setAttribute("seq", Integer.toString(this.seq));
+ data.setAttribute("block-size", Integer.toString(this.blockSize));
+ data.setAttribute("sid", this.sessionId);
+ data.setContent(base64);
+ this.account.getXmppConnection().sendIqPacket(iq, this.onAckReceived);
+ this.account.getXmppConnection().r(); //don't fill up stanza queue too much
+ this.seq++;
+ if (this.remainingSize > 0) {
+ connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100));
+ } else {
+ file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest()));
+ this.onFileTransmissionStatusChanged.onFileTransmitted(file);
+ fileInputStream.close();
+ }
+ } catch (IOException e) {
+ Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": "+e.getMessage());
+ StreamUtil.close(fileInputStream);
+ this.onFileTransmissionStatusChanged.onFileTransferAborted();
+ }
+ }
+
+ private void receiveNextBlock(String data) {
+ try {
+ byte[] buffer = Base64.decode(data, Base64.NO_WRAP);
+ if (this.remainingSize < buffer.length) {
+ buffer = Arrays.copyOfRange(buffer, 0, (int) this.remainingSize);
+ }
+ this.remainingSize -= buffer.length;
+ this.fileOutputStream.write(buffer);
+ this.digest.update(buffer);
+ if (this.remainingSize <= 0) {
+ file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest()));
+ fileOutputStream.flush();
+ fileOutputStream.close();
+ this.onFileTransmissionStatusChanged.onFileTransmitted(file);
+ } else {
+ connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100));
+ }
+ } catch (IOException e) {
+ Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": "+e.getMessage());
+ StreamUtil.close(fileOutputStream);
+ this.onFileTransmissionStatusChanged.onFileTransferAborted();
+ }
+ }
+
+ public void deliverPayload(IqPacket packet, Element payload) {
+ if (payload.getName().equals("open")) {
+ if (!established) {
+ established = true;
+ connected = true;
+ this.receiveNextBlock("");
+ this.account.getXmppConnection().sendIqPacket(
+ packet.generateResponse(IqPacket.TYPE.RESULT), null);
+ } else {
+ this.account.getXmppConnection().sendIqPacket(
+ packet.generateResponse(IqPacket.TYPE.ERROR), null);
+ }
+ } else if (connected && payload.getName().equals("data")) {
+ this.receiveNextBlock(payload.getContent());
+ this.account.getXmppConnection().sendIqPacket(
+ packet.generateResponse(IqPacket.TYPE.RESULT), null);
+ } else {
+ // TODO some sort of exception
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java
new file mode 100644
index 00000000..318a35f7
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java
@@ -0,0 +1,217 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import android.os.PowerManager;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.SocketAddress;
+import java.net.UnknownHostException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+import de.thedevstack.android.logcat.Logging;
+import de.thedevstack.conversationsplus.ConversationsPlusApplication;
+import de.thedevstack.conversationsplus.utils.StreamUtil;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.utils.SocksSocketFactory;
+
+public class JingleSocks5Transport extends JingleTransport {
+ private JingleCandidate candidate;
+ private JingleConnection connection;
+ private String destination;
+ private OutputStream outputStream;
+ private InputStream inputStream;
+ private boolean isEstablished = false;
+ private boolean activated = false;
+ protected Socket socket;
+
+ public JingleSocks5Transport(JingleConnection jingleConnection,
+ JingleCandidate candidate) {
+ this.candidate = candidate;
+ this.connection = jingleConnection;
+ try {
+ MessageDigest mDigest = MessageDigest.getInstance("SHA-1");
+ StringBuilder destBuilder = new StringBuilder();
+ destBuilder.append(jingleConnection.getSessionId());
+ if (candidate.isOurs()) {
+ destBuilder.append(jingleConnection.getAccount().getJid());
+ destBuilder.append(jingleConnection.getCounterPart());
+ } else {
+ destBuilder.append(jingleConnection.getCounterPart());
+ destBuilder.append(jingleConnection.getAccount().getJid());
+ }
+ mDigest.reset();
+ this.destination = CryptoHelper.bytesToHex(mDigest
+ .digest(destBuilder.toString().getBytes()));
+ } catch (NoSuchAlgorithmException e) {
+
+ }
+ }
+
+ public void connect(final OnTransportConnected callback) {
+ new Thread(new Runnable() {
+
+ @Override
+ public void run() {
+ try {
+ socket = new Socket();
+ SocketAddress address = new InetSocketAddress(candidate.getHost(),candidate.getPort());
+ socket.connect(address,Config.SOCKET_TIMEOUT * 1000);
+
+ inputStream = socket.getInputStream();
+ outputStream = socket.getOutputStream();
+ SocksSocketFactory.createSocksConnection(socket,destination,0);
+ isEstablished = true;
+ callback.established();
+ } catch (UnknownHostException e) {
+ callback.failed();
+ } catch (IOException e) {
+ callback.failed();
+ }
+ }
+ }).start();
+
+ }
+
+ public void send(final DownloadableFile file, final OnFileTransmissionStatusChanged callback) {
+ new Thread(new Runnable() {
+
+ @Override
+ public void run() {
+ InputStream fileInputStream = null;
+ final PowerManager.WakeLock wakeLock = ConversationsPlusApplication.createPartialWakeLock("jingle_send_"+connection.getSessionId());
+ try {
+ wakeLock.acquire();
+ MessageDigest digest = MessageDigest.getInstance("SHA-1");
+ digest.reset();
+ fileInputStream = connection.getFileInputStream();
+ if (fileInputStream == null) {
+ Logging.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": could not create input stream");
+ callback.onFileTransferAborted();
+ return;
+ }
+ long size = file.getExpectedSize();
+ long transmitted = 0;
+ int count;
+ byte[] buffer = new byte[8192];
+ while ((count = fileInputStream.read(buffer)) > 0) {
+ outputStream.write(buffer, 0, count);
+ digest.update(buffer, 0, count);
+ transmitted += count;
+ connection.updateProgress((int) ((((double) transmitted) / size) * 100));
+ }
+ outputStream.flush();
+ file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest()));
+ if (callback != null) {
+ callback.onFileTransmitted(file);
+ }
+ } catch (FileNotFoundException e) {
+ Logging.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": "+e.getMessage());
+ callback.onFileTransferAborted();
+ } catch (IOException e) {
+ Logging.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": "+e.getMessage());
+ callback.onFileTransferAborted();
+ } catch (NoSuchAlgorithmException e) {
+ Logging.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": "+e.getMessage());
+ callback.onFileTransferAborted();
+ } finally {
+ StreamUtil.close(fileInputStream);
+ wakeLock.release();
+ }
+ }
+ }).start();
+
+ }
+
+ public void receive(final DownloadableFile file, final OnFileTransmissionStatusChanged callback) {
+ new Thread(new Runnable() {
+
+ @Override
+ public void run() {
+ OutputStream fileOutputStream = null;
+ final PowerManager.WakeLock wakeLock = ConversationsPlusApplication.createPartialWakeLock("jingle_receive_"+connection.getSessionId());
+ try {
+ wakeLock.acquire();
+ MessageDigest digest = MessageDigest.getInstance("SHA-1");
+ digest.reset();
+ //inputStream.skip(45);
+ socket.setSoTimeout(30000);
+ file.getParentFile().mkdirs();
+ file.createNewFile();
+ fileOutputStream = connection.getFileOutputStream();
+ if (fileOutputStream == null) {
+ callback.onFileTransferAborted();
+ Logging.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": could not create output stream");
+ return;
+ }
+ double size = file.getExpectedSize();
+ long remainingSize = file.getExpectedSize();
+ byte[] buffer = new byte[8192];
+ int count;
+ while (remainingSize > 0) {
+ count = inputStream.read(buffer);
+ if (count == -1) {
+ callback.onFileTransferAborted();
+ Logging.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": file ended prematurely with "+remainingSize+" bytes remaining");
+ return;
+ } else {
+ fileOutputStream.write(buffer, 0, count);
+ digest.update(buffer, 0, count);
+ remainingSize -= count;
+ }
+ connection.updateProgress((int) (((size - remainingSize) / size) * 100));
+ }
+ fileOutputStream.flush();
+ fileOutputStream.close();
+ file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest()));
+ callback.onFileTransmitted(file);
+ } catch (FileNotFoundException e) {
+ Logging.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": "+e.getMessage());
+ callback.onFileTransferAborted();
+ } catch (IOException e) {
+ Logging.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": "+e.getMessage());
+ callback.onFileTransferAborted();
+ } catch (NoSuchAlgorithmException e) {
+ Logging.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": "+e.getMessage());
+ callback.onFileTransferAborted();
+ } finally {
+ wakeLock.release();
+ StreamUtil.close(fileOutputStream);
+ StreamUtil.close(inputStream);
+ }
+ }
+ }).start();
+ }
+
+ public boolean isProxy() {
+ return this.candidate.getType() == JingleCandidate.TYPE_PROXY;
+ }
+
+ public boolean needsActivation() {
+ return (this.isProxy() && !this.activated);
+ }
+
+ public void disconnect() {
+ StreamUtil.close(inputStream);
+ StreamUtil.close(outputStream);
+ StreamUtil.close(socket);
+ }
+
+ public boolean isEstablished() {
+ return this.isEstablished;
+ }
+
+ public JingleCandidate getCandidate() {
+ return this.candidate;
+ }
+
+ public void setActivated(boolean activated) {
+ this.activated = activated;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java
new file mode 100644
index 00000000..e832d3f5
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java
@@ -0,0 +1,15 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import eu.siacs.conversations.entities.DownloadableFile;
+
+public abstract class JingleTransport {
+ public abstract void connect(final OnTransportConnected callback);
+
+ public abstract void receive(final DownloadableFile file,
+ final OnFileTransmissionStatusChanged callback);
+
+ public abstract void send(final DownloadableFile file,
+ final OnFileTransmissionStatusChanged callback);
+
+ public abstract void disconnect();
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/OnFileTransmissionStatusChanged.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnFileTransmissionStatusChanged.java
new file mode 100644
index 00000000..91cba39f
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnFileTransmissionStatusChanged.java
@@ -0,0 +1,9 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import eu.siacs.conversations.entities.DownloadableFile;
+
+public interface OnFileTransmissionStatusChanged {
+ void onFileTransmitted(DownloadableFile file);
+
+ void onFileTransferAborted();
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java
new file mode 100644
index 00000000..9a60b392
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java
@@ -0,0 +1,9 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.xmpp.PacketReceived;
+import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
+
+public interface OnJinglePacketReceived extends PacketReceived {
+ void onJinglePacketReceived(Account account, JinglePacket packet);
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java
new file mode 100644
index 00000000..76e33717
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java
@@ -0,0 +1,5 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+public interface OnPrimaryCandidateFound {
+ void onPrimaryCandidateFound(boolean success, JingleCandidate canditate);
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/OnTransportConnected.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnTransportConnected.java
new file mode 100644
index 00000000..38f03c5d
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnTransportConnected.java
@@ -0,0 +1,7 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+public interface OnTransportConnected {
+ public void failed();
+
+ public void established();
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java
new file mode 100644
index 00000000..f752cc5d
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java
@@ -0,0 +1,103 @@
+package eu.siacs.conversations.xmpp.jingle.stanzas;
+
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.xml.Element;
+
+public class Content extends Element {
+
+ private String transportId;
+
+ private Content(String name) {
+ super(name);
+ }
+
+ public Content() {
+ super("content");
+ }
+
+ public Content(String creator, String name) {
+ super("content");
+ this.setAttribute("creator", creator);
+ this.setAttribute("name", name);
+ }
+
+ public void setTransportId(String sid) {
+ this.transportId = sid;
+ }
+
+ public Element setFileOffer(DownloadableFile actualFile, boolean otr) {
+ Element description = this.addChild("description",
+ "urn:xmpp:jingle:apps:file-transfer:3");
+ Element offer = description.addChild("offer");
+ Element file = offer.addChild("file");
+ file.addChild("size").setContent(Long.toString(actualFile.getExpectedSize()));
+ if (otr) {
+ file.addChild("name").setContent(actualFile.getName() + ".otr");
+ } else {
+ file.addChild("name").setContent(actualFile.getName());
+ }
+ return file;
+ }
+
+ public Element getFileOffer() {
+ Element description = this.findChild("description",
+ "urn:xmpp:jingle:apps:file-transfer:3");
+ if (description == null) {
+ return null;
+ }
+ Element offer = description.findChild("offer");
+ if (offer == null) {
+ return null;
+ }
+ return offer.findChild("file");
+ }
+
+ public void setFileOffer(Element fileOffer) {
+ Element description = this.findChild("description",
+ "urn:xmpp:jingle:apps:file-transfer:3");
+ if (description == null) {
+ description = this.addChild("description",
+ "urn:xmpp:jingle:apps:file-transfer:3");
+ }
+ description.addChild(fileOffer);
+ }
+
+ public String getTransportId() {
+ if (hasSocks5Transport()) {
+ this.transportId = socks5transport().getAttribute("sid");
+ } else if (hasIbbTransport()) {
+ this.transportId = ibbTransport().getAttribute("sid");
+ }
+ return this.transportId;
+ }
+
+ public Element socks5transport() {
+ Element transport = this.findChild("transport",
+ "urn:xmpp:jingle:transports:s5b:1");
+ if (transport == null) {
+ transport = this.addChild("transport",
+ "urn:xmpp:jingle:transports:s5b:1");
+ transport.setAttribute("sid", this.transportId);
+ }
+ return transport;
+ }
+
+ public Element ibbTransport() {
+ Element transport = this.findChild("transport",
+ "urn:xmpp:jingle:transports:ibb:1");
+ if (transport == null) {
+ transport = this.addChild("transport",
+ "urn:xmpp:jingle:transports:ibb:1");
+ transport.setAttribute("sid", this.transportId);
+ }
+ return transport;
+ }
+
+ public boolean hasSocks5Transport() {
+ return this.hasChild("transport", "urn:xmpp:jingle:transports:s5b:1");
+ }
+
+ public boolean hasIbbTransport() {
+ return this.hasChild("transport", "urn:xmpp:jingle:transports:ibb:1");
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java
new file mode 100644
index 00000000..4f73a83a
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java
@@ -0,0 +1,96 @@
+package eu.siacs.conversations.xmpp.jingle.stanzas;
+
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.jid.Jid;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+public class JinglePacket extends IqPacket {
+ Content content = null;
+ Reason reason = null;
+ Element jingle = new Element("jingle");
+
+ @Override
+ public Element addChild(Element child) {
+ if ("jingle".equals(child.getName())) {
+ Element contentElement = child.findChild("content");
+ if (contentElement != null) {
+ this.content = new Content();
+ this.content.setChildren(contentElement.getChildren());
+ this.content.setAttributes(contentElement.getAttributes());
+ }
+ Element reasonElement = child.findChild("reason");
+ if (reasonElement != null) {
+ this.reason = new Reason();
+ this.reason.setChildren(reasonElement.getChildren());
+ this.reason.setAttributes(reasonElement.getAttributes());
+ }
+ this.jingle.setAttributes(child.getAttributes());
+ }
+ return child;
+ }
+
+ public JinglePacket setContent(Content content) {
+ this.content = content;
+ return this;
+ }
+
+ public Content getJingleContent() {
+ if (this.content == null) {
+ this.content = new Content();
+ }
+ return this.content;
+ }
+
+ public JinglePacket setReason(Reason reason) {
+ this.reason = reason;
+ return this;
+ }
+
+ public Reason getReason() {
+ return this.reason;
+ }
+
+ private void build() {
+ this.children.clear();
+ this.jingle.clearChildren();
+ this.jingle.setAttribute("xmlns", "urn:xmpp:jingle:1");
+ if (this.content != null) {
+ jingle.addChild(this.content);
+ }
+ if (this.reason != null) {
+ jingle.addChild(this.reason);
+ }
+ this.children.add(jingle);
+ this.setAttribute("type", "set");
+ }
+
+ public String getSessionId() {
+ return this.jingle.getAttribute("sid");
+ }
+
+ public void setSessionId(String sid) {
+ this.jingle.setAttribute("sid", sid);
+ }
+
+ @Override
+ public String toString() {
+ this.build();
+ return super.toString();
+ }
+
+ public void setAction(String action) {
+ this.jingle.setAttribute("action", action);
+ }
+
+ public String getAction() {
+ return this.jingle.getAttribute("action");
+ }
+
+ public void setInitiator(final Jid initiator) {
+ this.jingle.setAttribute("initiator", initiator.toString());
+ }
+
+ public boolean isAction(String action) {
+ return action.equalsIgnoreCase(this.getAction());
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java
new file mode 100644
index 00000000..610d5e76
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java
@@ -0,0 +1,13 @@
+package eu.siacs.conversations.xmpp.jingle.stanzas;
+
+import eu.siacs.conversations.xml.Element;
+
+public class Reason extends Element {
+ private Reason(String name) {
+ super(name);
+ }
+
+ public Reason() {
+ super("reason");
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java b/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java
new file mode 100644
index 00000000..38bb5c8f
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java
@@ -0,0 +1,102 @@
+package eu.siacs.conversations.xmpp.pep;
+
+import android.util.Base64;
+
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+public class Avatar {
+
+ public enum Origin { PEP, VCARD };
+
+ public String type;
+ public String sha1sum;
+ public String image;
+ public int height;
+ public int width;
+ public long size;
+ public Jid owner;
+ public Origin origin = Origin.PEP; //default to maintain compat
+
+ public byte[] getImageAsBytes() {
+ return Base64.decode(image, Base64.DEFAULT);
+ }
+
+ public String getFilename() {
+ return sha1sum;
+ }
+
+ public static Avatar parseMetadata(Element items) {
+ Element item = items.findChild("item");
+ if (item == null) {
+ return null;
+ }
+ Element metadata = item.findChild("metadata");
+ if (metadata == null) {
+ return null;
+ }
+ String primaryId = item.getAttribute("id");
+ if (primaryId == null) {
+ return null;
+ }
+ for (Element child : metadata.getChildren()) {
+ if (child.getName().equals("info")
+ && primaryId.equals(child.getAttribute("id"))) {
+ Avatar avatar = new Avatar();
+ String height = child.getAttribute("height");
+ String width = child.getAttribute("width");
+ String size = child.getAttribute("bytes");
+ try {
+ if (height != null) {
+ avatar.height = Integer.parseInt(height);
+ }
+ if (width != null) {
+ avatar.width = Integer.parseInt(width);
+ }
+ if (size != null) {
+ avatar.size = Long.parseLong(size);
+ }
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ avatar.type = child.getAttribute("type");
+ String hash = child.getAttribute("id");
+ if (!isValidSHA1(hash)) {
+ return null;
+ }
+ avatar.sha1sum = hash;
+ avatar.origin = Origin.PEP;
+ return avatar;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (object != null && object instanceof Avatar) {
+ Avatar other = (Avatar) object;
+ return other.getFilename().equals(this.getFilename());
+ } else {
+ return false;
+ }
+ }
+
+ public static Avatar parsePresence(Element x) {
+ String hash = x == null ? null : x.findChildContent("photo");
+ if (hash == null) {
+ return null;
+ }
+ if (!isValidSHA1(hash)) {
+ return null;
+ }
+ Avatar avatar = new Avatar();
+ avatar.sha1sum = hash;
+ avatar.origin = Origin.VCARD;
+ return avatar;
+ }
+
+ private static boolean isValidSHA1(String s) {
+ return s != null && s.matches("[a-fA-F0-9]{40}");
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractAcknowledgeableStanza.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractAcknowledgeableStanza.java
new file mode 100644
index 00000000..fa5e6fbd
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractAcknowledgeableStanza.java
@@ -0,0 +1,31 @@
+package eu.siacs.conversations.xmpp.stanzas;
+
+import eu.siacs.conversations.xml.Element;
+
+abstract public class AbstractAcknowledgeableStanza extends AbstractStanza {
+
+ protected AbstractAcknowledgeableStanza(String name) {
+ super(name);
+ }
+
+
+ public String getId() {
+ return this.getAttribute("id");
+ }
+
+ public void setId(final String id) {
+ setAttribute("id", id);
+ }
+
+ public Element getError() {
+ Element error = findChild("error");
+ if (error != null) {
+ for(Element element : error.getChildren()) {
+ if (!element.getName().equals("text")) {
+ return element;
+ }
+ }
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java
new file mode 100644
index 00000000..a6144df2
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java
@@ -0,0 +1,50 @@
+package eu.siacs.conversations.xmpp.stanzas;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+public class AbstractStanza extends Element {
+
+ protected AbstractStanza(final String name) {
+ super(name);
+ }
+
+ public Jid getTo() {
+ return getAttributeAsJid("to");
+ }
+
+ public Jid getFrom() {
+ return getAttributeAsJid("from");
+ }
+
+ public void setTo(final Jid to) {
+ if (to != null) {
+ setAttribute("to", to.toString());
+ }
+ }
+
+ public void setFrom(final Jid from) {
+ if (from != null) {
+ setAttribute("from", from.toString());
+ }
+ }
+
+ public boolean fromServer(final Account account) {
+ return getFrom() == null
+ || getFrom().equals(account.getServer())
+ || getFrom().equals(account.getJid().toBareJid())
+ || getFrom().equals(account.getJid());
+ }
+
+ public boolean toServer(final Account account) {
+ return getTo() == null
+ || getTo().equals(account.getServer())
+ || getTo().equals(account.getJid().toBareJid())
+ || getTo().equals(account.getJid());
+ }
+
+ public boolean fromAccount(final Account account) {
+ return getFrom() != null && getFrom().toBareJid().equals(account.getJid().toBareJid());
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java
new file mode 100644
index 00000000..302dc78e
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java
@@ -0,0 +1,69 @@
+package eu.siacs.conversations.xmpp.stanzas;
+
+import eu.siacs.conversations.xml.Element;
+
+public class IqPacket extends AbstractAcknowledgeableStanza {
+
+ public enum TYPE {
+ ERROR,
+ SET,
+ RESULT,
+ GET,
+ INVALID,
+ TIMEOUT
+ }
+
+ public IqPacket(final TYPE type) {
+ super("iq");
+ if (type != TYPE.INVALID) {
+ this.setAttribute("type", type.toString().toLowerCase());
+ }
+ }
+
+ public IqPacket() {
+ super("iq");
+ }
+
+ public Element query() {
+ Element query = findChild("query");
+ if (query == null) {
+ query = addChild("query");
+ }
+ return query;
+ }
+
+ public Element query(final String xmlns) {
+ final Element query = query();
+ query.setAttribute("xmlns", xmlns);
+ return query();
+ }
+
+ public TYPE getType() {
+ final String type = getAttribute("type");
+ if (type == null) {
+ return TYPE.INVALID;
+ }
+ switch (type) {
+ case "error":
+ return TYPE.ERROR;
+ case "result":
+ return TYPE.RESULT;
+ case "set":
+ return TYPE.SET;
+ case "get":
+ return TYPE.GET;
+ case "timeout":
+ return TYPE.TIMEOUT;
+ default:
+ return TYPE.INVALID;
+ }
+ }
+
+ public IqPacket generateResponse(final TYPE type) {
+ final IqPacket packet = new IqPacket(type);
+ packet.setTo(this.getFrom());
+ packet.setId(this.getId());
+ return packet;
+ }
+
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java
new file mode 100644
index 00000000..941b4b5f
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java
@@ -0,0 +1,99 @@
+package eu.siacs.conversations.xmpp.stanzas;
+
+import android.util.Pair;
+
+import eu.siacs.conversations.parser.AbstractParser;
+import eu.siacs.conversations.xml.Element;
+
+public class MessagePacket extends AbstractAcknowledgeableStanza {
+ public static final int TYPE_CHAT = 0;
+ public static final int TYPE_NORMAL = 2;
+ public static final int TYPE_GROUPCHAT = 3;
+ public static final int TYPE_ERROR = 4;
+ public static final int TYPE_HEADLINE = 5;
+
+ public MessagePacket() {
+ super("message");
+ }
+
+ public String getBody() {
+ return findChildContent("body");
+ }
+
+ public void setBody(String text) {
+ this.children.remove(findChild("body"));
+ Element body = new Element("body");
+ body.setContent(text);
+ this.children.add(0, body);
+ }
+
+ public void setAxolotlMessage(Element axolotlMessage) {
+ this.children.remove(findChild("body"));
+ this.children.add(0, axolotlMessage);
+ }
+
+ public void setType(int type) {
+ switch (type) {
+ case TYPE_CHAT:
+ this.setAttribute("type", "chat");
+ break;
+ case TYPE_GROUPCHAT:
+ this.setAttribute("type", "groupchat");
+ break;
+ case TYPE_NORMAL:
+ break;
+ case TYPE_ERROR:
+ this.setAttribute("type","error");
+ break;
+ default:
+ this.setAttribute("type", "chat");
+ break;
+ }
+ }
+
+ public int getType() {
+ String type = getAttribute("type");
+ if (type == null) {
+ return TYPE_NORMAL;
+ } else if (type.equals("normal")) {
+ return TYPE_NORMAL;
+ } else if (type.equals("chat")) {
+ return TYPE_CHAT;
+ } else if (type.equals("groupchat")) {
+ return TYPE_GROUPCHAT;
+ } else if (type.equals("error")) {
+ return TYPE_ERROR;
+ } else if (type.equals("headline")) {
+ return TYPE_HEADLINE;
+ } else {
+ return TYPE_NORMAL;
+ }
+ }
+
+ public Pair<MessagePacket,Long> getForwardedMessagePacket(String name, String namespace) {
+ Element wrapper = findChild(name, namespace);
+ if (wrapper == null) {
+ return null;
+ }
+ Element forwarded = wrapper.findChild("forwarded", "urn:xmpp:forward:0");
+ if (forwarded == null) {
+ return null;
+ }
+ MessagePacket packet = create(forwarded.findChild("message"));
+ if (packet == null) {
+ return null;
+ }
+ Long timestamp = AbstractParser.getTimestamp(forwarded,null);
+ return new Pair(packet,timestamp);
+ }
+
+ public static MessagePacket create(Element element) {
+ if (element == null) {
+ return null;
+ }
+ MessagePacket packet = new MessagePacket();
+ packet.setAttributes(element.getAttributes());
+ packet.setChildren(element.getChildren());
+ return packet;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/PresencePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/PresencePacket.java
new file mode 100644
index 00000000..c321498d
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/PresencePacket.java
@@ -0,0 +1,8 @@
+package eu.siacs.conversations.xmpp.stanzas;
+
+public class PresencePacket extends AbstractAcknowledgeableStanza {
+
+ public PresencePacket() {
+ super("presence");
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/ActivePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/ActivePacket.java
new file mode 100644
index 00000000..78ab66d8
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/ActivePacket.java
@@ -0,0 +1,10 @@
+package eu.siacs.conversations.xmpp.stanzas.csi;
+
+import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
+
+public class ActivePacket extends AbstractStanza {
+ public ActivePacket() {
+ super("active");
+ setAttribute("xmlns", "urn:xmpp:csi:0");
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/InactivePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/InactivePacket.java
new file mode 100644
index 00000000..f109280f
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/InactivePacket.java
@@ -0,0 +1,10 @@
+package eu.siacs.conversations.xmpp.stanzas.csi;
+
+import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
+
+public class InactivePacket extends AbstractStanza {
+ public InactivePacket() {
+ super("inactive");
+ setAttribute("xmlns", "urn:xmpp:csi:0");
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/AckPacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/AckPacket.java
new file mode 100644
index 00000000..f93b5d87
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/AckPacket.java
@@ -0,0 +1,13 @@
+package eu.siacs.conversations.xmpp.stanzas.streammgmt;
+
+import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
+
+public class AckPacket extends AbstractStanza {
+
+ public AckPacket(int sequence, int smVersion) {
+ super("a");
+ this.setAttribute("xmlns", "urn:xmpp:sm:" + smVersion);
+ this.setAttribute("h", Integer.toString(sequence));
+ }
+
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/EnablePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/EnablePacket.java
new file mode 100644
index 00000000..78cd81ed
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/EnablePacket.java
@@ -0,0 +1,13 @@
+package eu.siacs.conversations.xmpp.stanzas.streammgmt;
+
+import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
+
+public class EnablePacket extends AbstractStanza {
+
+ public EnablePacket(int smVersion) {
+ super("enable");
+ this.setAttribute("xmlns", "urn:xmpp:sm:" + smVersion);
+ this.setAttribute("resume", "true");
+ }
+
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/RequestPacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/RequestPacket.java
new file mode 100644
index 00000000..98cfc748
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/RequestPacket.java
@@ -0,0 +1,12 @@
+package eu.siacs.conversations.xmpp.stanzas.streammgmt;
+
+import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
+
+public class RequestPacket extends AbstractStanza {
+
+ public RequestPacket(int smVersion) {
+ super("r");
+ this.setAttribute("xmlns", "urn:xmpp:sm:" + smVersion);
+ }
+
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/ResumePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/ResumePacket.java
new file mode 100644
index 00000000..9cdcfa5e
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/ResumePacket.java
@@ -0,0 +1,14 @@
+package eu.siacs.conversations.xmpp.stanzas.streammgmt;
+
+import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
+
+public class ResumePacket extends AbstractStanza {
+
+ public ResumePacket(String id, int sequence, int smVersion) {
+ super("resume");
+ this.setAttribute("xmlns", "urn:xmpp:sm:" + smVersion);
+ this.setAttribute("previd", id);
+ this.setAttribute("h", Integer.toString(sequence));
+ }
+
+}