aboutsummaryrefslogtreecommitdiffstats
path: root/conversations/src/main/java/eu/siacs
diff options
context:
space:
mode:
Diffstat (limited to 'conversations/src/main/java/eu/siacs')
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/Config.java25
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/crypto/OtrEngine.java231
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java385
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/entities/AbstractEntity.java21
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/entities/Account.java399
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/entities/Bookmark.java137
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/entities/Contact.java367
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/entities/Conversation.java500
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/entities/Downloadable.java21
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java154
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/entities/ListItem.java7
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/entities/Message.java478
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/entities/MucOptions.java369
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/entities/Presences.java76
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/entities/Roster.java83
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java48
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/generator/IqGenerator.java96
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java178
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java57
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/http/HttpConnection.java255
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java28
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/parser/AbstractParser.java92
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/parser/IqParser.java92
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/parser/MessageParser.java517
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/parser/PresenceParser.java133
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java335
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/persistance/FileBackend.java480
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/persistance/OnPhoneContactsMerged.java5
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java23
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/services/AvatarService.java298
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/services/EventReceiver.java24
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/services/NotificationService.java237
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java1927
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java145
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java280
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java436
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java947
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java781
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java423
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/ui/EditMessage.java39
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java217
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java242
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java74
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/ui/SettingsFragment.java15
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java185
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java677
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/ui/UiCallback.java11
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/ui/XmppActivity.java637
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java102
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java135
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java74
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java44
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java560
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java112
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/utils/DNSHelper.java185
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java44
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java117
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java9
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/utils/PRNGFixes.java327
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java95
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/utils/UIHelper.java225
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/utils/Validator.java14
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/utils/XmlHelper.java12
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/utils/zlib/ZLibInputStream.java54
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/utils/zlib/ZLibOutputStream.java95
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/xml/Element.java148
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/xml/Tag.java104
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/xml/TagWriter.java114
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/xml/XmlReader.java141
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/xmpp/OnBindListener.java7
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/xmpp/OnContactStatusChanged.java7
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/xmpp/OnIqPacketReceived.java8
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/xmpp/OnMessageAcknowledged.java7
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/xmpp/OnMessagePacketReceived.java8
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/xmpp/OnPresencePacketReceived.java8
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/xmpp/OnStatusChanged.java7
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/xmpp/PacketReceived.java5
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java1130
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java143
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java910
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java163
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java191
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java212
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java13
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnFileTransmissionStatusChanged.java9
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java9
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java6
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnTransportConnected.java7
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java102
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java95
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java13
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java71
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java34
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java76
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java66
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/PresencePacket.java8
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/ActivePacket.java10
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/InactivePacket.java10
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/AckPacket.java13
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/EnablePacket.java13
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/RequestPacket.java12
-rw-r--r--conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/ResumePacket.java14
102 files changed, 19285 insertions, 0 deletions
diff --git a/conversations/src/main/java/eu/siacs/conversations/Config.java b/conversations/src/main/java/eu/siacs/conversations/Config.java
new file mode 100644
index 00000000..1725eca6
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/Config.java
@@ -0,0 +1,25 @@
+package eu.siacs.conversations;
+
+import android.graphics.Bitmap;
+
+public final class Config {
+
+ public static final String LOGTAG = "conversations";
+
+ public static final int PING_MAX_INTERVAL = 300;
+ public static final int PING_MIN_INTERVAL = 30;
+ public static final int PING_TIMEOUT = 10;
+ public static final int CONNECT_TIMEOUT = 90;
+ public static final int CARBON_GRACE_PERIOD = 60;
+
+ public static final int AVATAR_SIZE = 192;
+ public static final Bitmap.CompressFormat AVATAR_FORMAT = Bitmap.CompressFormat.WEBP;
+
+ public static final int MESSAGE_MERGE_WINDOW = 20;
+
+ public static final boolean PARSE_EMOTICONS = false;
+
+ private Config() {
+
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/crypto/OtrEngine.java b/conversations/src/main/java/eu/siacs/conversations/crypto/OtrEngine.java
new file mode 100644
index 00000000..e0bd0e79
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/crypto/OtrEngine.java
@@ -0,0 +1,231 @@
+package eu.siacs.conversations.crypto;
+
+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 org.json.JSONException;
+import org.json.JSONObject;
+
+import android.util.Log;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
+
+import net.java.otr4j.OtrEngineHost;
+import net.java.otr4j.OtrException;
+import net.java.otr4j.OtrPolicy;
+import net.java.otr4j.OtrPolicyImpl;
+import net.java.otr4j.session.InstanceTag;
+import net.java.otr4j.session.SessionID;
+
+public class OtrEngine implements OtrEngineHost {
+
+ private Account account;
+ private OtrPolicy otrPolicy;
+ private KeyPair keyPair;
+ private XmppConnectionService mXmppConnectionService;
+
+ public OtrEngine(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 (NoSuchAlgorithmException e) {
+ e.printStackTrace();
+ } catch (InvalidKeySpecException e) {
+ e.printStackTrace();
+ }
+
+ }
+
+ @Override
+ public void askForSecret(SessionID arg0, InstanceTag arg1, String arg2) {
+ // TODO Auto-generated method stub
+
+ }
+
+ @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) {
+ // TODO Auto-generated method stub
+ 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) {
+ Log.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.getFullJid());
+ if (session.getUserID().isEmpty()) {
+ packet.setTo(session.getAccountID());
+ } else {
+ packet.setTo(session.getAccountID() + "/" + session.getUserID());
+ }
+ packet.setBody(body);
+ packet.addChild("private", "urn:xmpp:carbons:2");
+ packet.addChild("no-copy", "urn:xmpp:hints");
+ packet.setType(MessagePacket.TYPE_CHAT);
+ account.getXmppConnection().sendMessagePacket(packet);
+ }
+
+ @Override
+ public void messageFromAnotherInstanceReceived(SessionID id) {
+ Log.d(Config.LOGTAG,
+ "unreadable message received from " + id.getAccountID());
+ }
+
+ @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 {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void smpAborted(SessionID arg0) throws OtrException {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void smpError(SessionID arg0, int arg1, boolean arg2)
+ throws OtrException {
+ throw new OtrException(new Exception("smp error"));
+ }
+
+ @Override
+ public void unencryptedMessageReceived(SessionID arg0, String arg1)
+ throws OtrException {
+ throw new OtrException(new Exception("unencrypted message received"));
+ }
+
+ @Override
+ public void unreadableMessageReceived(SessionID arg0) throws OtrException {
+ throw new OtrException(new Exception("unreadable message received"));
+ }
+
+ @Override
+ public void unverify(SessionID arg0, String arg1) {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void verify(SessionID arg0, String arg1, boolean arg2) {
+ // TODO Auto-generated method stub
+
+ }
+
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java b/conversations/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java
new file mode 100644
index 00000000..2696c7d2
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java
@@ -0,0 +1,385 @@
+package eu.siacs.conversations.crypto;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import org.openintents.openpgp.OpenPgpError;
+import org.openintents.openpgp.OpenPgpSignatureResult;
+import org.openintents.openpgp.util.OpenPgpApi;
+import org.openintents.openpgp.util.OpenPgpApi.IOpenPgpCallback;
+
+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.Conversation;
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.ui.UiCallback;
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.graphics.BitmapFactory;
+import android.util.Log;
+
+public class PgpEngine {
+ private OpenPgpApi api;
+ private XmppConnectionService mXmppConnectionService;
+
+ public PgpEngine(OpenPgpApi api, XmppConnectionService service) {
+ this.api = api;
+ this.mXmppConnectionService = service;
+ }
+
+ public void decrypt(final Message message,
+ final UiCallback<Message> callback) {
+ Log.d(Config.LOGTAG, "decrypting message " + message.getUuid());
+ Intent params = new Intent();
+ params.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY);
+ params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, message
+ .getConversation().getAccount().getJid());
+ 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) {
+ 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.setBody(os.toString());
+ message.setEncryption(Message.ENCRYPTION_DECRYPTED);
+ 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:
+ OpenPgpError error = result
+ .getParcelableExtra(OpenPgpApi.RESULT_ERROR);
+ Log.d(Config.LOGTAG,
+ "openpgp error: " + error.getMessage());
+ callback.error(R.string.openpgp_error, message);
+ return;
+ default:
+ return;
+ }
+ }
+ });
+ } else if (message.getType() == Message.TYPE_IMAGE) {
+ try {
+ final DownloadableFile inputFile = this.mXmppConnectionService
+ .getFileBackend().getFile(message, false);
+ final DownloadableFile outputFile = this.mXmppConnectionService
+ .getFileBackend().getFile(message, true);
+ 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) {
+ switch (result.getIntExtra(OpenPgpApi.RESULT_CODE,
+ OpenPgpApi.RESULT_CODE_ERROR)) {
+ case OpenPgpApi.RESULT_CODE_SUCCESS:
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(
+ outputFile.getAbsolutePath(), options);
+ int imageHeight = options.outHeight;
+ int imageWidth = options.outWidth;
+ message.setBody(Long.toString(outputFile.getSize())
+ + ',' + imageWidth + ',' + imageHeight);
+ message.setEncryption(Message.ENCRYPTION_DECRYPTED);
+ PgpEngine.this.mXmppConnectionService
+ .updateMessage(message);
+ ;
+ 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);
+ return;
+ default:
+ return;
+ }
+ }
+ });
+ } catch (FileNotFoundException e) {
+ callback.error(R.string.error_decrypting_file, message);
+ } catch (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);
+ if (message.getConversation().getMode() == Conversation.MODE_SINGLE) {
+ long[] keys = { message.getConversation().getContact()
+ .getPgpKeyId() };
+ params.putExtra(OpenPgpApi.EXTRA_KEY_IDS, keys);
+ } else {
+ params.putExtra(OpenPgpApi.EXTRA_KEY_IDS, message.getConversation()
+ .getMucOptions().getPgpKeyIds());
+ }
+ params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, message
+ .getConversation().getAccount().getJid());
+
+ if (message.getType() == Message.TYPE_TEXT) {
+ params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true);
+
+ 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) {
+ 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].trim());
+ }
+ }
+ 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 if (message.getType() == Message.TYPE_IMAGE) {
+ try {
+ DownloadableFile inputFile = this.mXmppConnectionService
+ .getFileBackend().getFile(message, true);
+ DownloadableFile outputFile = this.mXmppConnectionService
+ .getFileBackend().getFile(message, false);
+ 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) {
+ switch (result.getIntExtra(OpenPgpApi.RESULT_CODE,
+ OpenPgpApi.RESULT_CODE_ERROR)) {
+ case OpenPgpApi.RESULT_CODE_SUCCESS:
+ 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 (FileNotFoundException e) {
+ Log.d(Config.LOGTAG, "file not found: " + e.getMessage());
+ } catch (IOException e) {
+ Log.d(Config.LOGTAG, "io exception during file encrypt");
+ }
+ }
+ }
+
+ public long fetchKeyId(Account account, String status, String signature) {
+ if ((signature == null) || (api == null)) {
+ return 0;
+ }
+ if (status == null) {
+ status = "";
+ }
+ 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", "").trim());
+ 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);
+ params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, account.getJid());
+ InputStream is = new ByteArrayInputStream(pgpSig.toString().getBytes());
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ Intent result = api.executeApi(params, is, os);
+ 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:
+ Log.d(Config.LOGTAG,
+ "openpgp error: "
+ + ((OpenPgpError) result
+ .getParcelableExtra(OpenPgpApi.RESULT_ERROR))
+ .getMessage());
+ return 0;
+ }
+ return 0;
+ }
+
+ public void generateSignature(final Account account, String status,
+ final UiCallback<Account> callback) {
+ Intent params = new Intent();
+ params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true);
+ params.setAction(OpenPgpApi.ACTION_SIGN);
+ params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, account.getJid());
+ InputStream is = new ByteArrayInputStream(status.getBytes());
+ final OutputStream os = new ByteArrayOutputStream();
+ api.executeApiAsync(params, is, os, new IOpenPgpCallback() {
+
+ @Override
+ public void onReturn(Intent 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.trim());
+ }
+ }
+ }
+ if (line.contains("BEGIN PGP SIGNATURE")) {
+ sig = true;
+ }
+ }
+ } catch (IOException e) {
+ callback.error(R.string.openpgp_error, account);
+ return;
+ }
+ account.setKey("pgp_signature", 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);
+ return;
+ }
+ }
+ });
+ }
+
+ 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());
+ params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, contact.getAccount()
+ .getJid());
+ 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);
+ return;
+ }
+ }
+ });
+ }
+
+ public PendingIntent getIntentForKey(Contact contact) {
+ Intent params = new Intent();
+ params.setAction(OpenPgpApi.ACTION_GET_KEY);
+ params.putExtra(OpenPgpApi.EXTRA_KEY_ID, contact.getPgpKeyId());
+ params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, contact.getAccount()
+ .getJid());
+ 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);
+ params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, account.getJid());
+ Intent result = api.executeApi(params, null, null);
+ return (PendingIntent) result
+ .getParcelableExtra(OpenPgpApi.RESULT_INTENT);
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/entities/AbstractEntity.java b/conversations/src/main/java/eu/siacs/conversations/entities/AbstractEntity.java
new file mode 100644
index 00000000..92b8a729
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/entities/AbstractEntity.java
@@ -0,0 +1,21 @@
+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/conversations/src/main/java/eu/siacs/conversations/entities/Account.java b/conversations/src/main/java/eu/siacs/conversations/entities/Account.java
new file mode 100644
index 00000000..80a9d62f
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/entities/Account.java
@@ -0,0 +1,399 @@
+package eu.siacs.conversations.entities;
+
+import java.security.interfaces.DSAPublicKey;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import net.java.otr4j.crypto.OtrCryptoEngineImpl;
+import net.java.otr4j.crypto.OtrCryptoException;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.crypto.OtrEngine;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.os.SystemClock;
+
+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 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 static final int STATUS_CONNECTING = 0;
+ public static final int STATUS_DISABLED = -2;
+ public static final int STATUS_OFFLINE = -1;
+ public static final int STATUS_ONLINE = 1;
+ public static final int STATUS_NO_INTERNET = 2;
+ public static final int STATUS_UNAUTHORIZED = 3;
+ public static final int STATUS_SERVER_NOT_FOUND = 5;
+
+ public static final int STATUS_REGISTRATION_FAILED = 7;
+ public static final int STATUS_REGISTRATION_CONFLICT = 8;
+ public static final int STATUS_REGISTRATION_SUCCESSFULL = 9;
+ public static final int STATUS_REGISTRATION_NOT_SUPPORTED = 10;
+
+ protected String username;
+ protected String server;
+ protected String password;
+ protected int options = 0;
+ protected String rosterVersion;
+ protected String resource = "mobile";
+ protected int status = -1;
+ protected JSONObject keys = new JSONObject();
+ protected String avatar;
+
+ protected boolean online = false;
+
+ private OtrEngine otrEngine = null;
+ private XmppConnection xmppConnection = null;
+ private Presences presences = new Presences();
+ private long mEndGracePeriod = 0L;
+ private String otrFingerprint;
+ private Roster roster = null;
+
+ private List<Bookmark> bookmarks = new CopyOnWriteArrayList<Bookmark>();
+ public List<Conversation> pendingConferenceJoins = new CopyOnWriteArrayList<Conversation>();
+ public List<Conversation> pendingConferenceLeaves = new CopyOnWriteArrayList<Conversation>();
+
+ public Account() {
+ this.uuid = "0";
+ }
+
+ public Account(String username, String server, String password) {
+ this(java.util.UUID.randomUUID().toString(), username, server,
+ password, 0, null, "", null);
+ }
+
+ public Account(String uuid, String username, String server,
+ String password, int options, String rosterVersion, String keys,
+ String avatar) {
+ this.uuid = uuid;
+ this.username = username;
+ this.server = server;
+ this.password = password;
+ this.options = options;
+ this.rosterVersion = rosterVersion;
+ try {
+ this.keys = new JSONObject(keys);
+ } catch (JSONException e) {
+
+ }
+ this.avatar = avatar;
+ }
+
+ public boolean isOptionSet(int option) {
+ return ((options & (1 << option)) != 0);
+ }
+
+ public void setOption(int option, boolean value) {
+ if (value) {
+ this.options |= 1 << option;
+ } else {
+ this.options &= ~(1 << option);
+ }
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public String getServer() {
+ return server;
+ }
+
+ public void setServer(String server) {
+ this.server = server;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ public void setStatus(int status) {
+ this.status = status;
+ }
+
+ public int getStatus() {
+ if (isOptionSet(OPTION_DISABLED)) {
+ return STATUS_DISABLED;
+ } else {
+ return this.status;
+ }
+ }
+
+ public boolean errorStatus() {
+ int s = getStatus();
+ return (s == STATUS_REGISTRATION_FAILED
+ || s == STATUS_REGISTRATION_CONFLICT
+ || s == STATUS_REGISTRATION_NOT_SUPPORTED
+ || s == STATUS_SERVER_NOT_FOUND || s == STATUS_UNAUTHORIZED);
+ }
+
+ public boolean hasErrorStatus() {
+ if (getXmppConnection() == null) {
+ return false;
+ } else {
+ return getStatus() > STATUS_NO_INTERNET
+ && (getXmppConnection().getAttempt() >= 2);
+ }
+ }
+
+ public void setResource(String resource) {
+ this.resource = resource;
+ }
+
+ public String getResource() {
+ return this.resource;
+ }
+
+ public String getJid() {
+ return username.toLowerCase(Locale.getDefault()) + "@"
+ + server.toLowerCase(Locale.getDefault());
+ }
+
+ public JSONObject getKeys() {
+ return keys;
+ }
+
+ public String getSSLFingerprint() {
+ if (keys.has("ssl_cert")) {
+ try {
+ return keys.getString("ssl_cert");
+ } catch (JSONException e) {
+ return null;
+ }
+ } else {
+ return null;
+ }
+ }
+
+ public void setSSLCertFingerprint(String fingerprint) {
+ this.setKey("ssl_cert", fingerprint);
+ }
+
+ public boolean setKey(String keyName, String keyValue) {
+ try {
+ this.keys.put(keyName, keyValue);
+ return true;
+ } catch (JSONException e) {
+ return false;
+ }
+ }
+
+ @Override
+ public ContentValues getContentValues() {
+ ContentValues values = new ContentValues();
+ values.put(UUID, uuid);
+ values.put(USERNAME, username);
+ values.put(SERVER, server);
+ values.put(PASSWORD, password);
+ values.put(OPTIONS, options);
+ values.put(KEYS, this.keys.toString());
+ values.put(ROSTERVERSION, rosterVersion);
+ values.put(AVATAR, avatar);
+ return values;
+ }
+
+ public static Account fromCursor(Cursor cursor) {
+ return new Account(cursor.getString(cursor.getColumnIndex(UUID)),
+ cursor.getString(cursor.getColumnIndex(USERNAME)),
+ cursor.getString(cursor.getColumnIndex(SERVER)),
+ cursor.getString(cursor.getColumnIndex(PASSWORD)),
+ cursor.getInt(cursor.getColumnIndex(OPTIONS)),
+ cursor.getString(cursor.getColumnIndex(ROSTERVERSION)),
+ cursor.getString(cursor.getColumnIndex(KEYS)),
+ cursor.getString(cursor.getColumnIndex(AVATAR)));
+ }
+
+ public OtrEngine getOtrEngine(XmppConnectionService context) {
+ if (otrEngine == null) {
+ otrEngine = new OtrEngine(context, this);
+ }
+ return this.otrEngine;
+ }
+
+ public XmppConnection getXmppConnection() {
+ return this.xmppConnection;
+ }
+
+ public void setXmppConnection(XmppConnection connection) {
+ this.xmppConnection = connection;
+ }
+
+ public String getFullJid() {
+ return this.getJid() + "/" + this.resource;
+ }
+
+ public String getOtrFingerprint() {
+ if (this.otrFingerprint == null) {
+ try {
+ DSAPublicKey pubkey = (DSAPublicKey) this.otrEngine
+ .getPublicKey();
+ if (pubkey == null) {
+ return null;
+ }
+ StringBuilder builder = new StringBuilder(
+ new OtrCryptoEngineImpl().getFingerprint(pubkey));
+ builder.insert(8, " ");
+ builder.insert(17, " ");
+ builder.insert(26, " ");
+ builder.insert(35, " ");
+ this.otrFingerprint = builder.toString();
+ } catch (OtrCryptoException e) {
+
+ }
+ }
+ return this.otrFingerprint;
+ }
+
+ public String getRosterVersion() {
+ if (this.rosterVersion == null) {
+ return "";
+ } else {
+ return this.rosterVersion;
+ }
+ }
+
+ public void setRosterVersion(String version) {
+ this.rosterVersion = version;
+ }
+
+ public String getOtrFingerprint(XmppConnectionService service) {
+ this.getOtrEngine(service);
+ return this.getOtrFingerprint();
+ }
+
+ public void updatePresence(String resource, int status) {
+ this.presences.updatePresence(resource, status);
+ }
+
+ public void removePresence(String resource) {
+ this.presences.removePresence(resource);
+ }
+
+ public void clearPresences() {
+ this.presences = new Presences();
+ }
+
+ public int countPresences() {
+ return this.presences.size();
+ }
+
+ public String getPgpSignature() {
+ if (keys.has("pgp_signature")) {
+ try {
+ return keys.getString("pgp_signature");
+ } catch (JSONException e) {
+ return null;
+ }
+ } else {
+ return null;
+ }
+ }
+
+ public Roster getRoster() {
+ if (this.roster == null) {
+ this.roster = new Roster(this);
+ }
+ return this.roster;
+ }
+
+ public void setBookmarks(List<Bookmark> bookmarks) {
+ this.bookmarks = bookmarks;
+ }
+
+ public List<Bookmark> getBookmarks() {
+ return this.bookmarks;
+ }
+
+ public boolean hasBookmarkFor(String conferenceJid) {
+ for (Bookmark bmark : this.bookmarks) {
+ if (bmark.getJid().equals(conferenceJid)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public boolean setAvatar(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 int getReadableStatusId() {
+ switch (getStatus()) {
+
+ case Account.STATUS_DISABLED:
+ return R.string.account_status_disabled;
+ case Account.STATUS_ONLINE:
+ return R.string.account_status_online;
+ case Account.STATUS_CONNECTING:
+ return R.string.account_status_connecting;
+ case Account.STATUS_OFFLINE:
+ return R.string.account_status_offline;
+ case Account.STATUS_UNAUTHORIZED:
+ return R.string.account_status_unauthorized;
+ case Account.STATUS_SERVER_NOT_FOUND:
+ return R.string.account_status_not_found;
+ case Account.STATUS_NO_INTERNET:
+ return R.string.account_status_no_internet;
+ case Account.STATUS_REGISTRATION_FAILED:
+ return R.string.account_status_regis_fail;
+ case Account.STATUS_REGISTRATION_CONFLICT:
+ return R.string.account_status_regis_conflict;
+ case Account.STATUS_REGISTRATION_SUCCESSFULL:
+ return R.string.account_status_regis_success;
+ case Account.STATUS_REGISTRATION_NOT_SUPPORTED:
+ return R.string.account_status_regis_not_sup;
+ default:
+ return R.string.account_status_unknown;
+ }
+ }
+
+ 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;
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/entities/Bookmark.java b/conversations/src/main/java/eu/siacs/conversations/entities/Bookmark.java
new file mode 100644
index 00000000..dd9e805c
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/entities/Bookmark.java
@@ -0,0 +1,137 @@
+package eu.siacs.conversations.entities;
+
+import java.util.Locale;
+
+import eu.siacs.conversations.xml.Element;
+
+public class Bookmark extends Element implements ListItem {
+
+ private Account account;
+ private Conversation mJoinedConversation;
+
+ public Bookmark(Account account, String jid) {
+ super("conference");
+ this.setAttribute("jid", jid);
+ 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");
+ }
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public void setNick(String nick) {
+ Element element = this.findChild("nick");
+ if (element == null) {
+ element = this.addChild("nick");
+ }
+ element.setContent(nick);
+ }
+
+ public void setPassword(String password) {
+ Element element = this.findChild("password");
+ if (element != null) {
+ element.setContent(password);
+ }
+ }
+
+ @Override
+ public int compareTo(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 (getName() != null) {
+ return getName();
+ } else {
+ return this.getJid().split("@")[0];
+ }
+ }
+
+ @Override
+ public String getJid() {
+ String jid = this.getAttribute("jid");
+ if (jid != null) {
+ return jid.toLowerCase(Locale.US);
+ } else {
+ return null;
+ }
+ }
+
+ public String getNick() {
+ Element nick = this.findChild("nick");
+ if (nick != null) {
+ return nick.getContent();
+ } else {
+ return null;
+ }
+ }
+
+ public boolean autojoin() {
+ String autojoin = this.getAttribute("autojoin");
+ return (autojoin != null && (autojoin.equalsIgnoreCase("true") || autojoin
+ .equalsIgnoreCase("1")));
+ }
+
+ public String getPassword() {
+ Element password = this.findChild("password");
+ if (password != null) {
+ return password.getContent();
+ } else {
+ return null;
+ }
+ }
+
+ public boolean match(String needle) {
+ return needle == null
+ || getJid().contains(needle.toLowerCase(Locale.US))
+ || getDisplayName().toLowerCase(Locale.US).contains(
+ needle.toLowerCase(Locale.US));
+ }
+
+ public Account getAccount() {
+ return this.account;
+ }
+
+ public void setConversation(Conversation conversation) {
+ this.mJoinedConversation = conversation;
+ }
+
+ public Conversation getConversation() {
+ return this.mJoinedConversation;
+ }
+
+ public String getName() {
+ return this.getAttribute("name");
+ }
+
+ public void unregisterConversation() {
+ if (this.mJoinedConversation != null) {
+ this.mJoinedConversation.deregisterWithBookmark();
+ }
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/entities/Contact.java b/conversations/src/main/java/eu/siacs/conversations/entities/Contact.java
new file mode 100644
index 00000000..60c31a42
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/entities/Contact.java
@@ -0,0 +1,367 @@
+package eu.siacs.conversations.entities;
+
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import eu.siacs.conversations.xml.Element;
+import android.content.ContentValues;
+import android.database.Cursor;
+
+public class Contact implements ListItem {
+ 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";
+
+ protected String accountUuid;
+ protected String systemName;
+ protected String serverName;
+ protected String presenceName;
+ protected String jid;
+ protected int subscription = 0;
+ protected String systemAccount;
+ protected String photoUri;
+ protected String avatar;
+ protected JSONObject keys = new JSONObject();
+ protected Presences presences = new Presences();
+
+ protected Account account;
+
+ protected boolean inRoster = true;
+
+ public Lastseen lastseen = new Lastseen();
+
+ public Contact(String account, String systemName, String serverName,
+ String jid, int subscription, String photoUri,
+ String systemAccount, String keys, String avatar) {
+ this.accountUuid = account;
+ this.systemName = systemName;
+ this.serverName = serverName;
+ this.jid = jid;
+ this.subscription = subscription;
+ this.photoUri = photoUri;
+ this.systemAccount = systemAccount;
+ if (keys == null) {
+ keys = "";
+ }
+ try {
+ this.keys = new JSONObject(keys);
+ } catch (JSONException e) {
+ this.keys = new JSONObject();
+ }
+ this.avatar = avatar;
+ }
+
+ public Contact(String jid) {
+ this.jid = jid;
+ }
+
+ public String getDisplayName() {
+ if (this.systemName != null) {
+ return this.systemName;
+ } else if (this.serverName != null) {
+ return this.serverName;
+ } else if (this.presenceName != null) {
+ return this.presenceName;
+ } else {
+ return this.jid.split("@")[0];
+ }
+ }
+
+ public String getProfilePhoto() {
+ return this.photoUri;
+ }
+
+ public String getJid() {
+ return this.jid.toLowerCase(Locale.getDefault());
+ }
+
+ public boolean match(String needle) {
+ return needle == null
+ || jid.contains(needle.toLowerCase())
+ || getDisplayName().toLowerCase()
+ .contains(needle.toLowerCase());
+ }
+
+ public ContentValues getContentValues() {
+ ContentValues values = new ContentValues();
+ values.put(ACCOUNT, accountUuid);
+ values.put(SYSTEMNAME, systemName);
+ values.put(SERVERNAME, serverName);
+ values.put(JID, jid);
+ values.put(OPTIONS, subscription);
+ values.put(SYSTEMACCOUNT, systemAccount);
+ values.put(PHOTOURI, photoUri);
+ values.put(KEYS, keys.toString());
+ values.put(AVATAR, avatar);
+ return values;
+ }
+
+ public static Contact fromCursor(Cursor cursor) {
+ return new Contact(cursor.getString(cursor.getColumnIndex(ACCOUNT)),
+ cursor.getString(cursor.getColumnIndex(SYSTEMNAME)),
+ cursor.getString(cursor.getColumnIndex(SERVERNAME)),
+ cursor.getString(cursor.getColumnIndex(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)));
+ }
+
+ public int getSubscription() {
+ return this.subscription;
+ }
+
+ public void setSystemAccount(String account) {
+ this.systemAccount = account;
+ }
+
+ public void setAccount(Account account) {
+ this.account = account;
+ this.accountUuid = account.getUuid();
+ }
+
+ public Account getAccount() {
+ return this.account;
+ }
+
+ public Presences getPresences() {
+ return this.presences;
+ }
+
+ public void updatePresence(String resource, int status) {
+ this.presences.updatePresence(resource, status);
+ }
+
+ public void removePresence(String resource) {
+ this.presences.removePresence(resource);
+ }
+
+ public void clearPresences() {
+ this.presences.clearPresences();
+ this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
+ }
+
+ public int getMostAvailableStatus() {
+ return this.presences.getMostAvailableStatus();
+ }
+
+ public void setPresences(Presences pres) {
+ this.presences = pres;
+ }
+
+ public void setPhotoUri(String uri) {
+ this.photoUri = uri;
+ }
+
+ 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 Set<String> getOtrFingerprints() {
+ Set<String> set = new HashSet<String>();
+ try {
+ if (this.keys.has("otr_fingerprints")) {
+ JSONArray fingerprints = this.keys
+ .getJSONArray("otr_fingerprints");
+ for (int i = 0; i < fingerprints.length(); ++i) {
+ set.add(fingerprints.getString(i));
+ }
+ }
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ return set;
+ }
+
+ public void addOtrFingerprint(String print) {
+ 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);
+ } catch (JSONException e) {
+
+ }
+ }
+
+ public void setPgpKeyId(long keyId) {
+ try {
+ this.keys.put("pgp_keyid", keyId);
+ } catch (JSONException e) {
+
+ }
+ }
+
+ public long getPgpKeyId() {
+ if (this.keys.has("pgp_keyid")) {
+ try {
+ return this.keys.getLong("pgp_keyid");
+ } catch (JSONException e) {
+ return 0;
+ }
+ } else {
+ return 0;
+ }
+ }
+
+ 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) {
+ if (subscription.equals("to")) {
+ this.resetOption(Contact.Options.FROM);
+ this.setOption(Contact.Options.TO);
+ } else if (subscription.equals("from")) {
+ this.resetOption(Contact.Options.TO);
+ this.setOption(Contact.Options.FROM);
+ this.resetOption(Contact.Options.PREEMPTIVE_GRANT);
+ } else if (subscription.equals("both")) {
+ this.setOption(Contact.Options.TO);
+ this.setOption(Contact.Options.FROM);
+ this.resetOption(Contact.Options.PREEMPTIVE_GRANT);
+ } else if (subscription.equals("none")) {
+ this.resetOption(Contact.Options.FROM);
+ this.resetOption(Contact.Options.TO);
+ }
+ }
+
+ // 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 Element asElement() {
+ Element item = new Element("item");
+ item.setAttribute("jid", this.jid);
+ if (this.serverName != null) {
+ item.setAttribute("name", this.serverName);
+ }
+ return item;
+ }
+
+ public 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;
+ }
+
+ public class Lastseen {
+ public long time = 0;
+ public String presence = null;
+ }
+
+ @Override
+ public int compareTo(ListItem another) {
+ return this.getDisplayName().compareToIgnoreCase(
+ another.getDisplayName());
+ }
+
+ public String getServer() {
+ String[] split = getJid().split("@");
+ if (split.length >= 2) {
+ return split[1];
+ } else {
+ return null;
+ }
+ }
+
+ public boolean setAvatar(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 boolean deleteOtrFingerprint(String fingerprint) {
+ 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);
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/entities/Conversation.java b/conversations/src/main/java/eu/siacs/conversations/entities/Conversation.java
new file mode 100644
index 00000000..9d4c36db
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/entities/Conversation.java
@@ -0,0 +1,500 @@
+package eu.siacs.conversations.entities;
+
+import java.security.interfaces.DSAPublicKey;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import eu.siacs.conversations.services.XmppConnectionService;
+
+import net.java.otr4j.OtrException;
+import net.java.otr4j.crypto.OtrCryptoEngineImpl;
+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 android.content.ContentValues;
+import android.database.Cursor;
+import android.os.SystemClock;
+
+public class Conversation extends AbstractEntity {
+ 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";
+
+ private String name;
+ private String contactUuid;
+ private String accountUuid;
+ private String contactJid;
+ private int status;
+ private long created;
+ private int mode;
+
+ private JSONObject attributes = new JSONObject();
+
+ private String nextPresence;
+
+ protected ArrayList<Message> messages = new ArrayList<Message>();
+ protected Account account = null;
+
+ private transient SessionImpl otrSession;
+
+ private transient String otrFingerprint = null;
+
+ private String nextMessage;
+
+ private transient MucOptions mucOptions = null;
+
+ // private transient String latestMarkableMessageId;
+
+ private byte[] symmetricKey;
+
+ private Bookmark bookmark;
+
+ public Conversation(String name, Account account, String contactJid,
+ int mode) {
+ this(java.util.UUID.randomUUID().toString(), name, null, account
+ .getUuid(), contactJid, System.currentTimeMillis(),
+ STATUS_AVAILABLE, mode, "");
+ this.account = account;
+ }
+
+ public Conversation(String uuid, String name, String contactUuid,
+ String accountUuid, String contactJid, long created, int status,
+ int mode, 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 {
+ if (attributes == null) {
+ attributes = new String();
+ }
+ this.attributes = new JSONObject(attributes);
+ } catch (JSONException e) {
+ this.attributes = new JSONObject();
+ }
+ }
+
+ public List<Message> getMessages() {
+ return messages;
+ }
+
+ public boolean isRead() {
+ if ((this.messages == null) || (this.messages.size() == 0))
+ return true;
+ return this.messages.get(this.messages.size() - 1).isRead();
+ }
+
+ public void markRead() {
+ if (this.messages == null) {
+ return;
+ }
+ for (int i = this.messages.size() - 1; i >= 0; --i) {
+ if (messages.get(i).isRead()) {
+ break;
+ }
+ this.messages.get(i).markRead();
+ }
+ }
+
+ public String getLatestMarkableMessageId() {
+ if (this.messages == null) {
+ return null;
+ }
+ 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).getRemoteMsgId();
+ }
+ }
+ }
+ return null;
+ }
+
+ public Message getLatestMessage() {
+ if ((this.messages == null) || (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 void setMessages(ArrayList<Message> msgs) {
+ this.messages = msgs;
+ }
+
+ public String getName() {
+ if (getMode() == MODE_MULTI && getMucOptions().getSubject() != null) {
+ return getMucOptions().getSubject();
+ } else if (getMode() == MODE_MULTI && bookmark != null
+ && bookmark.getName() != null) {
+ return bookmark.getName();
+ } else {
+ return this.getContact().getDisplayName();
+ }
+ }
+
+ public String getProfilePhotoString() {
+ return this.getContact().getProfilePhoto();
+ }
+
+ 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(Account account) {
+ this.account = account;
+ }
+
+ public String getContactJid() {
+ 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);
+ 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) {
+ return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)),
+ cursor.getString(cursor.getColumnIndex(NAME)),
+ cursor.getString(cursor.getColumnIndex(CONTACT)),
+ cursor.getString(cursor.getColumnIndex(ACCOUNT)),
+ cursor.getString(cursor.getColumnIndex(CONTACTJID)),
+ 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(XmppConnectionService service,
+ String presence, boolean sendStart) {
+ if (this.otrSession != null) {
+ return this.otrSession;
+ } else {
+ SessionID sessionId = new SessionID(this.getContactJid().split("/",
+ 2)[0], presence, "xmpp");
+ this.otrSession = new SessionImpl(sessionId, getAccount()
+ .getOtrEngine(service));
+ 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;
+ }
+
+ public void startOtrIfNeeded() {
+ if (this.otrSession != null
+ && this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) {
+ try {
+ this.otrSession.startSession();
+ } catch (OtrException e) {
+ this.resetOtrSession();
+ }
+ }
+ }
+
+ 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 String getOtrFingerprint() {
+ if (this.otrFingerprint == null) {
+ try {
+ if (getOtrSession() == null) {
+ return "";
+ }
+ DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession()
+ .getRemotePublicKey();
+ StringBuilder builder = new StringBuilder(
+ new OtrCryptoEngineImpl().getFingerprint(remotePubKey));
+ builder.insert(8, " ");
+ builder.insert(17, " ");
+ builder.insert(26, " ");
+ builder.insert(35, " ");
+ this.otrFingerprint = builder.toString();
+ } catch (OtrCryptoException e) {
+
+ }
+ }
+ return this.otrFingerprint;
+ }
+
+ 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(String jid) {
+ this.contactJid = jid;
+ }
+
+ public void setNextPresence(String presence) {
+ this.nextPresence = presence;
+ }
+
+ public String getNextPresence() {
+ return this.nextPresence;
+ }
+
+ public int getLatestEncryption() {
+ int latestEncryption = this.getLatestMessage().getEncryption();
+ if ((latestEncryption == Message.ENCRYPTION_DECRYPTED)
+ || (latestEncryption == Message.ENCRYPTION_DECRYPTION_FAILED)) {
+ return Message.ENCRYPTION_PGP;
+ } else {
+ return latestEncryption;
+ }
+ }
+
+ public int getNextEncryption(boolean force) {
+ int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1);
+ if (next == -1) {
+ int latest = this.getLatestEncryption();
+ if (latest == Message.ENCRYPTION_NONE) {
+ if (force && getMode() == MODE_SINGLE) {
+ return Message.ENCRYPTION_OTR;
+ } else if (getContact().getPresences().size() == 1) {
+ if (getContact().getOtrFingerprints().size() >= 1) {
+ return Message.ENCRYPTION_OTR;
+ } else {
+ return latest;
+ }
+ } else {
+ return latest;
+ }
+ } else {
+ return latest;
+ }
+ }
+ if (next == Message.ENCRYPTION_NONE && force
+ && getMode() == MODE_SINGLE) {
+ return Message.ENCRYPTION_OTR;
+ } else {
+ 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 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) {
+ for (int i = this.getMessages().size() - 1; i >= 0; --i) {
+ if (this.messages.get(i).equals(message)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public void setMutedTill(long value) {
+ this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
+ }
+
+ public boolean isMuted() {
+ return SystemClock.elapsedRealtime() < this.getLongAttribute(
+ ATTRIBUTE_MUTED_TILL, 0);
+ }
+
+ public boolean setAttribute(String key, String value) {
+ try {
+ this.attributes.put(key, value);
+ return true;
+ } catch (JSONException e) {
+ return false;
+ }
+ }
+
+ public String getAttribute(String key) {
+ try {
+ return this.attributes.getString(key);
+ } catch (JSONException e) {
+ return null;
+ }
+ }
+
+ 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 void add(Message message) {
+ message.setConversation(this);
+ synchronized (this.messages) {
+ this.messages.add(message);
+ }
+ }
+
+ public void addAll(int index, List<Message> messages) {
+ synchronized (this.messages) {
+ this.messages.addAll(index, messages);
+ }
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/entities/Downloadable.java b/conversations/src/main/java/eu/siacs/conversations/entities/Downloadable.java
new file mode 100644
index 00000000..70516b20
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/entities/Downloadable.java
@@ -0,0 +1,21 @@
+package eu.siacs.conversations.entities;
+
+public interface Downloadable {
+
+ public final String[] VALID_EXTENSIONS = { "webp", "jpeg", "jpg", "png" };
+ public final String[] VALID_CRYPTO_EXTENSIONS = { "pgp", "gpg", "otr" };
+
+ public static final int STATUS_UNKNOWN = 0x200;
+ public static final int STATUS_CHECKING = 0x201;
+ public static final int STATUS_FAILED = 0x202;
+ public static final int STATUS_OFFER = 0x203;
+ public static final int STATUS_DOWNLOADING = 0x204;
+ public static final int STATUS_DELETED = 0x205;
+ public static final int STATUS_OFFER_CHECK_FILESIZE = 0x206;
+
+ public boolean start();
+
+ public int getStatus();
+
+ public long getFileSize();
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java b/conversations/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java
new file mode 100644
index 00000000..1605c75b
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java
@@ -0,0 +1,154 @@
+package eu.siacs.conversations.entities;
+
+import java.io.File;
+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.Key;
+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 eu.siacs.conversations.Config;
+import android.util.Log;
+
+public class DownloadableFile extends File {
+
+ private static final long serialVersionUID = 2247012619505115863L;
+
+ private long expectedSize = 0;
+ private String sha1sum;
+ private Key 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() {
+ if (this.aeskey != null) {
+ if (this.expectedSize == 0) {
+ return 0;
+ } else {
+ return (this.expectedSize / 16 + 1) * 16;
+ }
+ } else {
+ return this.expectedSize;
+ }
+ }
+
+ public void setExpectedSize(long size) {
+ this.expectedSize = size;
+ }
+
+ public String getSha1Sum() {
+ return this.sha1sum;
+ }
+
+ public void setSha1Sum(String sum) {
+ this.sha1sum = sum;
+ }
+
+ public void setKey(byte[] key) {
+ if (key.length == 48) {
+ byte[] secretKey = new byte[32];
+ byte[] iv = new byte[16];
+ System.arraycopy(key, 0, iv, 0, 16);
+ System.arraycopy(key, 16, secretKey, 0, 32);
+ this.aeskey = new SecretKeySpec(secretKey, "AES");
+ this.iv = iv;
+ } else if (key.length >= 32) {
+ byte[] secretKey = new byte[32];
+ System.arraycopy(key, 0, secretKey, 0, 32);
+ this.aeskey = new SecretKeySpec(secretKey, "AES");
+ } else if (key.length >= 16) {
+ byte[] secretKey = new byte[16];
+ System.arraycopy(key, 0, secretKey, 0, 16);
+ this.aeskey = new SecretKeySpec(secretKey, "AES");
+ }
+ }
+
+ public Key getKey() {
+ return this.aeskey;
+ }
+
+ public InputStream createInputStream() {
+ if (this.getKey() == null) {
+ try {
+ return new FileInputStream(this);
+ } catch (FileNotFoundException e) {
+ return null;
+ }
+ } else {
+ try {
+ IvParameterSpec ips = new IvParameterSpec(iv);
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+ cipher.init(Cipher.ENCRYPT_MODE, this.getKey(), ips);
+ Log.d(Config.LOGTAG, "opening encrypted input stream");
+ return new CipherInputStream(new FileInputStream(this), cipher);
+ } catch (NoSuchAlgorithmException e) {
+ Log.d(Config.LOGTAG, "no such algo: " + e.getMessage());
+ return null;
+ } catch (NoSuchPaddingException e) {
+ Log.d(Config.LOGTAG, "no such padding: " + e.getMessage());
+ return null;
+ } catch (InvalidKeyException e) {
+ Log.d(Config.LOGTAG, "invalid key: " + e.getMessage());
+ return null;
+ } catch (InvalidAlgorithmParameterException e) {
+ Log.d(Config.LOGTAG, "invavid iv:" + e.getMessage());
+ return null;
+ } catch (FileNotFoundException e) {
+ return null;
+ }
+ }
+ }
+
+ public OutputStream createOutputStream() {
+ if (this.getKey() == null) {
+ try {
+ return new FileOutputStream(this);
+ } catch (FileNotFoundException e) {
+ return null;
+ }
+ } else {
+ try {
+ IvParameterSpec ips = new IvParameterSpec(this.iv);
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+ cipher.init(Cipher.DECRYPT_MODE, this.getKey(), ips);
+ Log.d(Config.LOGTAG, "opening encrypted output stream");
+ return new CipherOutputStream(new FileOutputStream(this),
+ cipher);
+ } catch (NoSuchAlgorithmException e) {
+ Log.d(Config.LOGTAG, "no such algo: " + e.getMessage());
+ return null;
+ } catch (NoSuchPaddingException e) {
+ Log.d(Config.LOGTAG, "no such padding: " + e.getMessage());
+ return null;
+ } catch (InvalidKeyException e) {
+ Log.d(Config.LOGTAG, "invalid key: " + e.getMessage());
+ return null;
+ } catch (InvalidAlgorithmParameterException e) {
+ Log.d(Config.LOGTAG, "invavid iv:" + e.getMessage());
+ return null;
+ } catch (FileNotFoundException e) {
+ return null;
+ }
+ }
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/entities/ListItem.java b/conversations/src/main/java/eu/siacs/conversations/entities/ListItem.java
new file mode 100644
index 00000000..a1872d2f
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/entities/ListItem.java
@@ -0,0 +1,7 @@
+package eu.siacs.conversations.entities;
+
+public interface ListItem extends Comparable<ListItem> {
+ public String getDisplayName();
+
+ public String getJid();
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/entities/Message.java b/conversations/src/main/java/eu/siacs/conversations/entities/Message.java
new file mode 100644
index 00000000..a390c7ca
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/entities/Message.java
@@ -0,0 +1,478 @@
+package eu.siacs.conversations.entities;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Arrays;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+
+public class Message extends AbstractEntity {
+
+ public static final String TABLENAME = "messages";
+
+ public static final int STATUS_RECEIVED = 0;
+ public static final int STATUS_UNSEND = 1;
+ public static final int STATUS_SEND = 2;
+ public static final int STATUS_SEND_FAILED = 3;
+ public static final int STATUS_SEND_REJECTED = 4;
+ 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 TYPE_TEXT = 0;
+ public static final int TYPE_IMAGE = 1;
+ public static final int TYPE_AUDIO = 2;
+ public static final int TYPE_STATUS = 3;
+ public static final int TYPE_PRIVATE = 4;
+
+ public static String CONVERSATION = "conversationUuid";
+ public static String COUNTERPART = "counterpart";
+ public static String TRUE_COUNTERPART = "trueCounterpart";
+ public static String BODY = "body";
+ public static String TIME_SENT = "timeSent";
+ public static String ENCRYPTION = "encryption";
+ public static String STATUS = "status";
+ public static String TYPE = "type";
+ public static String REMOTE_MSG_ID = "remoteMsgId";
+
+ protected String conversationUuid;
+ protected String counterpart;
+ protected String trueCounterpart;
+ protected String body;
+ protected String encryptedBody;
+ protected long timeSent;
+ protected int encryption;
+ protected int status;
+ protected int type;
+ protected boolean read = true;
+ protected String remoteMsgId = null;
+
+ protected Conversation conversation = null;
+ protected Downloadable downloadable = null;
+ public boolean markable = false;
+
+ private Message mNextMessage = null;
+ private Message mPreviousMessage = null;
+
+ private Message() {
+
+ }
+
+ public Message(Conversation conversation, String body, int encryption) {
+ this(java.util.UUID.randomUUID().toString(), conversation.getUuid(),
+ conversation.getContactJid(), null, body, System
+ .currentTimeMillis(), encryption,
+ Message.STATUS_UNSEND, TYPE_TEXT, null);
+ this.conversation = conversation;
+ }
+
+ public Message(Conversation conversation, String counterpart, String body,
+ int encryption, int status) {
+ this(java.util.UUID.randomUUID().toString(), conversation.getUuid(),
+ counterpart, null, body, System.currentTimeMillis(),
+ encryption, status, TYPE_TEXT, null);
+ this.conversation = conversation;
+ }
+
+ public Message(String uuid, String conversationUUid, String counterpart,
+ String trueCounterpart, String body, long timeSent, int encryption,
+ int status, int type, String remoteMsgId) {
+ 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.remoteMsgId = remoteMsgId;
+ }
+
+ @Override
+ public ContentValues getContentValues() {
+ ContentValues values = new ContentValues();
+ values.put(UUID, uuid);
+ values.put(CONVERSATION, conversationUuid);
+ values.put(COUNTERPART, counterpart);
+ values.put(TRUE_COUNTERPART, trueCounterpart);
+ values.put(BODY, body);
+ values.put(TIME_SENT, timeSent);
+ values.put(ENCRYPTION, encryption);
+ values.put(STATUS, status);
+ values.put(TYPE, type);
+ values.put(REMOTE_MSG_ID, remoteMsgId);
+ return values;
+ }
+
+ public String getConversationUuid() {
+ return conversationUuid;
+ }
+
+ public Conversation getConversation() {
+ return this.conversation;
+ }
+
+ public String getCounterpart() {
+ return 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 String getReadableBody(Context context) {
+ if (encryption == ENCRYPTION_PGP) {
+ return context.getText(R.string.encrypted_message_received)
+ .toString();
+ } else if (encryption == ENCRYPTION_DECRYPTION_FAILED) {
+ return context.getText(R.string.decryption_failed).toString();
+ } else if (type == TYPE_IMAGE) {
+ return context.getText(R.string.image_file).toString();
+ } else {
+ return body.trim();
+ }
+ }
+
+ public long getTimeSent() {
+ return timeSent;
+ }
+
+ public int getEncryption() {
+ return encryption;
+ }
+
+ public int getStatus() {
+ return status;
+ }
+
+ public String getRemoteMsgId() {
+ return this.remoteMsgId;
+ }
+
+ public void setRemoteMsgId(String id) {
+ this.remoteMsgId = id;
+ }
+
+ public static Message fromCursor(Cursor cursor) {
+ return new Message(cursor.getString(cursor.getColumnIndex(UUID)),
+ cursor.getString(cursor.getColumnIndex(CONVERSATION)),
+ cursor.getString(cursor.getColumnIndex(COUNTERPART)),
+ cursor.getString(cursor.getColumnIndex(TRUE_COUNTERPART)),
+ cursor.getString(cursor.getColumnIndex(BODY)),
+ cursor.getLong(cursor.getColumnIndex(TIME_SENT)),
+ cursor.getInt(cursor.getColumnIndex(ENCRYPTION)),
+ cursor.getInt(cursor.getColumnIndex(STATUS)),
+ cursor.getInt(cursor.getColumnIndex(TYPE)),
+ cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)));
+ }
+
+ public void setConversation(Conversation conv) {
+ this.conversation = conv;
+ }
+
+ public void setStatus(int status) {
+ this.status = status;
+ }
+
+ 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 void setEncryption(int encryption) {
+ this.encryption = encryption;
+ }
+
+ public void setBody(String body) {
+ this.body = body;
+ }
+
+ public String getEncryptedBody() {
+ return this.encryptedBody;
+ }
+
+ public void setEncryptedBody(String body) {
+ this.encryptedBody = body;
+ }
+
+ public void setType(int type) {
+ this.type = type;
+ }
+
+ public int getType() {
+ return this.type;
+ }
+
+ public void setPresence(String presence) {
+ if (presence == null) {
+ this.counterpart = this.counterpart.split("/", 2)[0];
+ } else {
+ this.counterpart = this.counterpart.split("/", 2)[0] + "/"
+ + presence;
+ }
+ }
+
+ public void setTrueCounterpart(String trueCounterpart) {
+ this.trueCounterpart = trueCounterpart;
+ }
+
+ public String getPresence() {
+ String[] counterparts = this.counterpart.split("/", 2);
+ if (counterparts.length == 2) {
+ return counterparts[1];
+ } else {
+ if (this.counterpart.contains("/")) {
+ return "";
+ } else {
+ return null;
+ }
+ }
+ }
+
+ public void setDownloadable(Downloadable downloadable) {
+ this.downloadable = downloadable;
+ }
+
+ public Downloadable getDownloadable() {
+ return this.downloadable;
+ }
+
+ public static Message createStatusMessage(Conversation conversation) {
+ Message message = new Message();
+ message.setType(Message.TYPE_STATUS);
+ message.setConversation(conversation);
+ return message;
+ }
+
+ public void setCounterpart(String counterpart) {
+ this.counterpart = counterpart;
+ }
+
+ public boolean equals(Message message) {
+ if ((this.remoteMsgId != null) && (this.body != null)
+ && (this.counterpart != null)) {
+ return this.remoteMsgId.equals(message.getRemoteMsgId())
+ && this.body.equals(message.getBody())
+ && this.counterpart.equals(message.getCounterpart());
+ } else {
+ return false;
+ }
+ }
+
+ public Message next() {
+ if (this.mNextMessage == null) {
+ synchronized (this.conversation.messages) {
+ int index = this.conversation.messages.indexOf(this);
+ if (index < 0
+ || index >= this.conversation.getMessages().size() - 1) {
+ this.mNextMessage = null;
+ } else {
+ this.mNextMessage = this.conversation.messages
+ .get(index + 1);
+ }
+ }
+ }
+ return this.mNextMessage;
+ }
+
+ public Message prev() {
+ if (this.mPreviousMessage == null) {
+ synchronized (this.conversation.messages) {
+ 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 mergable(Message message) {
+ if (message == null) {
+ return false;
+ }
+ return (message.getType() == Message.TYPE_TEXT
+ && this.getDownloadable() == null
+ && message.getDownloadable() == null
+ && message.getEncryption() != Message.ENCRYPTION_PGP
+ && this.getType() == message.getType()
+ && this.getEncryption() == message.getEncryption()
+ && this.getCounterpart().equals(message.getCounterpart())
+ && (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) && ((this
+ .getStatus() == message.getStatus() || ((this.getStatus() == Message.STATUS_SEND || this
+ .getStatus() == Message.STATUS_SEND_RECEIVED) && (message
+ .getStatus() == Message.STATUS_UNSEND
+ || message.getStatus() == Message.STATUS_SEND || message
+ .getStatus() == Message.STATUS_SEND_DISPLAYED)))));
+ }
+
+ public String getMergedBody() {
+ Message next = this.next();
+ if (this.mergable(next)) {
+ return body.trim() + '\n' + next.getMergedBody();
+ }
+ return body.trim();
+ }
+
+ public int getMergedStatus() {
+ Message next = this.next();
+ if (this.mergable(next)) {
+ return next.getMergedStatus();
+ } else {
+ return getStatus();
+ }
+ }
+
+ public long getMergedTimeSent() {
+ Message next = this.next();
+ if (this.mergable(next)) {
+ return next.getMergedTimeSent();
+ } else {
+ return getTimeSent();
+ }
+ }
+
+ public boolean wasMergedIntoPrevious() {
+ Message prev = this.prev();
+ if (prev == null) {
+ return false;
+ } else {
+ return prev.mergable(this);
+ }
+ }
+
+ public boolean bodyContainsDownloadable() {
+ Contact contact = this.getContact();
+ if (status <= STATUS_RECEIVED
+ && (contact == null || !contact.trusted())) {
+ return false;
+ }
+ try {
+ URL url = new URL(this.getBody());
+ if (!url.getProtocol().equalsIgnoreCase("http")
+ && !url.getProtocol().equalsIgnoreCase("https")) {
+ return false;
+ }
+ if (url.getPath() == null) {
+ return false;
+ }
+ String[] pathParts = url.getPath().split("/");
+ String filename = pathParts[pathParts.length - 1];
+ String[] extensionParts = filename.split("\\.");
+ if (extensionParts.length == 2
+ && Arrays.asList(Downloadable.VALID_EXTENSIONS).contains(
+ extensionParts[extensionParts.length - 1])) {
+ return true;
+ } else if (extensionParts.length == 3
+ && Arrays
+ .asList(Downloadable.VALID_CRYPTO_EXTENSIONS)
+ .contains(extensionParts[extensionParts.length - 1])
+ && Arrays.asList(Downloadable.VALID_EXTENSIONS).contains(
+ extensionParts[extensionParts.length - 2])) {
+ return true;
+ } else {
+ return false;
+ }
+ } catch (MalformedURLException e) {
+ return false;
+ }
+ }
+
+ public ImageParams getImageParams() {
+ ImageParams params = new ImageParams();
+ if (this.downloadable != null) {
+ params.size = this.downloadable.getFileSize();
+ }
+ if (body == null) {
+ return params;
+ }
+ String parts[] = body.split(",");
+ if (parts.length == 1) {
+ try {
+ params.size = Long.parseLong(parts[0]);
+ } catch (NumberFormatException e) {
+ params.origin = parts[0];
+ }
+ } else if (parts.length == 3) {
+ try {
+ params.size = Long.parseLong(parts[0]);
+ } catch (NumberFormatException e) {
+ params.size = 0;
+ }
+ try {
+ params.width = Integer.parseInt(parts[1]);
+ } catch (NumberFormatException e) {
+ params.width = 0;
+ }
+ try {
+ params.height = Integer.parseInt(parts[2]);
+ } catch (NumberFormatException e) {
+ params.height = 0;
+ }
+ } else if (parts.length == 4) {
+ params.origin = parts[0];
+ try {
+ params.size = Long.parseLong(parts[1]);
+ } catch (NumberFormatException e) {
+ params.size = 0;
+ }
+ try {
+ params.width = Integer.parseInt(parts[2]);
+ } catch (NumberFormatException e) {
+ params.width = 0;
+ }
+ try {
+ params.height = Integer.parseInt(parts[3]);
+ } catch (NumberFormatException e) {
+ params.height = 0;
+ }
+ }
+ return params;
+ }
+
+ public class ImageParams {
+ public long size = 0;
+ public int width = 0;
+ public int height = 0;
+ public String origin;
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/conversations/src/main/java/eu/siacs/conversations/entities/MucOptions.java
new file mode 100644
index 00000000..d7407cd5
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/entities/MucOptions.java
@@ -0,0 +1,369 @@
+package eu.siacs.conversations.entities;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import eu.siacs.conversations.crypto.PgpEngine;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
+import android.annotation.SuppressLint;
+
+@SuppressLint("DefaultLocale")
+public class MucOptions {
+ public static final int ERROR_NO_ERROR = 0;
+ public static final int ERROR_NICK_IN_USE = 1;
+ public static final int ERROR_ROOM_NOT_FOUND = 2;
+ public static final int ERROR_PASSWORD_REQUIRED = 3;
+ public static final int ERROR_BANNED = 4;
+ public static final int ERROR_MEMBERS_ONLY = 5;
+
+ public static final int KICKED_FROM_ROOM = 9;
+
+ public static final String STATUS_CODE_BANNED = "301";
+ public static final String STATUS_CODE_KICKED = "307";
+
+ public interface OnRenameListener {
+ public void onRename(boolean success);
+ }
+
+ public class User {
+ public static final int ROLE_MODERATOR = 3;
+ public static final int ROLE_NONE = 0;
+ public static final int ROLE_PARTICIPANT = 2;
+ public static final int ROLE_VISITOR = 1;
+ public static final int AFFILIATION_ADMIN = 4;
+ public static final int AFFILIATION_OWNER = 3;
+ public static final int AFFILIATION_MEMBER = 2;
+ public static final int AFFILIATION_OUTCAST = 1;
+ public static final int AFFILIATION_NONE = 0;
+
+ private int role;
+ private int affiliation;
+ private String name;
+ private String jid;
+ private long pgpKeyId = 0;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String user) {
+ this.name = user;
+ }
+
+ public void setJid(String jid) {
+ this.jid = jid;
+ }
+
+ public String getJid() {
+ return this.jid;
+ }
+
+ public int getRole() {
+ return this.role;
+ }
+
+ public void setRole(String role) {
+ role = role.toLowerCase();
+ if (role.equals("moderator")) {
+ this.role = ROLE_MODERATOR;
+ } else if (role.equals("participant")) {
+ this.role = ROLE_PARTICIPANT;
+ } else if (role.equals("visitor")) {
+ this.role = ROLE_VISITOR;
+ } else {
+ this.role = ROLE_NONE;
+ }
+ }
+
+ public int getAffiliation() {
+ return this.affiliation;
+ }
+
+ public void setAffiliation(String affiliation) {
+ if (affiliation.equalsIgnoreCase("admin")) {
+ this.affiliation = AFFILIATION_ADMIN;
+ } else if (affiliation.equalsIgnoreCase("owner")) {
+ this.affiliation = AFFILIATION_OWNER;
+ } else if (affiliation.equalsIgnoreCase("member")) {
+ this.affiliation = AFFILIATION_MEMBER;
+ } else if (affiliation.equalsIgnoreCase("outcast")) {
+ this.affiliation = AFFILIATION_OUTCAST;
+ } else {
+ this.affiliation = AFFILIATION_NONE;
+ }
+ }
+
+ public void setPgpKeyId(long id) {
+ this.pgpKeyId = id;
+ }
+
+ public long getPgpKeyId() {
+ return this.pgpKeyId;
+ }
+
+ public Contact getContact() {
+ return account.getRoster().getContactFromRoster(getJid());
+ }
+ }
+
+ private Account account;
+ private List<User> users = new CopyOnWriteArrayList<User>();
+ private Conversation conversation;
+ private boolean isOnline = false;
+ private int error = ERROR_ROOM_NOT_FOUND;
+ private OnRenameListener renameListener = null;
+ private boolean aboutToRename = false;
+ private User self = new User();
+ private String subject = null;
+ private String joinnick;
+ private String password = null;
+
+ public MucOptions(Conversation conversation) {
+ this.account = conversation.getAccount();
+ this.conversation = conversation;
+ }
+
+ public void deleteUser(String name) {
+ for (int i = 0; i < users.size(); ++i) {
+ if (users.get(i).getName().equals(name)) {
+ users.remove(i);
+ return;
+ }
+ }
+ }
+
+ public void addUser(User user) {
+ for (int i = 0; i < users.size(); ++i) {
+ if (users.get(i).getName().equals(user.getName())) {
+ users.set(i, user);
+ return;
+ }
+ }
+ users.add(user);
+ }
+
+ public void processPacket(PresencePacket packet, PgpEngine pgp) {
+ String[] fromParts = packet.getFrom().split("/", 2);
+ if (fromParts.length >= 2) {
+ String name = fromParts[1];
+ String type = packet.getAttribute("type");
+ if (type == null) {
+ User user = new User();
+ Element item = packet.findChild("x",
+ "http://jabber.org/protocol/muc#user")
+ .findChild("item");
+ user.setName(name);
+ user.setAffiliation(item.getAttribute("affiliation"));
+ user.setRole(item.getAttribute("role"));
+ user.setJid(item.getAttribute("jid"));
+ user.setName(name);
+ if (name.equals(this.joinnick)) {
+ this.isOnline = true;
+ this.error = ERROR_NO_ERROR;
+ self = user;
+ if (aboutToRename) {
+ if (renameListener != null) {
+ renameListener.onRename(true);
+ }
+ aboutToRename = false;
+ }
+ } else {
+ addUser(user);
+ }
+ if (pgp != null) {
+ Element x = packet.findChild("x", "jabber:x:signed");
+ if (x != null) {
+ Element status = packet.findChild("status");
+ String msg;
+ if (status != null) {
+ msg = status.getContent();
+ } else {
+ msg = "";
+ }
+ user.setPgpKeyId(pgp.fetchKeyId(account, msg,
+ x.getContent()));
+ }
+ }
+ } else if (type.equals("unavailable") && name.equals(this.joinnick)) {
+ Element x = packet.findChild("x",
+ "http://jabber.org/protocol/muc#user");
+ if (x != null) {
+ Element status = x.findChild("status");
+ if (status != null) {
+ String code = status.getAttribute("code");
+ if (STATUS_CODE_KICKED.equals(code)) {
+ this.isOnline = false;
+ this.error = KICKED_FROM_ROOM;
+ } else if (STATUS_CODE_BANNED.equals(code)) {
+ this.isOnline = false;
+ this.error = ERROR_BANNED;
+ }
+ }
+ }
+ } else if (type.equals("unavailable")) {
+ deleteUser(packet.getAttribute("from").split("/", 2)[1]);
+ } else if (type.equals("error")) {
+ Element error = packet.findChild("error");
+ if (error != null && error.hasChild("conflict")) {
+ if (aboutToRename) {
+ if (renameListener != null) {
+ renameListener.onRename(false);
+ }
+ aboutToRename = false;
+ this.setJoinNick(getActualNick());
+ } else {
+ this.error = ERROR_NICK_IN_USE;
+ }
+ } else if (error != null && error.hasChild("not-authorized")) {
+ this.error = ERROR_PASSWORD_REQUIRED;
+ } else if (error != null && error.hasChild("forbidden")) {
+ this.error = ERROR_BANNED;
+ } else if (error != null
+ && error.hasChild("registration-required")) {
+ this.error = ERROR_MEMBERS_ONLY;
+ }
+ }
+ }
+ }
+
+ public List<User> getUsers() {
+ return this.users;
+ }
+
+ public String getProposedNick() {
+ String[] mucParts = conversation.getContactJid().split("/", 2);
+ if (conversation.getBookmark() != null
+ && conversation.getBookmark().getNick() != null) {
+ return conversation.getBookmark().getNick();
+ } else {
+ if (mucParts.length == 2) {
+ return mucParts[1];
+ } else {
+ return account.getUsername();
+ }
+ }
+ }
+
+ public String getActualNick() {
+ if (this.self.getName() != null) {
+ return this.self.getName();
+ } else {
+ return this.getProposedNick();
+ }
+ }
+
+ public void setJoinNick(String nick) {
+ this.joinnick = nick;
+ }
+
+ public boolean online() {
+ return this.isOnline;
+ }
+
+ public int getError() {
+ return this.error;
+ }
+
+ public void setOnRenameListener(OnRenameListener listener) {
+ this.renameListener = listener;
+ }
+
+ public OnRenameListener getOnRenameListener() {
+ return this.renameListener;
+ }
+
+ public void setOffline() {
+ this.users.clear();
+ this.error = 0;
+ this.isOnline = false;
+ }
+
+ public User getSelf() {
+ return self;
+ }
+
+ public void setSubject(String content) {
+ this.subject = content;
+ }
+
+ public String getSubject() {
+ return this.subject;
+ }
+
+ public void flagAboutToRename() {
+ this.aboutToRename = true;
+ }
+
+ public long[] getPgpKeyIds() {
+ List<Long> ids = new ArrayList<Long>();
+ for (User user : getUsers()) {
+ if (user.getPgpKeyId() != 0) {
+ ids.add(user.getPgpKeyId());
+ }
+ }
+ long[] primitivLongArray = new long[ids.size()];
+ for (int i = 0; i < ids.size(); ++i) {
+ primitivLongArray[i] = ids.get(i);
+ }
+ return primitivLongArray;
+ }
+
+ public boolean pgpKeysInUse() {
+ for (User user : getUsers()) {
+ if (user.getPgpKeyId() != 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public boolean everybodyHasKeys() {
+ for (User user : getUsers()) {
+ if (user.getPgpKeyId() == 0) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public String getJoinJid() {
+ return this.conversation.getContactJid().split("/", 2)[0] + "/"
+ + this.joinnick;
+ }
+
+ public String getTrueCounterpart(String counterpart) {
+ for (User user : this.getUsers()) {
+ if (user.getName().equals(counterpart)) {
+ return user.getJid();
+ }
+ }
+ return null;
+ }
+
+ 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;
+ }
+} \ No newline at end of file
diff --git a/conversations/src/main/java/eu/siacs/conversations/entities/Presences.java b/conversations/src/main/java/eu/siacs/conversations/entities/Presences.java
new file mode 100644
index 00000000..b5899847
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/entities/Presences.java
@@ -0,0 +1,76 @@
+package eu.siacs.conversations.entities;
+
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.Map.Entry;
+
+import eu.siacs.conversations.xml.Element;
+
+public class Presences {
+
+ public static final int CHAT = -1;
+ public static final int ONLINE = 0;
+ public static final int AWAY = 1;
+ public static final int XA = 2;
+ public static final int DND = 3;
+ public static final int OFFLINE = 4;
+
+ private Hashtable<String, Integer> presences = new Hashtable<String, Integer>();
+
+ public Hashtable<String, Integer> getPresences() {
+ return this.presences;
+ }
+
+ public void updatePresence(String resource, int status) {
+ this.presences.put(resource, status);
+ }
+
+ public void removePresence(String resource) {
+ this.presences.remove(resource);
+ }
+
+ public void clearPresences() {
+ this.presences.clear();
+ }
+
+ public int getMostAvailableStatus() {
+ int status = OFFLINE;
+ Iterator<Entry<String, Integer>> it = presences.entrySet().iterator();
+ while (it.hasNext()) {
+ Entry<String, Integer> entry = it.next();
+ if (entry.getValue() < status)
+ status = entry.getValue();
+ }
+ return status;
+ }
+
+ public static int parseShow(Element show) {
+ if ((show == null) || (show.getContent() == null)) {
+ return Presences.ONLINE;
+ } else if (show.getContent().equals("away")) {
+ return Presences.AWAY;
+ } else if (show.getContent().equals("xa")) {
+ return Presences.XA;
+ } else if (show.getContent().equals("chat")) {
+ return Presences.CHAT;
+ } else if (show.getContent().equals("dnd")) {
+ return Presences.DND;
+ } else {
+ return Presences.OFFLINE;
+ }
+ }
+
+ public int size() {
+ return presences.size();
+ }
+
+ public String[] asStringArray() {
+ final String[] presencesArray = new String[presences.size()];
+ presences.keySet().toArray(presencesArray);
+ return presencesArray;
+ }
+
+ public boolean has(String presence) {
+ return presences.containsKey(presence);
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/entities/Roster.java b/conversations/src/main/java/eu/siacs/conversations/entities/Roster.java
new file mode 100644
index 00000000..3267b15a
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/entities/Roster.java
@@ -0,0 +1,83 @@
+package eu.siacs.conversations.entities;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class Roster {
+ Account account;
+ ConcurrentHashMap<String, Contact> contacts = new ConcurrentHashMap<String, Contact>();
+ private String version = null;
+
+ public Roster(Account account) {
+ this.account = account;
+ }
+
+ public Contact getContactFromRoster(String jid) {
+ if (jid == null) {
+ return null;
+ }
+ String cleanJid = jid.split("/", 2)[0];
+ Contact contact = contacts.get(cleanJid);
+ if (contact != null && contact.showInRoster()) {
+ return contact;
+ } else {
+ return null;
+ }
+ }
+
+ public Contact getContact(String jid) {
+ String cleanJid = jid.split("/", 2)[0].toLowerCase(Locale.getDefault());
+ if (contacts.containsKey(cleanJid)) {
+ return contacts.get(cleanJid);
+ } else {
+ Contact contact = new Contact(cleanJid);
+ contact.setAccount(account);
+ contacts.put(cleanJid, contact);
+ return contact;
+ }
+ }
+
+ public void clearPresences() {
+ for (Contact contact : getContacts()) {
+ contact.clearPresences();
+ }
+ }
+
+ public void markAllAsNotInRoster() {
+ for (Contact contact : getContacts()) {
+ contact.resetOption(Contact.Options.IN_ROSTER);
+ }
+ }
+
+ public void clearSystemAccounts() {
+ for (Contact contact : getContacts()) {
+ contact.setPhotoUri(null);
+ contact.setSystemName(null);
+ contact.setSystemAccount(null);
+ }
+ }
+
+ public List<Contact> getContacts() {
+ return new ArrayList<Contact>(this.contacts.values());
+ }
+
+ public void initContact(Contact contact) {
+ contact.setAccount(account);
+ contact.setOption(Contact.Options.IN_ROSTER);
+ contacts.put(contact.getJid(), contact);
+ }
+
+ public void setVersion(String version) {
+ this.version = version;
+ }
+
+ public String getVersion() {
+ return this.version;
+ }
+
+ public Account getAccount() {
+ return this.account;
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java b/conversations/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java
new file mode 100644
index 00000000..c96d116d
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java
@@ -0,0 +1,48 @@
+package eu.siacs.conversations.generator;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import eu.siacs.conversations.services.XmppConnectionService;
+
+import android.util.Base64;
+
+public abstract class AbstractGenerator {
+ public 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", "urn:xmpp:receipts",
+ "urn:xmpp:chat-markers:0", "http://jabber.org/protocol/muc",
+ "jabber:x:conference", "http://jabber.org/protocol/caps",
+ "http://jabber.org/protocol/disco#info",
+ "urn:xmpp:avatar:metadata+notify" };
+ public final String IDENTITY_NAME = "Conversations 0.7";
+ public final String IDENTITY_TYPE = "phone";
+
+ protected XmppConnectionService mXmppConnectionService;
+
+ protected AbstractGenerator(XmppConnectionService service) {
+ this.mXmppConnectionService = service;
+ }
+
+ public String getCapHash() {
+ StringBuilder s = new StringBuilder();
+ s.append("client/" + IDENTITY_TYPE + "//" + IDENTITY_NAME + "<");
+ MessageDigest md = null;
+ try {
+ md = MessageDigest.getInstance("SHA-1");
+ } catch (NoSuchAlgorithmException e) {
+ return null;
+ }
+ List<String> features = Arrays.asList(FEATURES);
+ Collections.sort(features);
+ for (String feature : features) {
+ s.append(feature + "<");
+ }
+ byte[] sha1 = md.digest(s.toString().getBytes());
+ return new String(Base64.encode(sha1, Base64.DEFAULT)).trim();
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/conversations/src/main/java/eu/siacs/conversations/generator/IqGenerator.java
new file mode 100644
index 00000000..d44bf0ca
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/generator/IqGenerator.java
@@ -0,0 +1,96 @@
+package eu.siacs.conversations.generator;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.pep.Avatar;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+public class IqGenerator extends AbstractGenerator {
+
+ public IqGenerator(XmppConnectionService service) {
+ super(service);
+ }
+
+ public IqPacket discoResponse(IqPacket request) {
+ IqPacket packet = new IqPacket(IqPacket.TYPE_RESULT);
+ packet.setId(request.getId());
+ packet.setTo(request.getFrom());
+ Element query = packet.addChild("query",
+ "http://jabber.org/protocol/disco#info");
+ query.setAttribute("node", request.query().getAttribute("node"));
+ Element identity = query.addChild("identity");
+ identity.setAttribute("category", "client");
+ identity.setAttribute("type", this.IDENTITY_TYPE);
+ identity.setAttribute("name", IDENTITY_NAME);
+ List<String> features = Arrays.asList(FEATURES);
+ Collections.sort(features);
+ for (String feature : features) {
+ query.addChild("feature").setAttribute("var", feature);
+ }
+ return packet;
+ }
+
+ protected IqPacket publish(String node, Element item) {
+ IqPacket packet = new IqPacket(IqPacket.TYPE_SET);
+ Element pubsub = packet.addChild("pubsub",
+ "http://jabber.org/protocol/pubsub");
+ Element publish = pubsub.addChild("publish");
+ publish.setAttribute("node", node);
+ publish.addChild(item);
+ return packet;
+ }
+
+ protected IqPacket retrieve(String node, Element item) {
+ IqPacket packet = new IqPacket(IqPacket.TYPE_GET);
+ Element pubsub = packet.addChild("pubsub",
+ "http://jabber.org/protocol/pubsub");
+ Element items = pubsub.addChild("items");
+ items.setAttribute("node", node);
+ if (item != null) {
+ items.addChild(item);
+ }
+ return packet;
+ }
+
+ public IqPacket publishAvatar(Avatar avatar) {
+ Element item = new Element("item");
+ item.setAttribute("id", avatar.sha1sum);
+ Element data = item.addChild("data", "urn:xmpp:avatar:data");
+ data.setContent(avatar.image);
+ return publish("urn:xmpp:avatar:data", item);
+ }
+
+ public IqPacket publishAvatarMetadata(Avatar avatar) {
+ Element item = new Element("item");
+ item.setAttribute("id", avatar.sha1sum);
+ Element metadata = item
+ .addChild("metadata", "urn:xmpp:avatar:metadata");
+ Element info = metadata.addChild("info");
+ info.setAttribute("bytes", avatar.size);
+ info.setAttribute("id", avatar.sha1sum);
+ info.setAttribute("height", avatar.height);
+ info.setAttribute("width", avatar.height);
+ info.setAttribute("type", avatar.type);
+ return publish("urn:xmpp:avatar:metadata", item);
+ }
+
+ public IqPacket retrieveAvatar(Avatar avatar) {
+ Element item = new Element("item");
+ item.setAttribute("id", avatar.sha1sum);
+ IqPacket packet = retrieve("urn:xmpp:avatar:data", item);
+ packet.setTo(avatar.owner);
+ return packet;
+ }
+
+ public IqPacket retrieveAvatarMetaData(String to) {
+ IqPacket packet = retrieve("urn:xmpp:avatar:metadata", null);
+ if (to != null) {
+ packet.setTo(to);
+ }
+ return packet;
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/conversations/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java
new file mode 100644
index 00000000..dd833e56
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java
@@ -0,0 +1,178 @@
+package eu.siacs.conversations.generator;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
+import net.java.otr4j.OtrException;
+import net.java.otr4j.session.Session;
+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.xml.Element;
+import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
+
+public class MessageGenerator extends AbstractGenerator {
+ public MessageGenerator(XmppConnectionService service) {
+ super(service);
+ }
+
+ private MessagePacket preparePacket(Message message, boolean addDelay) {
+ 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 (this.mXmppConnectionService.indicateReceived()) {
+ packet.addChild("request", "urn:xmpp:receipts");
+ }
+ } else if (message.getType() == Message.TYPE_PRIVATE) {
+ packet.setTo(message.getCounterpart());
+ packet.setType(MessagePacket.TYPE_CHAT);
+ } else {
+ packet.setTo(message.getCounterpart().split("/", 2)[0]);
+ packet.setType(MessagePacket.TYPE_GROUPCHAT);
+ }
+ packet.setFrom(account.getFullJid());
+ packet.setId(message.getUuid());
+ if (addDelay) {
+ addDelay(packet, message.getTimeSent());
+ }
+ return packet;
+ }
+
+ private 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 generateOtrChat(Message message) {
+ return generateOtrChat(message, false);
+ }
+
+ public MessagePacket generateOtrChat(Message message, boolean addDelay) {
+ Session otrSession = message.getConversation().getOtrSession();
+ if (otrSession == null) {
+ return null;
+ }
+ MessagePacket packet = preparePacket(message, addDelay);
+ packet.addChild("private", "urn:xmpp:carbons:2");
+ packet.addChild("no-copy", "urn:xmpp:hints");
+ try {
+ packet.setBody(otrSession.transformSending(message.getBody()));
+ return packet;
+ } catch (OtrException e) {
+ return null;
+ }
+ }
+
+ public MessagePacket generateChat(Message message) {
+ return generateChat(message, false);
+ }
+
+ public MessagePacket generateChat(Message message, boolean addDelay) {
+ MessagePacket packet = preparePacket(message, addDelay);
+ packet.setBody(message.getBody());
+ return packet;
+ }
+
+ public MessagePacket generatePgpChat(Message message) {
+ return generatePgpChat(message, false);
+ }
+
+ public MessagePacket generatePgpChat(Message message, boolean addDelay) {
+ MessagePacket packet = preparePacket(message, addDelay);
+ packet.setBody("This is an XEP-0027 encryted 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 generateNotAcceptable(MessagePacket origin) {
+ MessagePacket packet = generateError(origin);
+ Element error = packet.addChild("error");
+ error.setAttribute("type", "modify");
+ error.setAttribute("code", "406");
+ error.addChild("not-acceptable");
+ return packet;
+ }
+
+ private MessagePacket generateError(MessagePacket origin) {
+ MessagePacket packet = new MessagePacket();
+ packet.setId(origin.getId());
+ packet.setTo(origin.getFrom());
+ packet.setBody(origin.getBody());
+ packet.setType(MessagePacket.TYPE_ERROR);
+ return packet;
+ }
+
+ public MessagePacket confirm(Account account, String to, String id) {
+ MessagePacket packet = new MessagePacket();
+ packet.setType(MessagePacket.TYPE_NORMAL);
+ packet.setTo(to);
+ packet.setFrom(account.getFullJid());
+ 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.getContactJid().split("/", 2)[0]);
+ Element subjectChild = new Element("subject");
+ subjectChild.setContent(subject);
+ packet.addChild(subjectChild);
+ packet.setFrom(conversation.getAccount().getJid());
+ return packet;
+ }
+
+ public MessagePacket directInvite(Conversation conversation, String contact) {
+ MessagePacket packet = new MessagePacket();
+ packet.setType(MessagePacket.TYPE_NORMAL);
+ packet.setTo(contact);
+ packet.setFrom(conversation.getAccount().getFullJid());
+ Element x = packet.addChild("x", "jabber:x:conference");
+ x.setAttribute("jid", conversation.getContactJid().split("/", 2)[0]);
+ return packet;
+ }
+
+ public MessagePacket invite(Conversation conversation, String contact) {
+ MessagePacket packet = new MessagePacket();
+ packet.setTo(conversation.getContactJid().split("/", 2)[0]);
+ packet.setFrom(conversation.getAccount().getFullJid());
+ Element x = new Element("x");
+ x.setAttribute("xmlns", "http://jabber.org/protocol/muc#user");
+ Element invite = new Element("invite");
+ invite.setAttribute("to", contact);
+ x.addChild(invite);
+ packet.addChild(x);
+ return packet;
+ }
+
+ public MessagePacket received(Account account,
+ MessagePacket originalMessage, String namespace) {
+ MessagePacket receivedPacket = new MessagePacket();
+ receivedPacket.setType(MessagePacket.TYPE_NORMAL);
+ receivedPacket.setTo(originalMessage.getFrom());
+ receivedPacket.setFrom(account.getFullJid());
+ Element received = receivedPacket.addChild("received", namespace);
+ received.setAttribute("id", originalMessage.getId());
+ return receivedPacket;
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java b/conversations/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java
new file mode 100644
index 00000000..d896dd00
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java
@@ -0,0 +1,57 @@
+package eu.siacs.conversations.generator;
+
+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.stanzas.PresencePacket;
+
+public class PresenceGenerator extends AbstractGenerator {
+
+ public PresenceGenerator(XmppConnectionService service) {
+ super(service);
+ }
+
+ private PresencePacket subscription(String type, Contact contact) {
+ PresencePacket packet = new PresencePacket();
+ packet.setAttribute("type", type);
+ packet.setAttribute("to", contact.getJid());
+ packet.setAttribute("from", contact.getAccount().getJid());
+ 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 sendPresence(Account account) {
+ PresencePacket packet = new PresencePacket();
+ packet.setAttribute("from", account.getFullJid());
+ String sig = account.getPgpSignature();
+ if (sig != null) {
+ packet.addChild("status").setContent("online");
+ 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://conversions.siacs.eu");
+ cap.setAttribute("ver", capHash);
+ }
+ return packet;
+ }
+} \ No newline at end of file
diff --git a/conversations/src/main/java/eu/siacs/conversations/http/HttpConnection.java b/conversations/src/main/java/eu/siacs/conversations/http/HttpConnection.java
new file mode 100644
index 00000000..407a13d9
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/http/HttpConnection.java
@@ -0,0 +1,255 @@
+package eu.siacs.conversations.http;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.X509TrustManager;
+
+import org.apache.http.conn.ssl.StrictHostnameVerifier;
+
+import android.content.Intent;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.util.Log;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Downloadable;
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.utils.CryptoHelper;
+
+public class HttpConnection implements Downloadable {
+
+ private HttpConnectionManager mHttpConnectionManager;
+ private XmppConnectionService mXmppConnectionService;
+
+ private URL mUrl;
+ private Message message;
+ private DownloadableFile file;
+ private int mStatus = Downloadable.STATUS_UNKNOWN;
+
+ public HttpConnection(HttpConnectionManager manager) {
+ this.mHttpConnectionManager = manager;
+ this.mXmppConnectionService = manager.getXmppConnectionService();
+ }
+
+ @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) {
+ this.message = message;
+ this.message.setDownloadable(this);
+ try {
+ mUrl = new URL(message.getBody());
+ this.file = mXmppConnectionService.getFileBackend().getFile(
+ message, false);
+ String reference = mUrl.getRef();
+ if (reference != null && reference.length() == 96) {
+ this.file.setKey(CryptoHelper.hexToBytes(reference));
+ }
+ if (this.message.getEncryption() == Message.ENCRYPTION_OTR
+ && this.file.getKey() == null) {
+ this.message.setEncryption(Message.ENCRYPTION_NONE);
+ }
+ checkFileSize(false);
+ } catch (MalformedURLException e) {
+ this.cancel();
+ }
+ }
+
+ private void checkFileSize(boolean interactive) {
+ new Thread(new FileSizeChecker(interactive)).start();
+ }
+
+ public void cancel() {
+ mHttpConnectionManager.finishConnection(this);
+ message.setDownloadable(null);
+ mXmppConnectionService.updateConversationUi();
+ }
+
+ private void finish() {
+ Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
+ intent.setData(Uri.fromFile(file));
+ mXmppConnectionService.sendBroadcast(intent);
+ message.setDownloadable(null);
+ mHttpConnectionManager.finishConnection(this);
+ }
+
+ private void changeStatus(int status) {
+ this.mStatus = status;
+ mXmppConnectionService.updateConversationUi();
+ }
+
+ private void setupTrustManager(HttpsURLConnection connection,
+ boolean interactive) {
+ X509TrustManager trustManager;
+ HostnameVerifier hostnameVerifier;
+ if (interactive) {
+ trustManager = mXmppConnectionService.getMemorizingTrustManager();
+ hostnameVerifier = mXmppConnectionService
+ .getMemorizingTrustManager().wrapHostnameVerifier(
+ new StrictHostnameVerifier());
+ } else {
+ trustManager = mXmppConnectionService.getMemorizingTrustManager()
+ .getNonInteractive();
+ hostnameVerifier = mXmppConnectionService
+ .getMemorizingTrustManager()
+ .wrapHostnameVerifierNonInteractive(
+ new StrictHostnameVerifier());
+ }
+ try {
+ SSLContext sc = SSLContext.getInstance("TLS");
+ sc.init(null, new X509TrustManager[] { trustManager },
+ mXmppConnectionService.getRNG());
+ connection.setSSLSocketFactory(sc.getSocketFactory());
+ connection.setHostnameVerifier(hostnameVerifier);
+ } catch (KeyManagementException e) {
+ return;
+ } catch (NoSuchAlgorithmException e) {
+ return;
+ }
+ }
+
+ 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);
+ return;
+ } catch (IOException e) {
+ cancel();
+ return;
+ }
+ file.setExpectedSize(size);
+ if (size <= mHttpConnectionManager.getAutoAcceptFileSize()) {
+ new Thread(new FileDownloader(interactive)).start();
+ } else {
+ changeStatus(STATUS_OFFER);
+ }
+ }
+
+ private long retrieveFileSize() throws IOException,
+ SSLHandshakeException {
+ changeStatus(STATUS_CHECKING);
+ HttpURLConnection connection = (HttpURLConnection) mUrl
+ .openConnection();
+ connection.setRequestMethod("HEAD");
+ if (connection instanceof HttpsURLConnection) {
+ setupTrustManager((HttpsURLConnection) connection, interactive);
+ }
+ connection.connect();
+ String contentLength = connection.getHeaderField("Content-Length");
+ if (contentLength == null) {
+ throw new IOException();
+ }
+ try {
+ return Long.parseLong(contentLength, 10);
+ } catch (NumberFormatException e) {
+ throw new IOException();
+ }
+ }
+
+ }
+
+ private class FileDownloader implements Runnable {
+
+ private boolean interactive = false;
+
+ 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 (IOException e) {
+ cancel();
+ }
+ }
+
+ private void download() throws SSLHandshakeException, IOException {
+ HttpURLConnection connection = (HttpURLConnection) mUrl
+ .openConnection();
+ if (connection instanceof HttpsURLConnection) {
+ setupTrustManager((HttpsURLConnection) connection, interactive);
+ }
+ connection.connect();
+ BufferedInputStream is = new BufferedInputStream(
+ connection.getInputStream());
+ OutputStream os = file.createOutputStream();
+ int count = -1;
+ byte[] buffer = new byte[1024];
+ while ((count = is.read(buffer)) != -1) {
+ os.write(buffer, 0, count);
+ }
+ os.flush();
+ os.close();
+ is.close();
+ }
+
+ private void updateImageBounds() {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(file.getAbsolutePath(), options);
+ int imageHeight = options.outHeight;
+ int imageWidth = options.outWidth;
+ message.setBody(mUrl.toString() + "," + file.getSize() + ','
+ + imageWidth + ',' + imageHeight);
+ message.setType(Message.TYPE_IMAGE);
+ mXmppConnectionService.updateMessage(message);
+ }
+
+ }
+
+ @Override
+ public int getStatus() {
+ return this.mStatus;
+ }
+
+ @Override
+ public long getFileSize() {
+ if (this.file != null) {
+ return this.file.getExpectedSize();
+ } else {
+ return 0;
+ }
+ }
+} \ No newline at end of file
diff --git a/conversations/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java b/conversations/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java
new file mode 100644
index 00000000..9a2a2405
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java
@@ -0,0 +1,28 @@
+package eu.siacs.conversations.http;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.services.AbstractConnectionManager;
+import eu.siacs.conversations.services.XmppConnectionService;
+
+public class HttpConnectionManager extends AbstractConnectionManager {
+
+ public HttpConnectionManager(XmppConnectionService service) {
+ super(service);
+ }
+
+ private List<HttpConnection> connections = new CopyOnWriteArrayList<HttpConnection>();
+
+ public HttpConnection createNewConnection(Message message) {
+ HttpConnection connection = new HttpConnection(this);
+ connection.init(message);
+ this.connections.add(connection);
+ return connection;
+ }
+
+ public void finishConnection(HttpConnection connection) {
+ this.connections.remove(connection);
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/parser/AbstractParser.java b/conversations/src/main/java/eu/siacs/conversations/parser/AbstractParser.java
new file mode 100644
index 00000000..5541c1c6
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/parser/AbstractParser.java
@@ -0,0 +1,92 @@
+package eu.siacs.conversations.parser;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+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;
+
+public abstract class AbstractParser {
+
+ protected XmppConnectionService mXmppConnectionService;
+
+ protected AbstractParser(XmppConnectionService service) {
+ this.mXmppConnectionService = service;
+ }
+
+ protected long getTimestamp(Element packet) {
+ long now = System.currentTimeMillis();
+ ArrayList<String> stamps = new ArrayList<String>();
+ for (Element child : packet.getChildren()) {
+ if (child.getName().equals("delay")) {
+ stamps.add(child.getAttribute("stamp").replace("Z", "+0000"));
+ }
+ }
+ Collections.sort(stamps);
+ if (stamps.size() >= 1) {
+ try {
+ String stamp = stamps.get(stamps.size() - 1);
+ if (stamp.contains(".")) {
+ Date date = new SimpleDateFormat(
+ "yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.US)
+ .parse(stamp);
+ if (now < date.getTime()) {
+ return now;
+ } else {
+ return date.getTime();
+ }
+ } else {
+ Date date = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ",
+ Locale.US).parse(stamp);
+ if (now < date.getTime()) {
+ return now;
+ } else {
+ return date.getTime();
+ }
+ }
+ } catch (ParseException e) {
+ return now;
+ }
+ } else {
+ return now;
+ }
+ }
+
+ protected void updateLastseen(Element packet, Account account,
+ boolean presenceOverwrite) {
+ String[] fromParts = packet.getAttribute("from").split("/", 2);
+ String from = fromParts[0];
+ String presence = null;
+ if (fromParts.length >= 2) {
+ presence = fromParts[1];
+ } else {
+ presence = "";
+ }
+ Contact contact = account.getRoster().getContact(from);
+ long timestamp = getTimestamp(packet);
+ if (timestamp >= contact.lastseen.time) {
+ contact.lastseen.time = timestamp;
+ if ((presence != null) && (presenceOverwrite)) {
+ contact.lastseen.presence = presence;
+ }
+ }
+ }
+
+ protected String avatarData(Element items) {
+ Element item = items.findChild("item");
+ if (item == null) {
+ return null;
+ }
+ Element data = item.findChild("data", "urn:xmpp:avatar:data");
+ if (data == null) {
+ return null;
+ }
+ return data.getContent();
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/parser/IqParser.java b/conversations/src/main/java/eu/siacs/conversations/parser/IqParser.java
new file mode 100644
index 00000000..df6754f2
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/parser/IqParser.java
@@ -0,0 +1,92 @@
+package eu.siacs.conversations.parser;
+
+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.OnIqPacketReceived;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+public class IqParser extends AbstractParser implements OnIqPacketReceived {
+
+ public IqParser(XmppConnectionService service) {
+ super(service);
+ }
+
+ public void rosterItems(Account account, Element query) {
+ String version = query.getAttribute("ver");
+ if (version != null) {
+ account.getRoster().setVersion(version);
+ }
+ for (Element item : query.getChildren()) {
+ if (item.getName().equals("item")) {
+ String jid = item.getAttribute("jid");
+ String name = item.getAttribute("name");
+ String subscription = item.getAttribute("subscription");
+ Contact contact = account.getRoster().getContact(jid);
+ if (!contact.getOption(Contact.Options.DIRTY_PUSH)) {
+ contact.setServerName(name);
+ }
+ 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);
+ }
+ }
+ }
+ }
+ mXmppConnectionService.updateRosterUi();
+ }
+
+ public String avatarData(IqPacket packet) {
+ Element pubsub = packet.findChild("pubsub",
+ "http://jabber.org/protocol/pubsub");
+ if (pubsub == null) {
+ return null;
+ }
+ Element items = pubsub.findChild("items");
+ if (items == null) {
+ return null;
+ }
+ return super.avatarData(items);
+ }
+
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ if (packet.hasChild("query", "jabber:iq:roster")) {
+ String from = packet.getFrom();
+ if ((from == null) || (from.equals(account.getJid()))) {
+ Element query = packet.findChild("query");
+ this.rosterItems(account, query);
+ }
+ } 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")) {
+ IqPacket response = mXmppConnectionService.getIqGenerator()
+ .discoResponse(packet);
+ account.getXmppConnection().sendIqPacket(response, null);
+ } else if (packet.hasChild("ping", "urn:xmpp:ping")) {
+ IqPacket response = packet.generateRespone(IqPacket.TYPE_RESULT);
+ mXmppConnectionService.sendIqPacket(account, response, null);
+ } else {
+ if ((packet.getType() == IqPacket.TYPE_GET)
+ || (packet.getType() == IqPacket.TYPE_SET)) {
+ IqPacket response = packet.generateRespone(IqPacket.TYPE_ERROR);
+ 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/conversations/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/conversations/src/main/java/eu/siacs/conversations/parser/MessageParser.java
new file mode 100644
index 00000000..b5e14305
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/parser/MessageParser.java
@@ -0,0 +1,517 @@
+package eu.siacs.conversations.parser;
+
+import net.java.otr4j.session.Session;
+import net.java.otr4j.session.SessionStatus;
+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.services.NotificationService;
+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.pep.Avatar;
+import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
+
+public class MessageParser extends AbstractParser implements
+ OnMessagePacketReceived {
+ public MessageParser(XmppConnectionService service) {
+ super(service);
+ }
+
+ private Message parseChat(MessagePacket packet, Account account) {
+ String[] fromParts = packet.getFrom().split("/", 2);
+ Conversation conversation = mXmppConnectionService
+ .findOrCreateConversation(account, fromParts[0], false);
+ updateLastseen(packet, account, true);
+ String pgpBody = getPgpBody(packet);
+ Message finishedMessage;
+ if (pgpBody != null) {
+ finishedMessage = new Message(conversation, packet.getFrom(),
+ pgpBody, Message.ENCRYPTION_PGP, Message.STATUS_RECEIVED);
+ } else {
+ finishedMessage = new Message(conversation, packet.getFrom(),
+ packet.getBody(), Message.ENCRYPTION_NONE,
+ Message.STATUS_RECEIVED);
+ }
+ finishedMessage.setRemoteMsgId(packet.getId());
+ finishedMessage.markable = isMarkable(packet);
+ if (conversation.getMode() == Conversation.MODE_MULTI
+ && fromParts.length >= 2) {
+ finishedMessage.setType(Message.TYPE_PRIVATE);
+ finishedMessage.setPresence(fromParts[1]);
+ finishedMessage.setTrueCounterpart(conversation.getMucOptions()
+ .getTrueCounterpart(fromParts[1]));
+ if (conversation.hasDuplicateMessage(finishedMessage)) {
+ return null;
+ }
+
+ }
+ finishedMessage.setTime(getTimestamp(packet));
+ return finishedMessage;
+ }
+
+ private Message parseOtrChat(MessagePacket packet, Account account) {
+ boolean properlyAddressed = (packet.getTo().split("/", 2).length == 2)
+ || (account.countPresences() == 1);
+ String[] fromParts = packet.getFrom().split("/", 2);
+ Conversation conversation = mXmppConnectionService
+ .findOrCreateConversation(account, fromParts[0], false);
+ String presence;
+ if (fromParts.length >= 2) {
+ presence = fromParts[1];
+ } else {
+ presence = "";
+ }
+ updateLastseen(packet, account, true);
+ String body = packet.getBody();
+ if (body.matches("^\\?OTRv\\d*\\?")) {
+ conversation.endOtrIfNeeded();
+ }
+ if (!conversation.hasValidOtrSession()) {
+ if (properlyAddressed) {
+ conversation.startOtrSession(mXmppConnectionService, presence,
+ false);
+ } else {
+ return null;
+ }
+ } else {
+ String foreignPresence = conversation.getOtrSession()
+ .getSessionID().getUserID();
+ if (!foreignPresence.equals(presence)) {
+ conversation.endOtrIfNeeded();
+ if (properlyAddressed) {
+ conversation.startOtrSession(mXmppConnectionService,
+ presence, false);
+ } else {
+ return null;
+ }
+ }
+ }
+ try {
+ Session otrSession = conversation.getOtrSession();
+ SessionStatus before = otrSession.getSessionStatus();
+ body = otrSession.transformReceiving(body);
+ SessionStatus after = otrSession.getSessionStatus();
+ if ((before != after) && (after == SessionStatus.ENCRYPTED)) {
+ mXmppConnectionService.onOtrSessionEstablished(conversation);
+ } else if ((before != after) && (after == SessionStatus.FINISHED)) {
+ conversation.resetOtrSession();
+ mXmppConnectionService.updateConversationUi();
+ }
+ 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;
+ }
+ Message finishedMessage = new Message(conversation,
+ packet.getFrom(), body, Message.ENCRYPTION_OTR,
+ Message.STATUS_RECEIVED);
+ finishedMessage.setTime(getTimestamp(packet));
+ finishedMessage.setRemoteMsgId(packet.getId());
+ finishedMessage.markable = isMarkable(packet);
+ return finishedMessage;
+ } catch (Exception e) {
+ String receivedId = packet.getId();
+ if (receivedId != null) {
+ mXmppConnectionService.replyWithNotAcceptable(account, packet);
+ }
+ conversation.resetOtrSession();
+ return null;
+ }
+ }
+
+ private Message parseGroupchat(MessagePacket packet, Account account) {
+ int status;
+ String[] fromParts = packet.getFrom().split("/", 2);
+ if (mXmppConnectionService.find(account.pendingConferenceLeaves,
+ account, fromParts[0]) != null) {
+ return null;
+ }
+ Conversation conversation = mXmppConnectionService
+ .findOrCreateConversation(account, fromParts[0], true);
+ if (packet.hasChild("subject")) {
+ conversation.getMucOptions().setSubject(
+ packet.findChild("subject").getContent());
+ mXmppConnectionService.updateConversationUi();
+ return null;
+ }
+ if ((fromParts.length == 1)) {
+ return null;
+ }
+ String counterPart = fromParts[1];
+ if (counterPart.equals(conversation.getMucOptions().getActualNick())) {
+ if (mXmppConnectionService.markMessage(conversation,
+ packet.getId(), Message.STATUS_SEND)) {
+ return null;
+ } else {
+ status = Message.STATUS_SEND;
+ }
+ } else {
+ status = Message.STATUS_RECEIVED;
+ }
+ String pgpBody = getPgpBody(packet);
+ Message finishedMessage;
+ if (pgpBody == null) {
+ finishedMessage = new Message(conversation, counterPart,
+ packet.getBody(), Message.ENCRYPTION_NONE, status);
+ } else {
+ finishedMessage = new Message(conversation, counterPart, pgpBody,
+ Message.ENCRYPTION_PGP, status);
+ }
+ finishedMessage.setRemoteMsgId(packet.getId());
+ finishedMessage.markable = isMarkable(packet);
+ if (status == Message.STATUS_RECEIVED) {
+ finishedMessage.setTrueCounterpart(conversation.getMucOptions()
+ .getTrueCounterpart(counterPart));
+ }
+ if (packet.hasChild("delay")
+ && conversation.hasDuplicateMessage(finishedMessage)) {
+ return null;
+ }
+ finishedMessage.setTime(getTimestamp(packet));
+ return finishedMessage;
+ }
+
+ private Message parseCarbonMessage(MessagePacket packet, Account account) {
+ int status;
+ String fullJid;
+ Element forwarded;
+ if (packet.hasChild("received", "urn:xmpp:carbons:2")) {
+ forwarded = packet.findChild("received", "urn:xmpp:carbons:2")
+ .findChild("forwarded", "urn:xmpp:forward:0");
+ status = Message.STATUS_RECEIVED;
+ } else if (packet.hasChild("sent", "urn:xmpp:carbons:2")) {
+ forwarded = packet.findChild("sent", "urn:xmpp:carbons:2")
+ .findChild("forwarded", "urn:xmpp:forward:0");
+ status = Message.STATUS_SEND;
+ } else {
+ return null;
+ }
+ if (forwarded == null) {
+ return null;
+ }
+ Element message = forwarded.findChild("message");
+ if (message == null) {
+ return null;
+ }
+ if (!message.hasChild("body")) {
+ if (status == Message.STATUS_RECEIVED
+ && message.getAttribute("from") != null) {
+ parseNonMessage(message, account);
+ } else if (status == Message.STATUS_SEND
+ && message.hasChild("displayed", "urn:xmpp:chat-markers:0")) {
+ String to = message.getAttribute("to");
+ if (to != null) {
+ Conversation conversation = mXmppConnectionService.find(
+ mXmppConnectionService.getConversations(), account,
+ to.split("/")[0]);
+ if (conversation != null) {
+ mXmppConnectionService.markRead(conversation, false);
+ }
+ }
+ }
+ return null;
+ }
+ if (status == Message.STATUS_RECEIVED) {
+ fullJid = message.getAttribute("from");
+ if (fullJid == null) {
+ return null;
+ } else {
+ updateLastseen(message, account, true);
+ }
+ } else {
+ fullJid = message.getAttribute("to");
+ if (fullJid == null) {
+ return null;
+ }
+ }
+ String[] parts = fullJid.split("/", 2);
+ Conversation conversation = mXmppConnectionService
+ .findOrCreateConversation(account, parts[0], false);
+ String pgpBody = getPgpBody(message);
+ Message finishedMessage;
+ if (pgpBody != null) {
+ finishedMessage = new Message(conversation, fullJid, pgpBody,
+ Message.ENCRYPTION_PGP, status);
+ } else {
+ String body = message.findChild("body").getContent();
+ finishedMessage = new Message(conversation, fullJid, body,
+ Message.ENCRYPTION_NONE, status);
+ }
+ finishedMessage.setTime(getTimestamp(message));
+ finishedMessage.setRemoteMsgId(message.getAttribute("id"));
+ finishedMessage.markable = isMarkable(message);
+ if (conversation.getMode() == Conversation.MODE_MULTI
+ && parts.length >= 2) {
+ finishedMessage.setType(Message.TYPE_PRIVATE);
+ finishedMessage.setPresence(parts[1]);
+ finishedMessage.setTrueCounterpart(conversation.getMucOptions()
+ .getTrueCounterpart(parts[1]));
+ if (conversation.hasDuplicateMessage(finishedMessage)) {
+ return null;
+ }
+ }
+
+ return finishedMessage;
+ }
+
+ private void parseError(MessagePacket packet, Account account) {
+ String[] fromParts = packet.getFrom().split("/", 2);
+ mXmppConnectionService.markMessage(account, fromParts[0],
+ packet.getId(), Message.STATUS_SEND_FAILED);
+ }
+
+ private void parseNonMessage(Element packet, Account account) {
+ String from = packet.getAttribute("from");
+ if (packet.hasChild("event", "http://jabber.org/protocol/pubsub#event")) {
+ Element event = packet.findChild("event",
+ "http://jabber.org/protocol/pubsub#event");
+ parseEvent(event, packet.getAttribute("from"), account);
+ } else if (from != null
+ && packet.hasChild("displayed", "urn:xmpp:chat-markers:0")) {
+ String id = packet
+ .findChild("displayed", "urn:xmpp:chat-markers:0")
+ .getAttribute("id");
+ updateLastseen(packet, account, true);
+ mXmppConnectionService.markMessage(account, from.split("/", 2)[0],
+ id, Message.STATUS_SEND_DISPLAYED);
+ } else if (from != null
+ && packet.hasChild("received", "urn:xmpp:chat-markers:0")) {
+ String id = packet.findChild("received", "urn:xmpp:chat-markers:0")
+ .getAttribute("id");
+ updateLastseen(packet, account, false);
+ mXmppConnectionService.markMessage(account, from.split("/", 2)[0],
+ id, Message.STATUS_SEND_RECEIVED);
+ } else if (from != null
+ && packet.hasChild("received", "urn:xmpp:receipts")) {
+ String id = packet.findChild("received", "urn:xmpp:receipts")
+ .getAttribute("id");
+ updateLastseen(packet, account, false);
+ mXmppConnectionService.markMessage(account, from.split("/", 2)[0],
+ id, Message.STATUS_SEND_RECEIVED);
+ } else if (packet.hasChild("x", "http://jabber.org/protocol/muc#user")) {
+ Element x = packet.findChild("x",
+ "http://jabber.org/protocol/muc#user");
+ if (x.hasChild("invite")) {
+ Conversation conversation = mXmppConnectionService
+ .findOrCreateConversation(account,
+ packet.getAttribute("from"), true);
+ if (!conversation.getMucOptions().online()) {
+ if (x.hasChild("password")) {
+ Element password = x.findChild("password");
+ conversation.getMucOptions().setPassword(
+ password.getContent());
+ mXmppConnectionService.databaseBackend
+ .updateConversation(conversation);
+ }
+ mXmppConnectionService.joinMuc(conversation);
+ mXmppConnectionService.updateConversationUi();
+ }
+ }
+ } else if (packet.hasChild("x", "jabber:x:conference")) {
+ Element x = packet.findChild("x", "jabber:x:conference");
+ String jid = x.getAttribute("jid");
+ String password = x.getAttribute("password");
+ if (jid != null) {
+ Conversation conversation = mXmppConnectionService
+ .findOrCreateConversation(account, jid, true);
+ if (!conversation.getMucOptions().online()) {
+ if (password != null) {
+ conversation.getMucOptions().setPassword(password);
+ mXmppConnectionService.databaseBackend
+ .updateConversation(conversation);
+ }
+ mXmppConnectionService.joinMuc(conversation);
+ mXmppConnectionService.updateConversationUi();
+ }
+ }
+ }
+ }
+
+ private void parseEvent(Element event, String from, Account account) {
+ Element items = event.findChild("items");
+ String node = items.getAttribute("node");
+ if (node != null) {
+ if (node.equals("urn:xmpp:avatar:metadata")) {
+ Avatar avatar = Avatar.parseMetadata(items);
+ if (avatar != null) {
+ avatar.owner = from;
+ if (mXmppConnectionService.getFileBackend().isAvatarCached(
+ avatar)) {
+ if (account.getJid().equals(from)) {
+ if (account.setAvatar(avatar.getFilename())) {
+ mXmppConnectionService.databaseBackend
+ .updateAccount(account);
+ }
+ mXmppConnectionService.getAvatarService().clear(
+ account);
+ mXmppConnectionService.updateConversationUi();
+ mXmppConnectionService.updateAccountUi();
+ } else {
+ Contact contact = account.getRoster().getContact(
+ from);
+ contact.setAvatar(avatar.getFilename());
+ mXmppConnectionService.getAvatarService().clear(
+ contact);
+ mXmppConnectionService.updateConversationUi();
+ mXmppConnectionService.updateRosterUi();
+ }
+ } else {
+ mXmppConnectionService.fetchAvatar(account, avatar);
+ }
+ }
+ } else if (node.equals("http://jabber.org/protocol/nick")) {
+ Element item = items.findChild("item");
+ if (item != null) {
+ Element nick = item.findChild("nick",
+ "http://jabber.org/protocol/nick");
+ if (nick != null) {
+ if (from != null) {
+ Contact contact = account.getRoster().getContact(
+ from);
+ contact.setPresenceName(nick.getContent());
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private String getPgpBody(Element message) {
+ Element child = message.findChild("x", "jabber:x:encrypted");
+ if (child == null) {
+ return null;
+ } else {
+ return child.getContent();
+ }
+ }
+
+ private boolean isMarkable(Element message) {
+ return message.hasChild("markable", "urn:xmpp:chat-markers:0");
+ }
+
+ @Override
+ public void onMessagePacketReceived(Account account, MessagePacket packet) {
+ Message message = null;
+ boolean notify = mXmppConnectionService.getPreferences().getBoolean(
+ "show_notification", true);
+ boolean alwaysNotifyInConference = notify
+ && mXmppConnectionService.getPreferences().getBoolean(
+ "always_notify_in_conference", false);
+
+ this.parseNick(packet, account);
+
+ if ((packet.getType() == MessagePacket.TYPE_CHAT || packet.getType() == MessagePacket.TYPE_NORMAL)) {
+ if ((packet.getBody() != null)
+ && (packet.getBody().startsWith("?OTR"))) {
+ message = this.parseOtrChat(packet, account);
+ if (message != null) {
+ message.markUnread();
+ }
+ } else if (packet.hasChild("body")
+ && !(packet.hasChild("x",
+ "http://jabber.org/protocol/muc#user"))) {
+ message = this.parseChat(packet, account);
+ if (message != null) {
+ message.markUnread();
+ }
+ } else if (packet.hasChild("received", "urn:xmpp:carbons:2")
+ || (packet.hasChild("sent", "urn:xmpp:carbons:2"))) {
+ message = this.parseCarbonMessage(packet, account);
+ if (message != null) {
+ if (message.getStatus() == Message.STATUS_SEND) {
+ account.activateGracePeriod();
+ notify = false;
+ mXmppConnectionService.markRead(
+ message.getConversation(), false);
+ } else {
+ message.markUnread();
+ }
+ }
+ } else {
+ parseNonMessage(packet, account);
+ }
+ } else if (packet.getType() == MessagePacket.TYPE_GROUPCHAT) {
+ message = this.parseGroupchat(packet, account);
+ if (message != null) {
+ if (message.getStatus() == Message.STATUS_RECEIVED) {
+ message.markUnread();
+ notify = alwaysNotifyInConference
+ || NotificationService
+ .wasHighlightedOrPrivate(message);
+ } else {
+ mXmppConnectionService.markRead(message.getConversation(),
+ false);
+ account.activateGracePeriod();
+ notify = false;
+ }
+ }
+ } else if (packet.getType() == MessagePacket.TYPE_ERROR) {
+ this.parseError(packet, account);
+ return;
+ } else if (packet.getType() == MessagePacket.TYPE_HEADLINE) {
+ this.parseHeadline(packet, account);
+ return;
+ }
+ if ((message == null) || (message.getBody() == null)) {
+ return;
+ }
+ if ((mXmppConnectionService.confirmMessages())
+ && ((packet.getId() != null))) {
+ if (packet.hasChild("markable", "urn:xmpp:chat-markers:0")) {
+ MessagePacket receipt = mXmppConnectionService
+ .getMessageGenerator().received(account, packet,
+ "urn:xmpp:chat-markers:0");
+ mXmppConnectionService.sendMessagePacket(account, receipt);
+ }
+ if (packet.hasChild("request", "urn:xmpp:receipts")) {
+ MessagePacket receipt = mXmppConnectionService
+ .getMessageGenerator().received(account, packet,
+ "urn:xmpp:receipts");
+ mXmppConnectionService.sendMessagePacket(account, receipt);
+ }
+ }
+ Conversation conversation = message.getConversation();
+ conversation.add(message);
+ if (packet.getType() != MessagePacket.TYPE_ERROR) {
+ if (message.getEncryption() == Message.ENCRYPTION_NONE
+ || mXmppConnectionService.saveEncryptedMessages()) {
+ mXmppConnectionService.databaseBackend.createMessage(message);
+ }
+ }
+ if (message.bodyContainsDownloadable()) {
+ this.mXmppConnectionService.getHttpConnectionManager()
+ .createNewConnection(message);
+ }
+ notify = notify && !conversation.isMuted();
+ if (notify) {
+ mXmppConnectionService.getNotificationService().push(message);
+ }
+ mXmppConnectionService.updateConversationUi();
+ }
+
+ private void parseHeadline(MessagePacket packet, Account account) {
+ if (packet.hasChild("event", "http://jabber.org/protocol/pubsub#event")) {
+ Element event = packet.findChild("event",
+ "http://jabber.org/protocol/pubsub#event");
+ parseEvent(event, packet.getFrom(), account);
+ }
+ }
+
+ private void parseNick(MessagePacket packet, Account account) {
+ Element nick = packet.findChild("nick",
+ "http://jabber.org/protocol/nick");
+ if (nick != null) {
+ if (packet.getFrom() != null) {
+ Contact contact = account.getRoster().getContact(
+ packet.getFrom());
+ contact.setPresenceName(nick.getContent());
+ }
+ }
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/conversations/src/main/java/eu/siacs/conversations/parser/PresenceParser.java
new file mode 100644
index 00000000..4e90cda8
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/parser/PresenceParser.java
@@ -0,0 +1,133 @@
+package eu.siacs.conversations.parser;
+
+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.Presences;
+import eu.siacs.conversations.generator.PresenceGenerator;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.OnPresencePacketReceived;
+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) {
+ PgpEngine mPgpEngine = mXmppConnectionService.getPgpEngine();
+ if (packet.hasChild("x", "http://jabber.org/protocol/muc#user")) {
+ Conversation muc = mXmppConnectionService.find(account, packet
+ .getAttribute("from").split("/", 2)[0]);
+ if (muc != null) {
+ boolean before = muc.getMucOptions().online();
+ muc.getMucOptions().processPacket(packet, mPgpEngine);
+ if (before != muc.getMucOptions().online()) {
+ mXmppConnectionService.updateConversationUi();
+ }
+ mXmppConnectionService.getAvatarService().clear(muc);
+ }
+ } else if (packet.hasChild("x", "http://jabber.org/protocol/muc")) {
+ Conversation muc = mXmppConnectionService.find(account, packet
+ .getAttribute("from").split("/", 2)[0]);
+ if (muc != null) {
+ boolean before = muc.getMucOptions().online();
+ muc.getMucOptions().processPacket(packet, mPgpEngine);
+ if (before != muc.getMucOptions().online()) {
+ mXmppConnectionService.updateConversationUi();
+ }
+ mXmppConnectionService.getAvatarService().clear(muc);
+ }
+ }
+ }
+
+ public void parseContactPresence(PresencePacket packet, Account account) {
+ PresenceGenerator mPresenceGenerator = mXmppConnectionService
+ .getPresenceGenerator();
+ if (packet.getFrom() == null) {
+ return;
+ }
+ String[] fromParts = packet.getFrom().split("/", 2);
+ String type = packet.getAttribute("type");
+ if (fromParts[0].equals(account.getJid())) {
+ if (fromParts.length == 2) {
+ if (type == null) {
+ account.updatePresence(fromParts[1],
+ Presences.parseShow(packet.findChild("show")));
+ } else if (type.equals("unavailable")) {
+ account.removePresence(fromParts[1]);
+ account.deactivateGracePeriod();
+ }
+ }
+ } else {
+ Contact contact = account.getRoster().getContact(packet.getFrom());
+ if (type == null) {
+ String presence;
+ if (fromParts.length >= 2) {
+ presence = fromParts[1];
+ } else {
+ presence = "";
+ }
+ int sizeBefore = contact.getPresences().size();
+ contact.updatePresence(presence,
+ Presences.parseShow(packet.findChild("show")));
+ PgpEngine pgp = mXmppConnectionService.getPgpEngine();
+ if (pgp != null) {
+ Element x = packet.findChild("x", "jabber:x:signed");
+ if (x != null) {
+ Element status = packet.findChild("status");
+ String msg;
+ if (status != null) {
+ msg = status.getContent();
+ } else {
+ msg = "";
+ }
+ contact.setPgpKeyId(pgp.fetchKeyId(account, msg,
+ x.getContent()));
+ }
+ }
+ boolean online = sizeBefore < contact.getPresences().size();
+ updateLastseen(packet, account, true);
+ mXmppConnectionService.onContactStatusChanged
+ .onContactStatusChanged(contact, online);
+ } else if (type.equals("unavailable")) {
+ if (fromParts.length != 2) {
+ contact.clearPresences();
+ } else {
+ contact.removePresence(fromParts[1]);
+ }
+ 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);
+ }
+ }
+ Element nick = packet.findChild("nick",
+ "http://jabber.org/protocol/nick");
+ if (nick != null) {
+ contact.setPresenceName(nick.getContent());
+ }
+ }
+ mXmppConnectionService.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/conversations/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/conversations/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java
new file mode 100644
index 00000000..b49cf4e6
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java
@@ -0,0 +1,335 @@
+package eu.siacs.conversations.persistance;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+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 android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteCantOpenDatabaseException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+public class DatabaseBackend extends SQLiteOpenHelper {
+
+ private static DatabaseBackend instance = null;
+
+ private static final String DATABASE_NAME = "history";
+ private static final int DATABASE_VERSION = 8;
+
+ 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, "
+ + "FOREIGN KEY(" + Contact.ACCOUNT + ") REFERENCES "
+ + Account.TABLENAME + "(" + Account.UUID
+ + ") ON DELETE CASCADE, UNIQUE(" + Contact.ACCOUNT + ", "
+ + Contact.JID + ") ON CONFLICT REPLACE);";
+
+ private DatabaseBackend(Context context) {
+ super(context, DATABASE_NAME, null, 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.ROSTERVERSION + " TEXT," + Account.OPTIONS
+ + " NUMBER, " + Account.AVATAR + " TEXT, " + Account.KEYS
+ + " TEXT)");
+ 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.REMOTE_MSG_ID + " TEXT, FOREIGN KEY("
+ + Message.CONVERSATION + ") REFERENCES "
+ + Conversation.TABLENAME + "(" + Conversation.UUID
+ + ") ON DELETE CASCADE);");
+
+ db.execSQL(CREATE_CONTATCS_STATEMENT);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ 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");
+ }
+ }
+
+ 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) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ db.insert(Message.TABLENAME, null, message.getContentValues());
+ }
+
+ public void createAccount(Account account) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ db.insert(Account.TABLENAME, null, account.getContentValues());
+ }
+
+ public void createContact(Contact contact) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ db.insert(Contact.TABLENAME, null, contact.getContentValues());
+ }
+
+ public int getConversationCount() {
+ SQLiteDatabase db = this.getReadableDatabase();
+ Cursor cursor = db.rawQuery("select count(uuid) as count from "
+ + Conversation.TABLENAME + " where " + Conversation.STATUS
+ + "=" + Conversation.STATUS_AVAILABLE, null);
+ cursor.moveToFirst();
+ return cursor.getInt(0);
+ }
+
+ public CopyOnWriteArrayList<Conversation> getConversations(int status) {
+ CopyOnWriteArrayList<Conversation> list = new CopyOnWriteArrayList<Conversation>();
+ 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));
+ }
+ 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<Message>();
+ 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);
+ message.setConversation(conversation);
+ list.add(message);
+ } while (cursor.moveToPrevious());
+ }
+ return list;
+ }
+
+ public Conversation findConversation(Account account, String contactJid) {
+ SQLiteDatabase db = this.getReadableDatabase();
+ String[] selectionArgs = { account.getUuid(), contactJid + "%" };
+ Cursor cursor = db.query(Conversation.TABLENAME, null,
+ Conversation.ACCOUNT + "=? AND " + Conversation.CONTACTJID
+ + " like ?", selectionArgs, null, null, null);
+ if (cursor.getCount() == 0)
+ return null;
+ cursor.moveToFirst();
+ return Conversation.fromCursor(cursor);
+ }
+
+ public void updateConversation(Conversation conversation) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ String[] args = { conversation.getUuid() };
+ db.update(Conversation.TABLENAME, conversation.getContentValues(),
+ Conversation.UUID + "=?", args);
+ }
+
+ public List<Account> getAccounts() {
+ List<Account> list = new ArrayList<Account>();
+ SQLiteDatabase db = this.getReadableDatabase();
+ 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);
+ cursor.close();
+ return (count > 0);
+ } catch (SQLiteCantOpenDatabaseException e) {
+ return true; // better safe than sorry
+ }
+ }
+
+ @Override
+ public SQLiteDatabase getWritableDatabase() {
+ SQLiteDatabase db = super.getWritableDatabase();
+ db.execSQL("PRAGMA foreign_keys=ON;");
+ return db;
+ }
+
+ public void updateMessage(Message message) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ String[] args = { message.getUuid() };
+ db.update(Message.TABLENAME, message.getContentValues(), Message.UUID
+ + "=?", args);
+ }
+
+ 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(Roster roster) {
+ Account account = roster.getAccount();
+ SQLiteDatabase db = this.getWritableDatabase();
+ 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() };
+ db.delete(Contact.TABLENAME, where, whereArgs);
+ }
+ }
+ account.setRosterVersion(roster.getVersion());
+ updateAccount(account);
+ }
+
+ public void deleteMessage(Message message) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ String[] args = { message.getUuid() };
+ db.delete(Message.TABLENAME, Message.UUID + "=?", args);
+ }
+
+ public void deleteMessagesInConversation(Conversation conversation) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ String[] args = { conversation.getUuid() };
+ db.delete(Message.TABLENAME, Message.CONVERSATION + "=?", args);
+ }
+
+ public Conversation findConversationByUuid(String conversationUuid) {
+ SQLiteDatabase db = this.getReadableDatabase();
+ String[] selectionArgs = { conversationUuid };
+ Cursor cursor = db.query(Conversation.TABLENAME, null,
+ Conversation.UUID + "=?", selectionArgs, null, null, null);
+ if (cursor.getCount() == 0) {
+ return null;
+ }
+ cursor.moveToFirst();
+ return Conversation.fromCursor(cursor);
+ }
+
+ public Message findMessageByUuid(String messageUuid) {
+ SQLiteDatabase db = this.getReadableDatabase();
+ String[] selectionArgs = { messageUuid };
+ Cursor cursor = db.query(Message.TABLENAME, null, Message.UUID + "=?",
+ selectionArgs, null, null, null);
+ if (cursor.getCount() == 0) {
+ return null;
+ }
+ cursor.moveToFirst();
+ return Message.fromCursor(cursor);
+ }
+
+ public Account findAccountByUuid(String accountUuid) {
+ SQLiteDatabase db = this.getReadableDatabase();
+ String[] selectionArgs = { accountUuid };
+ Cursor cursor = db.query(Account.TABLENAME, null, Account.UUID + "=?",
+ selectionArgs, null, null, null);
+ if (cursor.getCount() == 0) {
+ return null;
+ }
+ cursor.moveToFirst();
+ return Account.fromCursor(cursor);
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/conversations/src/main/java/eu/siacs/conversations/persistance/FileBackend.java
new file mode 100644
index 00000000..b891e9ef
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/persistance/FileBackend.java
@@ -0,0 +1,480 @@
+package eu.siacs.conversations.persistance;
+
+import java.io.ByteArrayOutputStream;
+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.security.DigestOutputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.RectF;
+import android.media.ExifInterface;
+import android.net.Uri;
+import android.os.Environment;
+import android.provider.MediaStore;
+import android.util.Base64;
+import android.util.Base64OutputStream;
+import android.util.Log;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.xmpp.pep.Avatar;
+
+public class FileBackend {
+
+ private static int IMAGE_SIZE = 1920;
+
+ private SimpleDateFormat imageDateFormat = new SimpleDateFormat(
+ "yyyyMMdd_HHmmssSSS", Locale.US);
+
+ private XmppConnectionService mXmppConnectionService;
+
+ public FileBackend(XmppConnectionService service) {
+ this.mXmppConnectionService = service;
+ }
+
+ public DownloadableFile getFile(Message message) {
+ return getFile(message, true);
+ }
+
+ public DownloadableFile getFile(Message message, boolean decrypted) {
+ StringBuilder filename = new StringBuilder();
+ filename.append(getConversationsDirectory());
+ filename.append(message.getUuid());
+ if ((decrypted) || (message.getEncryption() == Message.ENCRYPTION_NONE)) {
+ filename.append(".webp");
+ } else {
+ if (message.getEncryption() == Message.ENCRYPTION_OTR) {
+ filename.append(".webp");
+ } else {
+ filename.append(".webp.pgp");
+ }
+ }
+ return new DownloadableFile(filename.toString());
+ }
+
+ public static String getConversationsDirectory() {
+ return Environment.getExternalStoragePublicDirectory(
+ Environment.DIRECTORY_PICTURES).getAbsolutePath()
+ + "/Conversations/";
+ }
+
+ public Bitmap resize(Bitmap originalBitmap, int size) {
+ int w = originalBitmap.getWidth();
+ int h = originalBitmap.getHeight();
+ if (Math.max(w, h) > size) {
+ int scalledW;
+ int scalledH;
+ if (w <= h) {
+ scalledW = (int) (w / ((double) h / size));
+ scalledH = size;
+ } else {
+ scalledW = size;
+ scalledH = (int) (h / ((double) w / size));
+ }
+ Bitmap scalledBitmap = Bitmap.createScaledBitmap(originalBitmap,
+ scalledW, scalledH, true);
+ return scalledBitmap;
+ } else {
+ return originalBitmap;
+ }
+ }
+
+ public Bitmap rotate(Bitmap bitmap, int degree) {
+ int w = bitmap.getWidth();
+ int h = bitmap.getHeight();
+ Matrix mtx = new Matrix();
+ mtx.postRotate(degree);
+ return Bitmap.createBitmap(bitmap, 0, 0, w, h, mtx, true);
+ }
+
+ public DownloadableFile copyImageToPrivateStorage(Message message, Uri image)
+ throws ImageCopyException {
+ return this.copyImageToPrivateStorage(message, image, 0);
+ }
+
+ private DownloadableFile copyImageToPrivateStorage(Message message,
+ Uri image, int sampleSize) throws ImageCopyException {
+ try {
+ InputStream is = mXmppConnectionService.getContentResolver()
+ .openInputStream(image);
+ DownloadableFile file = getFile(message);
+ file.getParentFile().mkdirs();
+ file.createNewFile();
+ Bitmap originalBitmap;
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ int inSampleSize = (int) Math.pow(2, sampleSize);
+ Log.d(Config.LOGTAG, "reading bitmap with sample size "
+ + inSampleSize);
+ options.inSampleSize = inSampleSize;
+ originalBitmap = BitmapFactory.decodeStream(is, null, options);
+ is.close();
+ if (originalBitmap == null) {
+ throw new ImageCopyException(R.string.error_not_an_image_file);
+ }
+ Bitmap scalledBitmap = resize(originalBitmap, IMAGE_SIZE);
+ originalBitmap = null;
+ int rotation = getRotation(image);
+ if (rotation > 0) {
+ scalledBitmap = rotate(scalledBitmap, rotation);
+ }
+ OutputStream os = new FileOutputStream(file);
+ boolean success = scalledBitmap.compress(
+ Bitmap.CompressFormat.WEBP, 75, os);
+ if (!success) {
+ throw new ImageCopyException(R.string.error_compressing_image);
+ }
+ os.flush();
+ os.close();
+ long size = file.getSize();
+ int width = scalledBitmap.getWidth();
+ int height = scalledBitmap.getHeight();
+ message.setBody(Long.toString(size) + ',' + width + ',' + height);
+ return file;
+ } catch (FileNotFoundException e) {
+ throw new ImageCopyException(R.string.error_file_not_found);
+ } catch (IOException e) {
+ throw new ImageCopyException(R.string.error_io_exception);
+ } catch (SecurityException e) {
+ throw new ImageCopyException(
+ R.string.error_security_exception_during_image_copy);
+ } catch (OutOfMemoryError e) {
+ ++sampleSize;
+ if (sampleSize <= 3) {
+ return copyImageToPrivateStorage(message, image, sampleSize);
+ } else {
+ throw new ImageCopyException(R.string.error_out_of_memory);
+ }
+ }
+ }
+
+ private int getRotation(Uri image) {
+ if ("content".equals(image.getScheme())) {
+ try {
+ Cursor cursor = mXmppConnectionService
+ .getContentResolver()
+ .query(image,
+ new String[] { MediaStore.Images.ImageColumns.ORIENTATION },
+ null, null, null);
+ if (cursor.getCount() != 1) {
+ return -1;
+ }
+ cursor.moveToFirst();
+ return cursor.getInt(0);
+ } catch (IllegalArgumentException e) {
+ return -1;
+ }
+ } else {
+ ExifInterface exif;
+ try {
+ exif = new ExifInterface(image.toString());
+ if (exif.getAttribute(ExifInterface.TAG_ORIENTATION)
+ .equalsIgnoreCase("6")) {
+ return 90;
+ } else if (exif.getAttribute(ExifInterface.TAG_ORIENTATION)
+ .equalsIgnoreCase("8")) {
+ return 270;
+ } else if (exif.getAttribute(ExifInterface.TAG_ORIENTATION)
+ .equalsIgnoreCase("3")) {
+ return 180;
+ } else {
+ return 0;
+ }
+ } catch (IOException e) {
+ return -1;
+ }
+ }
+ }
+
+ public Bitmap getImageFromMessage(Message message) {
+ return BitmapFactory.decodeFile(getFile(message).getAbsolutePath());
+ }
+
+ public Bitmap getThumbnail(Message message, int size, boolean cacheOnly)
+ throws FileNotFoundException {
+ Bitmap thumbnail = mXmppConnectionService.getBitmapCache().get(
+ message.getUuid());
+ if ((thumbnail == null) && (!cacheOnly)) {
+ File file = getFile(message);
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inSampleSize = calcSampleSize(file, size);
+ Bitmap fullsize = BitmapFactory.decodeFile(file.getAbsolutePath(),
+ options);
+ if (fullsize == null) {
+ throw new FileNotFoundException();
+ }
+ thumbnail = resize(fullsize, size);
+ this.mXmppConnectionService.getBitmapCache().put(message.getUuid(),
+ thumbnail);
+ }
+ return thumbnail;
+ }
+
+ public void removeFiles(Conversation conversation) {
+ String prefix = mXmppConnectionService.getFilesDir().getAbsolutePath();
+ String path = prefix + "/" + conversation.getAccount().getJid() + "/"
+ + conversation.getContactJid();
+ File file = new File(path);
+ try {
+ this.deleteFile(file);
+ } catch (IOException e) {
+ Log.d(Config.LOGTAG,
+ "error deleting file: " + file.getAbsolutePath());
+ }
+ }
+
+ private void deleteFile(File f) throws IOException {
+ if (f.isDirectory()) {
+ for (File c : f.listFiles())
+ deleteFile(c);
+ }
+ f.delete();
+ }
+
+ public Uri getTakePhotoUri() {
+ StringBuilder pathBuilder = new StringBuilder();
+ pathBuilder.append(Environment
+ .getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM));
+ pathBuilder.append('/');
+ pathBuilder.append("Camera");
+ pathBuilder.append('/');
+ pathBuilder.append("IMG_" + this.imageDateFormat.format(new Date())
+ + ".jpg");
+ Uri uri = Uri.parse("file://" + pathBuilder.toString());
+ File file = new File(uri.toString());
+ file.getParentFile().mkdirs();
+ return uri;
+ }
+
+ public Avatar getPepAvatar(Uri image, int size, Bitmap.CompressFormat format) {
+ try {
+ Avatar avatar = new Avatar();
+ Bitmap bm = cropCenterSquare(image, size);
+ if (bm == null) {
+ return null;
+ }
+ ByteArrayOutputStream mByteArrayOutputStream = new ByteArrayOutputStream();
+ Base64OutputStream mBase64OutputSttream = new Base64OutputStream(
+ mByteArrayOutputStream, Base64.DEFAULT);
+ MessageDigest digest = MessageDigest.getInstance("SHA-1");
+ DigestOutputStream mDigestOutputStream = new DigestOutputStream(
+ mBase64OutputSttream, digest);
+ if (!bm.compress(format, 75, mDigestOutputStream)) {
+ return null;
+ }
+ mDigestOutputStream.flush();
+ mDigestOutputStream.close();
+ avatar.sha1sum = CryptoHelper.bytesToHex(digest.digest());
+ avatar.image = new String(mByteArrayOutputStream.toByteArray());
+ return avatar;
+ } catch (NoSuchAlgorithmException e) {
+ return null;
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ public boolean isAvatarCached(Avatar avatar) {
+ File file = new File(getAvatarPath(avatar.getFilename()));
+ return file.exists();
+ }
+
+ public boolean save(Avatar avatar) {
+ if (isAvatarCached(avatar)) {
+ return true;
+ }
+ String filename = getAvatarPath(avatar.getFilename());
+ File file = new File(filename + ".tmp");
+ file.getParentFile().mkdirs();
+ try {
+ file.createNewFile();
+ FileOutputStream mFileOutputStream = new FileOutputStream(file);
+ MessageDigest digest = MessageDigest.getInstance("SHA-1");
+ digest.reset();
+ DigestOutputStream mDigestOutputStream = new DigestOutputStream(
+ mFileOutputStream, digest);
+ mDigestOutputStream.write(avatar.getImageAsBytes());
+ mDigestOutputStream.flush();
+ mDigestOutputStream.close();
+ avatar.size = file.length();
+ String sha1sum = CryptoHelper.bytesToHex(digest.digest());
+ if (sha1sum.equals(avatar.sha1sum)) {
+ file.renameTo(new File(filename));
+ return true;
+ } else {
+ Log.d(Config.LOGTAG, "sha1sum mismatch for " + avatar.owner);
+ file.delete();
+ return false;
+ }
+ } catch (FileNotFoundException e) {
+ return false;
+ } catch (IOException e) {
+ return false;
+ } catch (NoSuchAlgorithmException e) {
+ return false;
+ }
+ }
+
+ public String getAvatarPath(String avatar) {
+ return mXmppConnectionService.getFilesDir().getAbsolutePath()
+ + "/avatars/" + avatar;
+ }
+
+ public Uri getAvatarUri(String avatar) {
+ return Uri.parse("file:" + getAvatarPath(avatar));
+ }
+
+ public Bitmap cropCenterSquare(Uri image, int size) {
+ try {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inSampleSize = calcSampleSize(image, size);
+ InputStream is = mXmppConnectionService.getContentResolver()
+ .openInputStream(image);
+ Bitmap input = BitmapFactory.decodeStream(is, null, options);
+ if (input == null) {
+ return null;
+ } else {
+ int rotation = getRotation(image);
+ if (rotation > 0) {
+ input = rotate(input, rotation);
+ }
+ return cropCenterSquare(input, size);
+ }
+ } catch (FileNotFoundException e) {
+ return null;
+ }
+ }
+
+ public Bitmap cropCenter(Uri image, int newHeight, int newWidth) {
+ try {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inSampleSize = calcSampleSize(image,
+ Math.max(newHeight, newWidth));
+ InputStream is = mXmppConnectionService.getContentResolver()
+ .openInputStream(image);
+ Bitmap source = BitmapFactory.decodeStream(is, null, options);
+
+ int sourceWidth = source.getWidth();
+ int sourceHeight = source.getHeight();
+ float xScale = (float) newWidth / sourceWidth;
+ float yScale = (float) newHeight / sourceHeight;
+ float scale = Math.max(xScale, yScale);
+ float scaledWidth = scale * sourceWidth;
+ float scaledHeight = scale * sourceHeight;
+ float left = (newWidth - scaledWidth) / 2;
+ float top = (newHeight - scaledHeight) / 2;
+
+ RectF targetRect = new RectF(left, top, left + scaledWidth, top
+ + scaledHeight);
+ Bitmap dest = Bitmap.createBitmap(newWidth, newHeight,
+ source.getConfig());
+ Canvas canvas = new Canvas(dest);
+ canvas.drawBitmap(source, null, targetRect, null);
+
+ return dest;
+ } catch (FileNotFoundException e) {
+ return null;
+ }
+
+ }
+
+ public Bitmap cropCenterSquare(Bitmap input, int size) {
+ int w = input.getWidth();
+ int h = input.getHeight();
+
+ float scale = Math.max((float) size / h, (float) size / w);
+
+ float outWidth = scale * w;
+ float outHeight = scale * h;
+ float left = (size - outWidth) / 2;
+ float top = (size - outHeight) / 2;
+ RectF target = new RectF(left, top, left + outWidth, top + outHeight);
+
+ Bitmap output = Bitmap.createBitmap(size, size, input.getConfig());
+ Canvas canvas = new Canvas(output);
+ canvas.drawBitmap(input, null, target, null);
+ return output;
+ }
+
+ private int calcSampleSize(Uri image, int size)
+ throws FileNotFoundException {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeStream(mXmppConnectionService.getContentResolver()
+ .openInputStream(image), null, options);
+ return calcSampleSize(options, size);
+ }
+
+ private int calcSampleSize(File image, int size) {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(image.getAbsolutePath(), options);
+ return calcSampleSize(options, size);
+ }
+
+ private int calcSampleSize(BitmapFactory.Options options, int size) {
+ int height = options.outHeight;
+ int width = options.outWidth;
+ int inSampleSize = 1;
+
+ if (height > size || width > size) {
+ int halfHeight = height / 2;
+ int halfWidth = width / 2;
+
+ while ((halfHeight / inSampleSize) > size
+ && (halfWidth / inSampleSize) > size) {
+ inSampleSize *= 2;
+ }
+ }
+ return inSampleSize;
+ }
+
+ public Uri getJingleFileUri(Message message) {
+ File file = getFile(message);
+ return Uri.parse("file://" + file.getAbsolutePath());
+ }
+
+ public class ImageCopyException extends Exception {
+ private static final long serialVersionUID = -1010013599132881427L;
+ private int resId;
+
+ public ImageCopyException(int resId) {
+ this.resId = resId;
+ }
+
+ public int getResId() {
+ return resId;
+ }
+ }
+
+ public Bitmap getAvatar(String avatar, int size) {
+ if (avatar == null) {
+ return null;
+ }
+ Bitmap bm = cropCenter(getAvatarUri(avatar), size, size);
+ if (bm == null) {
+ return null;
+ }
+ return bm;
+ }
+
+ public boolean isFileAvailable(Message message) {
+ return getFile(message).exists();
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/persistance/OnPhoneContactsMerged.java b/conversations/src/main/java/eu/siacs/conversations/persistance/OnPhoneContactsMerged.java
new file mode 100644
index 00000000..6a457b17
--- /dev/null
+++ b/conversations/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/conversations/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java b/conversations/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java
new file mode 100644
index 00000000..676a09c9
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java
@@ -0,0 +1,23 @@
+package eu.siacs.conversations.services;
+
+public class AbstractConnectionManager {
+ protected XmppConnectionService mXmppConnectionService;
+
+ public AbstractConnectionManager(XmppConnectionService service) {
+ this.mXmppConnectionService = service;
+ }
+
+ public XmppConnectionService getXmppConnectionService() {
+ return this.mXmppConnectionService;
+ }
+
+ public long getAutoAcceptFileSize() {
+ String config = this.mXmppConnectionService.getPreferences().getString(
+ "auto_accept_file_size", "524288");
+ try {
+ return Long.parseLong(config);
+ } catch (NumberFormatException e) {
+ return 524288;
+ }
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/services/AvatarService.java b/conversations/src/main/java/eu/siacs/conversations/services/AvatarService.java
new file mode 100644
index 00000000..c0668a19
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/services/AvatarService.java
@@ -0,0 +1,298 @@
+package eu.siacs.conversations.services;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+import eu.siacs.conversations.Config;
+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.MucOptions;
+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 android.util.Log;
+
+public class AvatarService {
+
+ private static final int FG_COLOR = 0xFFFAFAFA;
+ private static final int TRANSPARENT = 0x00000000;
+
+ 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 ArrayList<Integer> sizes = new ArrayList<Integer>();
+
+ protected XmppConnectionService mXmppConnectionService = null;
+
+ public AvatarService(XmppConnectionService service) {
+ this.mXmppConnectionService = service;
+ }
+
+ public Bitmap get(Contact contact, int size) {
+ final String KEY = key(contact, size);
+ Bitmap avatar = this.mXmppConnectionService.getBitmapCache().get(KEY);
+ if (avatar != null) {
+ return avatar;
+ }
+ Log.d(Config.LOGTAG, "no cache hit for " + KEY);
+ avatar = mXmppConnectionService.getFileBackend().getAvatar(
+ contact.getAvatar(), size);
+ if (avatar == null) {
+ if (contact.getProfilePhoto() != null) {
+ avatar = mXmppConnectionService.getFileBackend()
+ .cropCenterSquare(Uri.parse(contact.getProfilePhoto()),
+ size);
+ if (avatar == null) {
+ avatar = get(contact.getDisplayName(), size);
+ }
+ } else {
+ avatar = get(contact.getDisplayName(), size);
+ }
+ }
+ this.mXmppConnectionService.getBitmapCache().put(KEY, avatar);
+ return avatar;
+ }
+
+ public void clear(Contact contact) {
+ for (Integer size : sizes) {
+ this.mXmppConnectionService.getBitmapCache().remove(
+ 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() + "_"
+ + contact.getJid() + "_" + String.valueOf(size);
+ }
+
+ public Bitmap get(ListItem item, int size) {
+ if (item instanceof Contact) {
+ return get((Contact) item, size);
+ } else if (item instanceof Bookmark) {
+ Bookmark bookmark = (Bookmark) item;
+ if (bookmark.getConversation() != null) {
+ return get(bookmark.getConversation(), size);
+ } else {
+ return get(bookmark.getDisplayName(), size);
+ }
+ } else {
+ return get(item.getDisplayName(), size);
+ }
+ }
+
+ public Bitmap get(Conversation conversation, int size) {
+ if (conversation.getMode() == Conversation.MODE_SINGLE) {
+ return get(conversation.getContact(), size);
+ } else {
+ return get(conversation.getMucOptions(), size);
+ }
+ }
+
+ public void clear(Conversation conversation) {
+ if (conversation.getMode() == Conversation.MODE_SINGLE) {
+ clear(conversation.getContact());
+ } else {
+ clear(conversation.getMucOptions());
+ }
+ }
+
+ public Bitmap get(MucOptions mucOptions, int size) {
+ final String KEY = key(mucOptions, size);
+ Bitmap bitmap = this.mXmppConnectionService.getBitmapCache().get(KEY);
+ if (bitmap != null) {
+ return bitmap;
+ }
+ Log.d(Config.LOGTAG, "no cache hit for " + KEY);
+ 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();
+ String letter = name.substring(0, 1);
+ int color = this.getColorForName(name);
+ drawTile(canvas, letter, color, 0, 0, size, size);
+ } else if (count == 1) {
+ drawTile(canvas, users.get(0), 0, 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", 0xFF202020, size / 2 + 1, size / 2 + 1,
+ size, size);
+ }
+ this.mXmppConnectionService.getBitmapCache().put(KEY, bitmap);
+ return bitmap;
+ }
+
+ public void clear(MucOptions options) {
+ for (Integer size : sizes) {
+ this.mXmppConnectionService.getBitmapCache().remove(
+ 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) {
+ final String KEY = key(account, size);
+ Bitmap avatar = mXmppConnectionService.getBitmapCache().get(KEY);
+ if (avatar != null) {
+ return avatar;
+ }
+ Log.d(Config.LOGTAG, "no cache hit for " + KEY);
+ avatar = mXmppConnectionService.getFileBackend().getAvatar(
+ account.getAvatar(), size);
+ if (avatar == null) {
+ avatar = get(account.getJid(), size);
+ }
+ mXmppConnectionService.getBitmapCache().put(KEY, avatar);
+ return avatar;
+ }
+
+ public void clear(Account account) {
+ for (Integer size : sizes) {
+ this.mXmppConnectionService.getBitmapCache().remove(
+ key(account, 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) {
+ final String KEY = key(name, size);
+ Bitmap bitmap = mXmppConnectionService.getBitmapCache().get(KEY);
+ if (bitmap != null) {
+ return bitmap;
+ }
+ Log.d(Config.LOGTAG, "no cache hit for " + KEY);
+ bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ String letter = name.substring(0, 1);
+ int color = this.getColorForName(name);
+ drawTile(canvas, letter, color, 0, 0, size, size);
+ mXmppConnectionService.getBitmapCache().put(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 void 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);
+ }
+
+ private void 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.getAvatar() != null) {
+ uri = mXmppConnectionService.getFileBackend().getAvatarUri(
+ contact.getAvatar());
+ } else if (contact.getProfilePhoto() != null) {
+ uri = Uri.parse(contact.getProfilePhoto());
+ }
+ if (uri != null) {
+ Bitmap bitmap = mXmppConnectionService.getFileBackend()
+ .cropCenter(uri, bottom - top, right - left);
+ if (bitmap != null) {
+ drawTile(canvas, bitmap, left, top, right, bottom);
+ } else {
+ String letter = user.getName().substring(0, 1);
+ int color = this.getColorForName(user.getName());
+ drawTile(canvas, letter, color, left, top, right, bottom);
+ }
+ } else {
+ String letter = user.getName().substring(0, 1);
+ int color = this.getColorForName(user.getName());
+ drawTile(canvas, letter, color, left, top, right, bottom);
+ }
+ } else {
+ String letter = user.getName().substring(0, 1);
+ int color = this.getColorForName(user.getName());
+ drawTile(canvas, letter, color, left, top, right, bottom);
+ }
+ }
+
+ private void 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);
+ }
+
+ private int getColorForName(String name) {
+ int holoColors[] = { 0xFFe91e63, 0xFF9c27b0, 0xFF673ab7, 0xFF3f51b5,
+ 0xFF5677fc, 0xFF03a9f4, 0xFF00bcd4, 0xFF009688, 0xFFff5722,
+ 0xFF795548, 0xFF607d8b };
+ return holoColors[(int) ((name.hashCode() & 0xffffffffl) % holoColors.length)];
+ }
+
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/services/EventReceiver.java b/conversations/src/main/java/eu/siacs/conversations/services/EventReceiver.java
new file mode 100644
index 00000000..dfbe9db7
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/services/EventReceiver.java
@@ -0,0 +1,24 @@
+package eu.siacs.conversations.services;
+
+import eu.siacs.conversations.persistance.DatabaseBackend;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+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/conversations/src/main/java/eu/siacs/conversations/services/NotificationService.java b/conversations/src/main/java/eu/siacs/conversations/services/NotificationService.java
new file mode 100644
index 00000000..00765deb
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/services/NotificationService.java
@@ -0,0 +1,237 @@
+package eu.siacs.conversations.services;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.PowerManager;
+import android.support.v4.app.NotificationCompat;
+import android.support.v4.app.TaskStackBuilder;
+import android.text.Html;
+import android.util.DisplayMetrics;
+
+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;
+
+public class NotificationService {
+
+ private XmppConnectionService mXmppConnectionService;
+
+ private LinkedHashMap<String, ArrayList<Message>> notifications = new LinkedHashMap<String, ArrayList<Message>>();
+
+ public int NOTIFICATION_ID = 0x2342;
+ private Conversation mOpenConversation;
+ private boolean mIsInForeground;
+
+ public NotificationService(XmppConnectionService service) {
+ this.mXmppConnectionService = service;
+ }
+
+ public void push(Message message) {
+ PowerManager pm = (PowerManager) mXmppConnectionService
+ .getSystemService(Context.POWER_SERVICE);
+ boolean isScreenOn = pm.isScreenOn();
+
+ if (this.mIsInForeground && isScreenOn
+ && this.mOpenConversation == message.getConversation()) {
+ return;
+ }
+ synchronized (notifications) {
+ String conversationUuid = message.getConversationUuid();
+ if (notifications.containsKey(conversationUuid)) {
+ notifications.get(conversationUuid).add(message);
+ } else {
+ ArrayList<Message> mList = new ArrayList<Message>();
+ mList.add(message);
+ notifications.put(conversationUuid, mList);
+ }
+ Account account = message.getConversation().getAccount();
+ updateNotification((!(this.mIsInForeground && this.mOpenConversation == null) || !isScreenOn)
+ && !account.inGracePeriod());
+ }
+
+ }
+
+ public void clear() {
+ synchronized (notifications) {
+ notifications.clear();
+ updateNotification(false);
+ }
+ }
+
+ public void clear(Conversation conversation) {
+ synchronized (notifications) {
+ notifications.remove(conversation.getUuid());
+ updateNotification(false);
+ }
+ }
+
+ private void updateNotification(boolean notify) {
+ NotificationManager notificationManager = (NotificationManager) mXmppConnectionService
+ .getSystemService(Context.NOTIFICATION_SERVICE);
+ SharedPreferences preferences = mXmppConnectionService.getPreferences();
+
+ String ringtone = preferences.getString("notification_ringtone", null);
+ boolean vibrate = preferences.getBoolean("vibrate_on_notification",
+ true);
+
+ if (notifications.size() == 0) {
+ notificationManager.cancel(NOTIFICATION_ID);
+ } else {
+ NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(
+ mXmppConnectionService);
+ mBuilder.setSmallIcon(R.drawable.ic_notification);
+ if (notifications.size() == 1) {
+ ArrayList<Message> messages = notifications.values().iterator()
+ .next();
+ if (messages.size() >= 1) {
+ Conversation conversation = messages.get(0)
+ .getConversation();
+ mBuilder.setLargeIcon(mXmppConnectionService
+ .getAvatarService().get(conversation, getPixel(64)));
+ mBuilder.setContentTitle(conversation.getName());
+ StringBuilder text = new StringBuilder();
+ for (int i = 0; i < messages.size(); ++i) {
+ text.append(messages.get(i).getReadableBody(
+ mXmppConnectionService));
+ if (i != messages.size() - 1) {
+ text.append("\n");
+ }
+ }
+ mBuilder.setStyle(new NotificationCompat.BigTextStyle()
+ .bigText(text.toString()));
+ mBuilder.setContentText(messages.get(0).getReadableBody(
+ mXmppConnectionService));
+ if (notify) {
+ mBuilder.setTicker(messages.get(messages.size() - 1)
+ .getReadableBody(mXmppConnectionService));
+ }
+ mBuilder.setContentIntent(createContentIntent(conversation
+ .getUuid()));
+ } else {
+ notificationManager.cancel(NOTIFICATION_ID);
+ return;
+ }
+ } else {
+ NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
+ style.setBigContentTitle(notifications.size()
+ + " "
+ + mXmppConnectionService
+ .getString(R.string.unread_conversations));
+ StringBuilder names = new StringBuilder();
+ Conversation conversation = null;
+ for (ArrayList<Message> messages : notifications.values()) {
+ if (messages.size() > 0) {
+ conversation = messages.get(0).getConversation();
+ String name = conversation.getName();
+ style.addLine(Html.fromHtml("<b>"
+ + name
+ + "</b> "
+ + messages.get(0).getReadableBody(
+ mXmppConnectionService)));
+ 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
+ .getUuid()));
+ }
+ }
+ if (notify) {
+ if (vibrate) {
+ int dat = 70;
+ long[] pattern = { 0, 3 * dat, dat, dat };
+ mBuilder.setVibrate(pattern);
+ }
+ if (ringtone != null) {
+ mBuilder.setSound(Uri.parse(ringtone));
+ }
+ }
+ mBuilder.setDeleteIntent(createDeleteIntent());
+ mBuilder.setLights(0xffffffff, 2000, 4000);
+ Notification notification = mBuilder.build();
+ notificationManager.notify(NOTIFICATION_ID, notification);
+ }
+ }
+
+ private PendingIntent createContentIntent(String conversationUuid) {
+ TaskStackBuilder stackBuilder = TaskStackBuilder
+ .create(mXmppConnectionService);
+ stackBuilder.addParentStack(ConversationActivity.class);
+
+ Intent viewConversationIntent = new Intent(mXmppConnectionService,
+ ConversationActivity.class);
+ viewConversationIntent.setAction(Intent.ACTION_VIEW);
+ viewConversationIntent.putExtra(ConversationActivity.CONVERSATION,
+ conversationUuid);
+ viewConversationIntent.setType(ConversationActivity.VIEW_CONVERSATION);
+
+ stackBuilder.addNextIntent(viewConversationIntent);
+
+ PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+ return resultPendingIntent;
+ }
+
+ private PendingIntent createDeleteIntent() {
+ Intent intent = new Intent(mXmppConnectionService,
+ XmppConnectionService.class);
+ intent.setAction("clear_notification");
+ return PendingIntent.getService(mXmppConnectionService, 0, intent, 0);
+ }
+
+ public static boolean wasHighlightedOrPrivate(Message message) {
+ String nick = message.getConversation().getMucOptions().getActualNick();
+ Pattern highlight = generateNickHighlightPattern(nick);
+ if (message.getBody() == null || nick == null) {
+ return false;
+ }
+ Matcher m = highlight.matcher(message.getBody());
+ return (m.find() || message.getType() == Message.TYPE_PRIVATE);
+ }
+
+ private static Pattern generateNickHighlightPattern(String nick) {
+ // We expect a word boundary, i.e. space or start of string, followed by
+ // the
+ // nick (matched in case-insensitive manner), followed by optional
+ // punctuation (for example "bob: i disagree" or "how are you alice?"),
+ // followed by another word boundary.
+ return Pattern.compile("\\b" + nick + "\\p{Punct}?\\b",
+ Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
+ }
+
+ public void setOpenConversation(Conversation conversation) {
+ this.mOpenConversation = conversation;
+ }
+
+ public void setIsInForeground(boolean foreground) {
+ this.mIsInForeground = foreground;
+ }
+
+ private int getPixel(int dp) {
+ DisplayMetrics metrics = mXmppConnectionService.getResources()
+ .getDisplayMetrics();
+ return ((int) (dp * metrics.density));
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/conversations/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
new file mode 100644
index 00000000..37e334eb
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
@@ -0,0 +1,1927 @@
+package eu.siacs.conversations.services;
+
+import java.security.SecureRandom;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Locale;
+import java.util.TimeZone;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import org.openintents.openpgp.util.OpenPgpApi;
+import org.openintents.openpgp.util.OpenPgpServiceConnection;
+
+import de.duenndns.ssl.MemorizingTrustManager;
+
+import net.java.otr4j.OtrException;
+import net.java.otr4j.session.Session;
+import net.java.otr4j.session.SessionStatus;
+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.Downloadable;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.MucOptions;
+import eu.siacs.conversations.entities.MucOptions.OnRenameListener;
+import eu.siacs.conversations.entities.Presences;
+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.OnPhoneContactsLoadedListener;
+import eu.siacs.conversations.utils.PRNGFixes;
+import eu.siacs.conversations.utils.PhoneHelper;
+import eu.siacs.conversations.utils.UIHelper;
+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.OnMessageAcknowledged;
+import eu.siacs.conversations.xmpp.OnStatusChanged;
+import eu.siacs.conversations.xmpp.XmppConnection;
+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.pep.Avatar;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
+import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
+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.SharedPreferences;
+import android.database.ContentObserver;
+import android.graphics.Bitmap;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.FileObserver;
+import android.os.IBinder;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.os.SystemClock;
+import android.preference.PreferenceManager;
+import android.provider.ContactsContract;
+import android.util.Log;
+import android.util.LruCache;
+
+public class XmppConnectionService extends Service {
+
+ public DatabaseBackend databaseBackend;
+ private FileBackend fileBackend = new FileBackend(this);
+
+ public long startDate;
+
+ private static String ACTION_MERGE_PHONE_CONTACTS = "merge_phone_contacts";
+ public static String ACTION_CLEAR_NOTIFICATION = "clear_notification";
+
+ private MemorizingTrustManager mMemorizingTrustManager;
+
+ private NotificationService mNotificationService = new NotificationService(
+ this);
+
+ private MessageParser mMessageParser = new MessageParser(this);
+ private PresenceParser mPresenceParser = new PresenceParser(this);
+ private IqParser mIqParser = new IqParser(this);
+ private MessageGenerator mMessageGenerator = new MessageGenerator(this);
+ private PresenceGenerator mPresenceGenerator = new PresenceGenerator(this);
+
+ private List<Account> accounts;
+ private CopyOnWriteArrayList<Conversation> conversations = null;
+ private JingleConnectionManager mJingleConnectionManager = new JingleConnectionManager(
+ this);
+ private HttpConnectionManager mHttpConnectionManager = new HttpConnectionManager(
+ this);
+ private AvatarService mAvatarService = new AvatarService(this);
+
+ private OnConversationUpdate mOnConversationUpdate = null;
+ private Integer convChangedListenerCount = 0;
+ private OnAccountUpdate mOnAccountUpdate = null;
+ private Integer accountChangedListenerCount = 0;
+ private OnRosterUpdate mOnRosterUpdate = null;
+ private Integer rosterChangedListenerCount = 0;
+ public OnContactStatusChanged onContactStatusChanged = new OnContactStatusChanged() {
+
+ @Override
+ public void onContactStatusChanged(Contact contact, boolean online) {
+ Conversation conversation = find(getConversations(), contact);
+ if (conversation != null) {
+ conversation.endOtrIfNeeded();
+ if (online && (contact.getPresences().size() == 1)) {
+ sendUnsendMessages(conversation);
+ }
+ }
+ }
+ };
+
+ private SecureRandom mRandom;
+
+ 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 FileObserver fileObserver = new FileObserver(
+ FileBackend.getConversationsDirectory()) {
+
+ @Override
+ public void onEvent(int event, String path) {
+ if (event == FileObserver.DELETE) {
+ markFileDeleted(path.split("\\.")[0]);
+ }
+ }
+ };
+
+ private final IBinder mBinder = new XmppConnectionBinder();
+ private OnStatusChanged statusListener = new OnStatusChanged() {
+
+ @Override
+ public void onStatusChanged(Account account) {
+ XmppConnection connection = account.getXmppConnection();
+ if (mOnAccountUpdate != null) {
+ mOnAccountUpdate.onAccountUpdate();
+ ;
+ }
+ if (account.getStatus() == Account.STATUS_ONLINE) {
+ for (Conversation conversation : account.pendingConferenceLeaves) {
+ leaveMuc(conversation);
+ }
+ for (Conversation conversation : account.pendingConferenceJoins) {
+ joinMuc(conversation);
+ }
+ mJingleConnectionManager.cancelInTransmission();
+ List<Conversation> conversations = getConversations();
+ for (int i = 0; i < conversations.size(); ++i) {
+ if (conversations.get(i).getAccount() == account) {
+ conversations.get(i).startOtrIfNeeded();
+ sendUnsendMessages(conversations.get(i));
+ }
+ }
+ if (connection != null && connection.getFeatures().csi()) {
+ if (checkListeners()) {
+ Log.d(Config.LOGTAG, account.getJid()
+ + " sending csi//inactive");
+ connection.sendInactive();
+ } else {
+ Log.d(Config.LOGTAG, account.getJid()
+ + " sending csi//active");
+ connection.sendActive();
+ }
+ }
+ syncDirtyContacts(account);
+ scheduleWakeupCall(Config.PING_MAX_INTERVAL, true);
+ } else if (account.getStatus() == Account.STATUS_OFFLINE) {
+ resetSendingToWaiting(account);
+ if (!account.isOptionSet(Account.OPTION_DISABLED)) {
+ int timeToReconnect = mRandom.nextInt(50) + 10;
+ scheduleWakeupCall(timeToReconnect, false);
+ }
+ } else if (account.getStatus() == Account.STATUS_REGISTRATION_SUCCESSFULL) {
+ databaseBackend.updateAccount(account);
+ reconnectAccount(account, true);
+ } else if ((account.getStatus() != Account.STATUS_CONNECTING)
+ && (account.getStatus() != Account.STATUS_NO_INTERNET)) {
+ if (connection != null) {
+ int next = connection.getTimeToNextAttempt();
+ Log.d(Config.LOGTAG, account.getJid()
+ + ": error connecting account. try again in "
+ + next + "s for the "
+ + (connection.getAttempt() + 1) + " time");
+ scheduleWakeupCall((int) (next * 1.2), false);
+ }
+ }
+ UIHelper.showErrorNotification(getApplicationContext(),
+ getAccounts());
+ }
+ };
+
+ private OnJinglePacketReceived jingleListener = new OnJinglePacketReceived() {
+
+ @Override
+ public void onJinglePacketReceived(Account account, JinglePacket packet) {
+ mJingleConnectionManager.deliverPacket(account, packet);
+ }
+ };
+
+ private OpenPgpServiceConnection pgpServiceConnection;
+ private PgpEngine mPgpEngine = null;
+ private Intent pingIntent;
+ private PendingIntent pendingPingIntent = null;
+ private WakeLock wakeLock;
+ private PowerManager pm;
+ private OnBindListener mOnBindListener = new OnBindListener() {
+
+ @Override
+ public void onBind(final Account account) {
+ account.getRoster().clearPresences();
+ account.clearPresences(); // self presences
+ account.pendingConferenceJoins.clear();
+ account.pendingConferenceLeaves.clear();
+ fetchRosterFromServer(account);
+ fetchBookmarks(account);
+ sendPresencePacket(account,
+ mPresenceGenerator.sendPresence(account));
+ connectMultiModeConversations(account);
+ updateConversationUi();
+ }
+ };
+
+ private OnMessageAcknowledged mOnMessageAcknowledgedListener = new OnMessageAcknowledged() {
+
+ @Override
+ public void onMessageAcknowledged(Account account, String uuid) {
+ for (Conversation conversation : getConversations()) {
+ if (conversation.getAccount() == account) {
+ for (Message message : conversation.getMessages()) {
+ if ((message.getStatus() == Message.STATUS_UNSEND || message
+ .getStatus() == Message.STATUS_WAITING)
+ && message.getUuid().equals(uuid)) {
+ markMessage(message, Message.STATUS_SEND);
+ return;
+ }
+ }
+ }
+ }
+ }
+ };
+ private LruCache<String, Bitmap> mBitmapCache;
+
+ public PgpEngine getPgpEngine() {
+ if (pgpServiceConnection.isBound()) {
+ if (this.mPgpEngine == null) {
+ this.mPgpEngine = new PgpEngine(new OpenPgpApi(
+ getApplicationContext(),
+ pgpServiceConnection.getService()), this);
+ }
+ return mPgpEngine;
+ } else {
+ return null;
+ }
+
+ }
+
+ public FileBackend getFileBackend() {
+ return this.fileBackend;
+ }
+
+ public AvatarService getAvatarService() {
+ return this.mAvatarService;
+ }
+
+ public Message attachImageToConversation(final Conversation conversation,
+ final Uri uri, final UiCallback<Message> callback) {
+ final Message message;
+ if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) {
+ message = new Message(conversation, "",
+ Message.ENCRYPTION_DECRYPTED);
+ } else {
+ message = new Message(conversation, "",
+ conversation.getNextEncryption(forceEncryption()));
+ }
+ message.setPresence(conversation.getNextPresence());
+ message.setType(Message.TYPE_IMAGE);
+ message.setStatus(Message.STATUS_OFFERED);
+ new Thread(new Runnable() {
+
+ @Override
+ public void run() {
+ try {
+ getFileBackend().copyImageToPrivateStorage(message, uri);
+ if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) {
+ getPgpEngine().encrypt(message, callback);
+ } else {
+ callback.success(message);
+ }
+ } catch (FileBackend.ImageCopyException e) {
+ callback.error(e.getResId(), message);
+ }
+ }
+ }).start();
+ return message;
+ }
+
+ public Conversation find(Bookmark bookmark) {
+ return find(bookmark.getAccount(), bookmark.getJid());
+ }
+
+ public Conversation find(Account account, String jid) {
+ return find(getConversations(), account, jid);
+ }
+
+ public class XmppConnectionBinder extends Binder {
+ public XmppConnectionService getService() {
+ return XmppConnectionService.this;
+ }
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (intent != null && intent.getAction() != null) {
+ if (intent.getAction().equals(ACTION_MERGE_PHONE_CONTACTS)) {
+ mergePhoneContactsWithRoster();
+ return START_STICKY;
+ } else if (intent.getAction().equals(Intent.ACTION_SHUTDOWN)) {
+ logoutAndSave();
+ return START_NOT_STICKY;
+ } else if (intent.getAction().equals(ACTION_CLEAR_NOTIFICATION)) {
+ mNotificationService.clear();
+ }
+ }
+ this.wakeLock.acquire();
+
+ for (Account account : accounts) {
+ if (!account.isOptionSet(Account.OPTION_DISABLED)) {
+ if (!hasInternetConnection()) {
+ account.setStatus(Account.STATUS_NO_INTERNET);
+ if (statusListener != null) {
+ statusListener.onStatusChanged(account);
+ }
+ } else {
+ if (account.getStatus() == Account.STATUS_NO_INTERNET) {
+ account.setStatus(Account.STATUS_OFFLINE);
+ if (statusListener != null) {
+ statusListener.onStatusChanged(account);
+ }
+ }
+ if (account.getStatus() == Account.STATUS_ONLINE) {
+ long lastReceived = account.getXmppConnection()
+ .getLastPacketReceived();
+ long lastSent = account.getXmppConnection()
+ .getLastPingSent();
+ if (lastSent - lastReceived >= Config.PING_TIMEOUT * 1000) {
+ Log.d(Config.LOGTAG, account.getJid()
+ + ": ping timeout");
+ this.reconnectAccount(account, true);
+ } else if (SystemClock.elapsedRealtime() - lastReceived >= Config.PING_MIN_INTERVAL * 1000) {
+ account.getXmppConnection().sendPing();
+ this.scheduleWakeupCall(2, false);
+ }
+ } else if (account.getStatus() == Account.STATUS_OFFLINE) {
+ if (account.getXmppConnection() == null) {
+ account.setXmppConnection(this
+ .createConnection(account));
+ }
+ new Thread(account.getXmppConnection()).start();
+ } else if ((account.getStatus() == Account.STATUS_CONNECTING)
+ && ((SystemClock.elapsedRealtime() - account
+ .getXmppConnection().getLastConnect()) / 1000 >= Config.CONNECT_TIMEOUT)) {
+ Log.d(Config.LOGTAG, account.getJid()
+ + ": time out during connect reconnecting");
+ reconnectAccount(account, true);
+ } else {
+ if (account.getXmppConnection().getTimeToNextAttempt() <= 0) {
+ reconnectAccount(account, true);
+ }
+ }
+ // in any case. reschedule wakup call
+ this.scheduleWakeupCall(Config.PING_MAX_INTERVAL, true);
+ }
+ if (mOnAccountUpdate != null) {
+ mOnAccountUpdate.onAccountUpdate();
+ }
+ }
+ }
+ if (wakeLock.isHeld()) {
+ try {
+ wakeLock.release();
+ } catch (RuntimeException re) {
+ }
+ }
+ return START_STICKY;
+ }
+
+ public boolean hasInternetConnection() {
+ ConnectivityManager cm = (ConnectivityManager) getApplicationContext()
+ .getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
+ return activeNetwork != null && activeNetwork.isConnected();
+ }
+
+ @SuppressLint("TrulyRandom")
+ @Override
+ public void onCreate() {
+ ExceptionHelper.init(getApplicationContext());
+ PRNGFixes.apply();
+ this.mRandom = new SecureRandom();
+ this.mMemorizingTrustManager = new MemorizingTrustManager(
+ getApplicationContext());
+
+ int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
+ int cacheSize = maxMemory / 8;
+ this.mBitmapCache = new LruCache<String, Bitmap>(cacheSize) {
+ @Override
+ protected int sizeOf(String key, Bitmap bitmap) {
+ return bitmap.getByteCount() / 1024;
+ }
+ };
+
+ this.databaseBackend = DatabaseBackend
+ .getInstance(getApplicationContext());
+ this.accounts = databaseBackend.getAccounts();
+
+ for (Account account : this.accounts) {
+ this.databaseBackend.readRoster(account.getRoster());
+ }
+ this.mergePhoneContactsWithRoster();
+ this.getConversations();
+
+ getContentResolver().registerContentObserver(
+ ContactsContract.Contacts.CONTENT_URI, true, contactObserver);
+ this.fileObserver.startWatching();
+ this.pgpServiceConnection = new OpenPgpServiceConnection(
+ getApplicationContext(), "org.sufficientlysecure.keychain");
+ this.pgpServiceConnection.bindToService();
+
+ this.pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
+ this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
+ "XmppConnectionService");
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ this.logoutAndSave();
+ }
+
+ @Override
+ public void onTaskRemoved(Intent rootIntent) {
+ super.onTaskRemoved(rootIntent);
+ this.logoutAndSave();
+ }
+
+ private void logoutAndSave() {
+ for (Account account : accounts) {
+ databaseBackend.writeRoster(account.getRoster());
+ if (account.getXmppConnection() != null) {
+ disconnect(account, false);
+ }
+ }
+ Context context = getApplicationContext();
+ AlarmManager alarmManager = (AlarmManager) context
+ .getSystemService(Context.ALARM_SERVICE);
+ Intent intent = new Intent(context, EventReceiver.class);
+ alarmManager.cancel(PendingIntent.getBroadcast(context, 0, intent, 0));
+ Log.d(Config.LOGTAG, "good bye");
+ stopSelf();
+ }
+
+ protected void scheduleWakeupCall(int seconds, boolean ping) {
+ long timeToWake = SystemClock.elapsedRealtime() + seconds * 1000;
+ Context context = getApplicationContext();
+ AlarmManager alarmManager = (AlarmManager) context
+ .getSystemService(Context.ALARM_SERVICE);
+
+ if (ping) {
+ if (this.pingIntent == null) {
+ this.pingIntent = new Intent(context, EventReceiver.class);
+ this.pingIntent.setAction("ping");
+ this.pingIntent.putExtra("time", timeToWake);
+ this.pendingPingIntent = PendingIntent.getBroadcast(context, 0,
+ this.pingIntent, 0);
+ alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
+ timeToWake, pendingPingIntent);
+ } else {
+ long scheduledTime = this.pingIntent.getLongExtra("time", 0);
+ if (scheduledTime < SystemClock.elapsedRealtime()
+ || (scheduledTime > timeToWake)) {
+ this.pingIntent.putExtra("time", timeToWake);
+ alarmManager.cancel(this.pendingPingIntent);
+ this.pendingPingIntent = PendingIntent.getBroadcast(
+ context, 0, this.pingIntent, 0);
+ alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
+ timeToWake, pendingPingIntent);
+ }
+ }
+ } else {
+ Intent intent = new Intent(context, EventReceiver.class);
+ intent.setAction("ping_check");
+ PendingIntent alarmIntent = PendingIntent.getBroadcast(context, 0,
+ intent, 0);
+ alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake,
+ alarmIntent);
+ }
+
+ }
+
+ public XmppConnection createConnection(Account account) {
+ SharedPreferences sharedPref = getPreferences();
+ account.setResource(sharedPref.getString("resource", "mobile")
+ .toLowerCase(Locale.getDefault()));
+ 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);
+ return connection;
+ }
+
+ public void sendMessage(Message message) {
+ Account account = message.getConversation().getAccount();
+ account.deactivateGracePeriod();
+ Conversation conv = message.getConversation();
+ MessagePacket packet = null;
+ boolean saveInDb = true;
+ boolean send = false;
+ if (account.getStatus() == Account.STATUS_ONLINE
+ && account.getXmppConnection() != null) {
+ if (message.getType() == Message.TYPE_IMAGE) {
+ if (message.getPresence() != null) {
+ if (message.getEncryption() == Message.ENCRYPTION_OTR) {
+ if (!conv.hasValidOtrSession()
+ && (message.getPresence() != null)) {
+ conv.startOtrSession(this, message.getPresence(),
+ true);
+ message.setStatus(Message.STATUS_WAITING);
+ } else if (conv.hasValidOtrSession()
+ && conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) {
+ mJingleConnectionManager
+ .createNewConnection(message);
+ } else if (message.getPresence() == null) {
+ message.setStatus(Message.STATUS_WAITING);
+ }
+ } else {
+ mJingleConnectionManager.createNewConnection(message);
+ }
+ } else {
+ message.setStatus(Message.STATUS_WAITING);
+ }
+ } else {
+ if (message.getEncryption() == Message.ENCRYPTION_OTR) {
+ if (!conv.hasValidOtrSession()
+ && (message.getPresence() != null)) {
+ conv.startOtrSession(this, message.getPresence(), true);
+ message.setStatus(Message.STATUS_WAITING);
+ } else if (conv.hasValidOtrSession()
+ && conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) {
+ message.setPresence(conv.getOtrSession().getSessionID()
+ .getUserID());
+ packet = mMessageGenerator.generateOtrChat(message);
+ send = true;
+
+ } else if (message.getPresence() == null) {
+ message.setStatus(Message.STATUS_WAITING);
+ }
+ } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
+ message.getConversation().endOtrIfNeeded();
+ failWaitingOtrMessages(message.getConversation());
+ packet = mMessageGenerator.generatePgpChat(message);
+ send = true;
+ } else {
+ message.getConversation().endOtrIfNeeded();
+ failWaitingOtrMessages(message.getConversation());
+ packet = mMessageGenerator.generateChat(message);
+ send = true;
+ }
+ }
+ if (!account.getXmppConnection().getFeatures().sm()
+ && conv.getMode() != Conversation.MODE_MULTI) {
+ message.setStatus(Message.STATUS_SEND);
+ }
+ } else {
+ message.setStatus(Message.STATUS_WAITING);
+ if (message.getType() == Message.TYPE_TEXT) {
+ if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
+ 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);
+ } else if (message.getEncryption() == Message.ENCRYPTION_OTR) {
+ if (conv.hasValidOtrSession()) {
+ message.setPresence(conv.getOtrSession().getSessionID()
+ .getUserID());
+ } else if (!conv.hasValidOtrSession()
+ && message.getPresence() != null) {
+ conv.startOtrSession(this, message.getPresence(), false);
+ }
+ }
+ }
+
+ }
+ conv.add(message);
+ if (saveInDb) {
+ if (message.getEncryption() == Message.ENCRYPTION_NONE
+ || saveEncryptedMessages()) {
+ databaseBackend.createMessage(message);
+ }
+ }
+ if ((send) && (packet != null)) {
+ sendMessagePacket(account, packet);
+ }
+ updateConversationUi();
+ }
+
+ private void sendUnsendMessages(Conversation conversation) {
+ for (int i = 0; i < conversation.getMessages().size(); ++i) {
+ int status = conversation.getMessages().get(i).getStatus();
+ if (status == Message.STATUS_WAITING) {
+ resendMessage(conversation.getMessages().get(i));
+ }
+ }
+ }
+
+ private void resendMessage(Message message) {
+ Account account = message.getConversation().getAccount();
+ MessagePacket packet = null;
+ if (message.getEncryption() == Message.ENCRYPTION_OTR) {
+ Presences presences = message.getConversation().getContact()
+ .getPresences();
+ if (!message.getConversation().hasValidOtrSession()) {
+ if ((message.getPresence() != null)
+ && (presences.has(message.getPresence()))) {
+ message.getConversation().startOtrSession(this,
+ message.getPresence(), true);
+ } else {
+ if (presences.size() == 1) {
+ String presence = presences.asStringArray()[0];
+ message.getConversation().startOtrSession(this,
+ presence, true);
+ }
+ }
+ } else {
+ if (message.getConversation().getOtrSession()
+ .getSessionStatus() == SessionStatus.ENCRYPTED) {
+ if (message.getType() == Message.TYPE_TEXT) {
+ packet = mMessageGenerator.generateOtrChat(message,
+ true);
+ } else if (message.getType() == Message.TYPE_IMAGE) {
+ mJingleConnectionManager.createNewConnection(message);
+ }
+ }
+ }
+ } else if (message.getType() == Message.TYPE_TEXT) {
+ if (message.getEncryption() == Message.ENCRYPTION_NONE) {
+ packet = mMessageGenerator.generateChat(message, true);
+ } else if ((message.getEncryption() == Message.ENCRYPTION_DECRYPTED)
+ || (message.getEncryption() == Message.ENCRYPTION_PGP)) {
+ packet = mMessageGenerator.generatePgpChat(message, true);
+ }
+ } else if (message.getType() == Message.TYPE_IMAGE) {
+ Presences presences = message.getConversation().getContact()
+ .getPresences();
+ if ((message.getPresence() != null)
+ && (presences.has(message.getPresence()))) {
+ markMessage(message, Message.STATUS_OFFERED);
+ mJingleConnectionManager.createNewConnection(message);
+ } else {
+ if (presences.size() == 1) {
+ String presence = presences.asStringArray()[0];
+ message.setPresence(presence);
+ markMessage(message, Message.STATUS_OFFERED);
+ mJingleConnectionManager.createNewConnection(message);
+ }
+ }
+ }
+ if (packet != null) {
+ if (!account.getXmppConnection().getFeatures().sm()
+ && message.getConversation().getMode() != Conversation.MODE_MULTI) {
+ markMessage(message, Message.STATUS_SEND);
+ } else {
+ markMessage(message, Message.STATUS_UNSEND);
+ }
+ sendMessagePacket(account, packet);
+ }
+ }
+
+ public void fetchRosterFromServer(Account account) {
+ IqPacket iqPacket = new IqPacket(IqPacket.TYPE_GET);
+ if (!"".equals(account.getRosterVersion())) {
+ Log.d(Config.LOGTAG, account.getJid()
+ + ": fetching roster version " + account.getRosterVersion());
+ } else {
+ Log.d(Config.LOGTAG, account.getJid() + ": fetching roster");
+ }
+ iqPacket.query("jabber:iq:roster").setAttribute("ver",
+ account.getRosterVersion());
+ account.getXmppConnection().sendIqPacket(iqPacket,
+ new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(final Account account,
+ IqPacket packet) {
+ Element query = packet.findChild("query");
+ if (query != null) {
+ account.getRoster().markAllAsNotInRoster();
+ mIqParser.rosterItems(account, query);
+ }
+ }
+ });
+ }
+
+ public void fetchBookmarks(Account account) {
+ IqPacket iqPacket = new IqPacket(IqPacket.TYPE_GET);
+ Element query = iqPacket.query("jabber:iq:private");
+ query.addChild("storage", "storage:bookmarks");
+ OnIqPacketReceived callback = new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ Element query = packet.query();
+ List<Bookmark> bookmarks = new CopyOnWriteArrayList<Bookmark>();
+ Element storage = query.findChild("storage",
+ "storage:bookmarks");
+ if (storage != null) {
+ for (Element item : storage.getChildren()) {
+ if (item.getName().equals("conference")) {
+ Bookmark bookmark = Bookmark.parse(item, account);
+ bookmarks.add(bookmark);
+ Conversation conversation = find(bookmark);
+ if (conversation != null) {
+ conversation.setBookmark(bookmark);
+ } else {
+ if (bookmark.autojoin()) {
+ conversation = findOrCreateConversation(
+ account, bookmark.getJid(), true);
+ conversation.setBookmark(bookmark);
+ joinMuc(conversation);
+ }
+ }
+ }
+ }
+ }
+ account.setBookmarks(bookmarks);
+ }
+ };
+ sendIqPacket(account, iqPacket, callback);
+
+ }
+
+ public void pushBookmarks(Account account) {
+ 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, null);
+ }
+
+ private void mergePhoneContactsWithRoster() {
+ PhoneHelper.loadPhoneContacts(getApplicationContext(),
+ new OnPhoneContactsLoadedListener() {
+ @Override
+ public void onPhoneContactsLoaded(List<Bundle> phoneContacts) {
+ for (Account account : accounts) {
+ account.getRoster().clearSystemAccounts();
+ }
+ for (Bundle phoneContact : phoneContacts) {
+ for (Account account : accounts) {
+ String jid = phoneContact.getString("jid");
+ Contact contact = account.getRoster()
+ .getContact(jid);
+ String systemAccount = phoneContact
+ .getInt("phoneid")
+ + "#"
+ + phoneContact.getString("lookup");
+ contact.setSystemAccount(systemAccount);
+ contact.setPhotoUri(phoneContact
+ .getString("photouri"));
+ contact.setSystemName(phoneContact
+ .getString("displayname"));
+ getAvatarService().clear(contact);
+ }
+ }
+ }
+ });
+ }
+
+ public List<Conversation> getConversations() {
+ if (this.conversations == null) {
+ Hashtable<String, Account> accountLookupTable = new Hashtable<String, Account>();
+ for (Account account : this.accounts) {
+ accountLookupTable.put(account.getUuid(), account);
+ }
+ this.conversations = databaseBackend
+ .getConversations(Conversation.STATUS_AVAILABLE);
+ for (Conversation conv : this.conversations) {
+ Account account = accountLookupTable.get(conv.getAccountUuid());
+ conv.setAccount(account);
+ conv.setMessages(databaseBackend.getMessages(conv, 50));
+ checkDeletedFiles(conv);
+ }
+ }
+ return this.conversations;
+ }
+
+ private void checkDeletedFiles(Conversation conversation) {
+ for (Message message : conversation.getMessages()) {
+ if (message.getType() == Message.TYPE_IMAGE
+ && message.getEncryption() != Message.ENCRYPTION_PGP) {
+ if (!getFileBackend().isFileAvailable(message)) {
+ message.setDownloadable(new DeletedDownloadable());
+ }
+ }
+ }
+ }
+
+ private void markFileDeleted(String uuid) {
+ for (Conversation conversation : getConversations()) {
+ for (Message message : conversation.getMessages()) {
+ if (message.getType() == Message.TYPE_IMAGE
+ && message.getEncryption() != Message.ENCRYPTION_PGP
+ && message.getUuid().equals(uuid)) {
+ if (!getFileBackend().isFileAvailable(message)) {
+ message.setDownloadable(new DeletedDownloadable());
+ updateConversationUi();
+ }
+ return;
+ }
+ }
+ }
+ }
+
+ public void populateWithOrderedConversations(List<Conversation> list) {
+ populateWithOrderedConversations(list, true);
+ }
+
+ public void populateWithOrderedConversations(List<Conversation> list,
+ boolean includeConferences) {
+ list.clear();
+ if (includeConferences) {
+ list.addAll(getConversations());
+ } else {
+ for (Conversation conversation : getConversations()) {
+ if (conversation.getMode() == Conversation.MODE_SINGLE) {
+ 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 int loadMoreMessages(Conversation conversation, long timestamp) {
+ List<Message> messages = databaseBackend.getMessages(conversation, 50,
+ timestamp);
+ for (Message message : messages) {
+ message.setConversation(conversation);
+ }
+ conversation.addAll(0, messages);
+ return messages.size();
+ }
+
+ public List<Account> getAccounts() {
+ return this.accounts;
+ }
+
+ public Conversation find(List<Conversation> haystack, Contact contact) {
+ for (Conversation conversation : haystack) {
+ if (conversation.getContact() == contact) {
+ return conversation;
+ }
+ }
+ return null;
+ }
+
+ public Conversation find(List<Conversation> haystack, Account account,
+ String jid) {
+ for (Conversation conversation : haystack) {
+ if ((account == null || conversation.getAccount().equals(account))
+ && (conversation.getContactJid().split("/", 2)[0]
+ .equals(jid))) {
+ return conversation;
+ }
+ }
+ return null;
+ }
+
+ public Conversation findOrCreateConversation(Account account, String jid,
+ boolean muc) {
+ 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);
+ } else {
+ conversation.setMode(Conversation.MODE_SINGLE);
+ }
+ conversation.setMessages(databaseBackend.getMessages(conversation,
+ 50));
+ this.databaseBackend.updateConversation(conversation);
+ } else {
+ String conversationName;
+ Contact contact = account.getRoster().getContact(jid);
+ if (contact != null) {
+ conversationName = contact.getDisplayName();
+ } else {
+ conversationName = jid.split("@")[0];
+ }
+ if (muc) {
+ conversation = new Conversation(conversationName, account, jid,
+ Conversation.MODE_MULTI);
+ } else {
+ conversation = new Conversation(conversationName, account, jid,
+ Conversation.MODE_SINGLE);
+ }
+ this.databaseBackend.createConversation(conversation);
+ }
+ this.conversations.add(conversation);
+ updateConversationUi();
+ return conversation;
+ }
+
+ public void archiveConversation(Conversation conversation) {
+ if (conversation.getMode() == Conversation.MODE_MULTI) {
+ if (conversation.getAccount().getStatus() == Account.STATUS_ONLINE) {
+ Bookmark bookmark = conversation.getBookmark();
+ if (bookmark != null && bookmark.autojoin()) {
+ bookmark.setAutojoin(false);
+ pushBookmarks(bookmark.getAccount());
+ }
+ }
+ leaveMuc(conversation);
+ } else {
+ conversation.endOtrIfNeeded();
+ }
+ this.databaseBackend.updateConversation(conversation);
+ this.conversations.remove(conversation);
+ updateConversationUi();
+ }
+
+ public void clearConversationHistory(Conversation conversation) {
+ this.databaseBackend.deleteMessagesInConversation(conversation);
+ this.fileBackend.removeFiles(conversation);
+ conversation.getMessages().clear();
+ updateConversationUi();
+ }
+
+ public int getConversationCount() {
+ return this.databaseBackend.getConversationCount();
+ }
+
+ public void createAccount(Account account) {
+ databaseBackend.createAccount(account);
+ this.accounts.add(account);
+ this.reconnectAccount(account, false);
+ updateAccountUi();
+ }
+
+ public void updateAccount(Account account) {
+ this.statusListener.onStatusChanged(account);
+ databaseBackend.updateAccount(account);
+ reconnectAccount(account, false);
+ updateAccountUi();
+ UIHelper.showErrorNotification(getApplicationContext(), getAccounts());
+ }
+
+ public void deleteAccount(Account account) {
+ for (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);
+ }
+ databaseBackend.deleteAccount(account);
+ this.accounts.remove(account);
+ updateAccountUi();
+ UIHelper.showErrorNotification(getApplicationContext(), getAccounts());
+ }
+
+ public void setOnConversationListChangedListener(
+ OnConversationUpdate listener) {
+ if (!isScreenOn()) {
+ Log.d(Config.LOGTAG,
+ "ignoring setOnConversationListChangedListener");
+ return;
+ }
+ synchronized (this.convChangedListenerCount) {
+ if (checkListeners()) {
+ switchToForeground();
+ }
+ this.mOnConversationUpdate = listener;
+ this.mNotificationService.setIsInForeground(true);
+ this.convChangedListenerCount++;
+ }
+ }
+
+ public void removeOnConversationListChangedListener() {
+ synchronized (this.convChangedListenerCount) {
+ this.convChangedListenerCount--;
+ if (this.convChangedListenerCount <= 0) {
+ this.convChangedListenerCount = 0;
+ this.mOnConversationUpdate = null;
+ this.mNotificationService.setIsInForeground(false);
+ if (checkListeners()) {
+ switchToBackground();
+ }
+ }
+ }
+ }
+
+ public void setOnAccountListChangedListener(OnAccountUpdate listener) {
+ if (!isScreenOn()) {
+ Log.d(Config.LOGTAG, "ignoring setOnAccountListChangedListener");
+ return;
+ }
+ synchronized (this.accountChangedListenerCount) {
+ if (checkListeners()) {
+ switchToForeground();
+ }
+ this.mOnAccountUpdate = listener;
+ this.accountChangedListenerCount++;
+ }
+ }
+
+ public void removeOnAccountListChangedListener() {
+ synchronized (this.accountChangedListenerCount) {
+ this.accountChangedListenerCount--;
+ if (this.accountChangedListenerCount <= 0) {
+ this.mOnAccountUpdate = null;
+ this.accountChangedListenerCount = 0;
+ if (checkListeners()) {
+ switchToBackground();
+ }
+ }
+ }
+ }
+
+ public void setOnRosterUpdateListener(OnRosterUpdate listener) {
+ if (!isScreenOn()) {
+ Log.d(Config.LOGTAG, "ignoring setOnRosterUpdateListener");
+ return;
+ }
+ synchronized (this.rosterChangedListenerCount) {
+ if (checkListeners()) {
+ switchToForeground();
+ }
+ this.mOnRosterUpdate = listener;
+ this.rosterChangedListenerCount++;
+ }
+ }
+
+ public void removeOnRosterUpdateListener() {
+ synchronized (this.rosterChangedListenerCount) {
+ this.rosterChangedListenerCount--;
+ if (this.rosterChangedListenerCount <= 0) {
+ this.rosterChangedListenerCount = 0;
+ this.mOnRosterUpdate = null;
+ if (checkListeners()) {
+ switchToBackground();
+ }
+ }
+ }
+ }
+
+ private boolean checkListeners() {
+ return (this.mOnAccountUpdate == null
+ && this.mOnConversationUpdate == null && this.mOnRosterUpdate == null);
+ }
+
+ private void switchToForeground() {
+ for (Account account : getAccounts()) {
+ if (account.getStatus() == Account.STATUS_ONLINE) {
+ XmppConnection connection = account.getXmppConnection();
+ if (connection != null && connection.getFeatures().csi()) {
+ connection.sendActive();
+ }
+ }
+ }
+ Log.d(Config.LOGTAG, "app switched into foreground");
+ }
+
+ private void switchToBackground() {
+ for (Account account : getAccounts()) {
+ if (account.getStatus() == Account.STATUS_ONLINE) {
+ XmppConnection connection = account.getXmppConnection();
+ if (connection != null && connection.getFeatures().csi()) {
+ connection.sendInactive();
+ }
+ }
+ }
+ this.mNotificationService.setIsInForeground(false);
+ Log.d(Config.LOGTAG, "app switched into background");
+ }
+
+ private boolean isScreenOn() {
+ PowerManager pm = (PowerManager) this
+ .getSystemService(Context.POWER_SERVICE);
+ return pm.isScreenOn();
+ }
+
+ public void connectMultiModeConversations(Account account) {
+ List<Conversation> conversations = getConversations();
+ for (int i = 0; i < conversations.size(); i++) {
+ Conversation conversation = conversations.get(i);
+ if ((conversation.getMode() == Conversation.MODE_MULTI)
+ && (conversation.getAccount() == account)) {
+ joinMuc(conversation);
+ }
+ }
+ }
+
+ public void joinMuc(Conversation conversation) {
+ Account account = conversation.getAccount();
+ account.pendingConferenceJoins.remove(conversation);
+ account.pendingConferenceLeaves.remove(conversation);
+ if (account.getStatus() == Account.STATUS_ONLINE) {
+ Log.d(Config.LOGTAG,
+ "joining conversation " + conversation.getContactJid());
+ String nick = conversation.getMucOptions().getProposedNick();
+ conversation.getMucOptions().setJoinNick(nick);
+ PresencePacket packet = new PresencePacket();
+ String joinJid = conversation.getMucOptions().getJoinJid();
+ packet.setAttribute("to", conversation.getMucOptions().getJoinJid());
+ Element x = new Element("x");
+ x.setAttribute("xmlns", "http://jabber.org/protocol/muc");
+ if (conversation.getMucOptions().getPassword() != null) {
+ Element password = x.addChild("password");
+ password.setContent(conversation.getMucOptions().getPassword());
+ }
+ String sig = account.getPgpSignature();
+ if (sig != null) {
+ packet.addChild("status").setContent("online");
+ packet.addChild("x", "jabber:x:signed").setContent(sig);
+ }
+ if (conversation.getMessages().size() != 0) {
+ final SimpleDateFormat mDateFormat = new SimpleDateFormat(
+ "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
+ mDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+ Date date = new Date(conversation.getLatestMessage()
+ .getTimeSent() + 1000);
+ x.addChild("history").setAttribute("since",
+ mDateFormat.format(date));
+ }
+ packet.addChild(x);
+ sendPresencePacket(account, packet);
+ if (!joinJid.equals(conversation.getContactJid())) {
+ conversation.setContactJid(joinJid);
+ databaseBackend.updateConversation(conversation);
+ }
+ } else {
+ account.pendingConferenceJoins.add(conversation);
+ }
+ }
+
+ private OnRenameListener renameListener = null;
+ private IqGenerator mIqGenerator = new IqGenerator(this);
+
+ public void setOnRenameListener(OnRenameListener listener) {
+ this.renameListener = listener;
+ }
+
+ public void providePasswordForMuc(Conversation conversation, String password) {
+ if (conversation.getMode() == Conversation.MODE_MULTI) {
+ conversation.getMucOptions().setPassword(password);
+ if (conversation.getBookmark() != null) {
+ conversation.getBookmark().setAutojoin(true);
+ pushBookmarks(conversation.getAccount());
+ }
+ databaseBackend.updateConversation(conversation);
+ joinMuc(conversation);
+ }
+ }
+
+ public void renameInMuc(final Conversation conversation, final String nick) {
+ final MucOptions options = conversation.getMucOptions();
+ options.setJoinNick(nick);
+ if (options.online()) {
+ Account account = conversation.getAccount();
+ options.setOnRenameListener(new OnRenameListener() {
+
+ @Override
+ public void onRename(boolean success) {
+ if (renameListener != null) {
+ renameListener.onRename(success);
+ }
+ if (success) {
+ conversation.setContactJid(conversation.getMucOptions()
+ .getJoinJid());
+ databaseBackend.updateConversation(conversation);
+ Bookmark bookmark = conversation.getBookmark();
+ if (bookmark != null) {
+ bookmark.setNick(nick);
+ pushBookmarks(bookmark.getAccount());
+ }
+ }
+ }
+ });
+ options.flagAboutToRename();
+ PresencePacket packet = new PresencePacket();
+ packet.setAttribute("to", options.getJoinJid());
+ packet.setAttribute("from", conversation.getAccount().getFullJid());
+
+ 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(options.getJoinJid());
+ databaseBackend.updateConversation(conversation);
+ if (conversation.getAccount().getStatus() == Account.STATUS_ONLINE) {
+ Bookmark bookmark = conversation.getBookmark();
+ if (bookmark != null) {
+ bookmark.setNick(nick);
+ pushBookmarks(bookmark.getAccount());
+ }
+ joinMuc(conversation);
+ }
+ }
+ }
+
+ public void leaveMuc(Conversation conversation) {
+ Account account = conversation.getAccount();
+ account.pendingConferenceJoins.remove(conversation);
+ account.pendingConferenceLeaves.remove(conversation);
+ if (account.getStatus() == Account.STATUS_ONLINE) {
+ PresencePacket packet = new PresencePacket();
+ packet.setAttribute("to", conversation.getMucOptions().getJoinJid());
+ packet.setAttribute("from", conversation.getAccount().getFullJid());
+ packet.setAttribute("type", "unavailable");
+ sendPresencePacket(conversation.getAccount(), packet);
+ conversation.getMucOptions().setOffline();
+ conversation.deregisterWithBookmark();
+ Log.d(Config.LOGTAG, conversation.getAccount().getJid()
+ + ": leaving muc " + conversation.getContactJid());
+ } else {
+ account.pendingConferenceLeaves.add(conversation);
+ }
+ }
+
+ public void disconnect(Account account, boolean force) {
+ if ((account.getStatus() == Account.STATUS_ONLINE)
+ || (account.getStatus() == Account.STATUS_DISABLED)) {
+ if (!force) {
+ List<Conversation> conversations = getConversations();
+ for (int i = 0; i < conversations.size(); i++) {
+ Conversation conversation = conversations.get(i);
+ if (conversation.getAccount() == account) {
+ if (conversation.getMode() == Conversation.MODE_MULTI) {
+ leaveMuc(conversation);
+ } else {
+ if (conversation.endOtrIfNeeded()) {
+ Log.d(Config.LOGTAG, account.getJid()
+ + ": ended otr session with "
+ + conversation.getContactJid());
+ }
+ }
+ }
+ }
+ }
+ 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) {
+ SharedPreferences sharedPref = getPreferences();
+ boolean autoGrant = sharedPref.getBoolean("grant_new_contacts", true);
+ if (autoGrant) {
+ contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
+ contact.setOption(Contact.Options.ASKING);
+ }
+ pushContactToServer(contact);
+ }
+
+ public void onOtrSessionEstablished(Conversation conversation) {
+ Account account = conversation.getAccount();
+ List<Message> messages = conversation.getMessages();
+ Session otrSession = conversation.getOtrSession();
+ Log.d(Config.LOGTAG,
+ account.getJid() + " otr session established with "
+ + conversation.getContactJid() + "/"
+ + otrSession.getSessionID().getUserID());
+ for (int i = 0; i < messages.size(); ++i) {
+ Message msg = messages.get(i);
+ if ((msg.getStatus() == Message.STATUS_UNSEND || msg.getStatus() == Message.STATUS_WAITING)
+ && (msg.getEncryption() == Message.ENCRYPTION_OTR)) {
+ msg.setPresence(otrSession.getSessionID().getUserID());
+ if (msg.getType() == Message.TYPE_TEXT) {
+ MessagePacket outPacket = mMessageGenerator
+ .generateOtrChat(msg, true);
+ if (outPacket != null) {
+ msg.setStatus(Message.STATUS_SEND);
+ databaseBackend.updateMessage(msg);
+ sendMessagePacket(account, outPacket);
+ }
+ } else if (msg.getType() == Message.TYPE_IMAGE) {
+ mJingleConnectionManager.createNewConnection(msg);
+ }
+ }
+ }
+ updateConversationUi();
+ }
+
+ public boolean renewSymmetricKey(Conversation conversation) {
+ Account account = conversation.getAccount();
+ byte[] symmetricKey = new byte[32];
+ this.mRandom.nextBytes(symmetricKey);
+ Session otrSession = conversation.getOtrSession();
+ if (otrSession != null) {
+ MessagePacket packet = new MessagePacket();
+ packet.setType(MessagePacket.TYPE_CHAT);
+ packet.setFrom(account.getFullJid());
+ packet.addChild("private", "urn:xmpp:carbons:2");
+ packet.addChild("no-copy", "urn:xmpp:hints");
+ packet.setTo(otrSession.getSessionID().getAccountID() + "/"
+ + otrSession.getSessionID().getUserID());
+ try {
+ packet.setBody(otrSession
+ .transformSending(CryptoHelper.FILETRANSFER
+ + CryptoHelper.bytesToHex(symmetricKey)));
+ sendMessagePacket(account, packet);
+ conversation.setSymmetricKey(symmetricKey);
+ return true;
+ } catch (OtrException e) {
+ return false;
+ }
+ }
+ return false;
+ }
+
+ public void pushContactToServer(Contact contact) {
+ contact.resetOption(Contact.Options.DIRTY_DELETE);
+ contact.setOption(Contact.Options.DIRTY_PUSH);
+ Account account = contact.getAccount();
+ if (account.getStatus() == Account.STATUS_ONLINE) {
+ boolean ask = contact.getOption(Contact.Options.ASKING);
+ boolean sendUpdates = contact
+ .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)
+ && contact.getOption(Contact.Options.PREEMPTIVE_GRANT);
+ IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
+ iq.query("jabber:iq:roster").addChild(contact.asElement());
+ account.getXmppConnection().sendIqPacket(iq, null);
+ if (sendUpdates) {
+ sendPresencePacket(account,
+ mPresenceGenerator.sendPresenceUpdatesTo(contact));
+ }
+ if (ask) {
+ sendPresencePacket(account,
+ mPresenceGenerator.requestPresenceUpdatesFrom(contact));
+ }
+ }
+ }
+
+ public void publishAvatar(Account account, Uri image,
+ final UiCallback<Avatar> callback) {
+ final Bitmap.CompressFormat format = Config.AVATAR_FORMAT;
+ final int size = Config.AVATAR_SIZE;
+ final Avatar avatar = getFileBackend()
+ .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 (!getFileBackend().save(avatar)) {
+ callback.error(R.string.error_saving_avatar, avatar);
+ return;
+ }
+ IqPacket packet = this.mIqGenerator.publishAvatar(avatar);
+ this.sendIqPacket(account, packet, new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket result) {
+ if (result.getType() == IqPacket.TYPE_RESULT) {
+ IqPacket packet = XmppConnectionService.this.mIqGenerator
+ .publishAvatarMetadata(avatar);
+ sendIqPacket(account, packet, new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account,
+ IqPacket result) {
+ if (result.getType() == IqPacket.TYPE_RESULT) {
+ if (account.setAvatar(avatar.getFilename())) {
+ databaseBackend.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);
+ }
+ }
+ });
+ } else {
+ callback.error(R.string.error_publish_avatar_converting, null);
+ }
+ }
+
+ public void fetchAvatar(Account account, Avatar avatar) {
+ fetchAvatar(account, avatar, null);
+ }
+
+ public void fetchAvatar(Account account, final Avatar avatar,
+ final UiCallback<Avatar> callback) {
+ IqPacket packet = this.mIqGenerator.retrieveAvatar(avatar);
+ sendIqPacket(account, packet, new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket result) {
+ final String ERROR = account.getJid()
+ + ": fetching avatar for " + avatar.owner + " failed ";
+ if (result.getType() == IqPacket.TYPE_RESULT) {
+ avatar.image = mIqParser.avatarData(result);
+ if (avatar.image != null) {
+ if (getFileBackend().save(avatar)) {
+ if (account.getJid().equals(avatar.owner)) {
+ if (account.setAvatar(avatar.getFilename())) {
+ databaseBackend.updateAccount(account);
+ }
+ getAvatarService().clear(account);
+ updateConversationUi();
+ updateAccountUi();
+ } else {
+ Contact contact = account.getRoster()
+ .getContact(avatar.owner);
+ contact.setAvatar(avatar.getFilename());
+ getAvatarService().clear(contact);
+ updateConversationUi();
+ updateRosterUi();
+ }
+ if (callback != null) {
+ callback.success(avatar);
+ }
+ Log.d(Config.LOGTAG, account.getJid()
+ + ": succesfully fetched avatar for "
+ + avatar.owner);
+ return;
+ }
+ } else {
+
+ Log.d(Config.LOGTAG, ERROR + "(parsing error)");
+ }
+ } else {
+ Element error = result.findChild("error");
+ if (error == null) {
+ Log.d(Config.LOGTAG, ERROR + "(server error)");
+ } else {
+ Log.d(Config.LOGTAG, ERROR + error.toString());
+ }
+ }
+ if (callback != null) {
+ callback.error(0, null);
+ }
+
+ }
+ });
+ }
+
+ public void checkForAvatar(Account account,
+ final UiCallback<Avatar> callback) {
+ IqPacket packet = this.mIqGenerator.retrieveAvatarMetaData(null);
+ this.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();
+ if (fileBackend.isAvatarCached(avatar)) {
+ if (account.setAvatar(avatar.getFilename())) {
+ databaseBackend.updateAccount(account);
+ }
+ getAvatarService().clear(account);
+ callback.success(avatar);
+ } else {
+ fetchAvatar(account, avatar, callback);
+ }
+ return;
+ }
+ }
+ }
+ }
+ callback.error(0, null);
+ }
+ });
+ }
+
+ 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.STATUS_ONLINE) {
+ IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
+ Element item = iq.query("jabber:iq:roster").addChild("item");
+ item.setAttribute("jid", contact.getJid());
+ item.setAttribute("subscription", "remove");
+ account.getXmppConnection().sendIqPacket(iq, null);
+ }
+ }
+
+ public void updateConversation(Conversation conversation) {
+ this.databaseBackend.updateConversation(conversation);
+ }
+
+ public void reconnectAccount(final Account account, final boolean force) {
+ new Thread(new Runnable() {
+
+ @Override
+ public void run() {
+ if (account.getXmppConnection() != null) {
+ disconnect(account, force);
+ }
+ if (!account.isOptionSet(Account.OPTION_DISABLED)) {
+ if (account.getXmppConnection() == null) {
+ account.setXmppConnection(createConnection(account));
+ }
+ Thread thread = new Thread(account.getXmppConnection());
+ thread.start();
+ scheduleWakeupCall((int) (Config.CONNECT_TIMEOUT * 1.2),
+ false);
+ } else {
+ account.getRoster().clearPresences();
+ account.setXmppConnection(null);
+ }
+ }
+ }).start();
+ }
+
+ public void invite(Conversation conversation, String contact) {
+ MessagePacket packet = mMessageGenerator.invite(conversation, contact);
+ sendMessagePacket(conversation.getAccount(), packet);
+ }
+
+ public void resetSendingToWaiting(Account account) {
+ for (Conversation conversation : getConversations()) {
+ if (conversation.getAccount() == account) {
+ for (Message message : conversation.getMessages()) {
+ if (message.getType() != Message.TYPE_IMAGE
+ && message.getStatus() == Message.STATUS_UNSEND) {
+ markMessage(message, Message.STATUS_WAITING);
+ }
+ }
+ }
+ }
+ }
+
+ public boolean markMessage(Account account, String recipient, String uuid,
+ int status) {
+ if (uuid == null) {
+ return false;
+ } else {
+ for (Conversation conversation : getConversations()) {
+ if (conversation.getContactJid().equals(recipient)
+ && conversation.getAccount().equals(account)) {
+ return markMessage(conversation, uuid, status);
+ }
+ }
+ return false;
+ }
+ }
+
+ public boolean markMessage(Conversation conversation, String uuid,
+ int status) {
+ if (uuid == null) {
+ return false;
+ } else {
+ for (Message message : conversation.getMessages()) {
+ if (uuid.equals(message.getUuid())
+ || (message.getStatus() >= Message.STATUS_SEND && uuid
+ .equals(message.getRemoteMsgId()))) {
+ markMessage(message, status);
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
+ public void markMessage(Message message, int status) {
+ if (status == Message.STATUS_SEND_FAILED
+ && (message.getStatus() == Message.STATUS_SEND_RECEIVED || message
+ .getStatus() == Message.STATUS_SEND_DISPLAYED)) {
+ return;
+ }
+ message.setStatus(status);
+ databaseBackend.updateMessage(message);
+ updateConversationUi();
+ }
+
+ public SharedPreferences getPreferences() {
+ return PreferenceManager
+ .getDefaultSharedPreferences(getApplicationContext());
+ }
+
+ public boolean forceEncryption() {
+ return getPreferences().getBoolean("force_encryption", false);
+ }
+
+ public boolean confirmMessages() {
+ return getPreferences().getBoolean("confirm_messages", true);
+ }
+
+ public boolean saveEncryptedMessages() {
+ return !getPreferences().getBoolean("dont_save_encrypted", false);
+ }
+
+ public boolean indicateReceived() {
+ return getPreferences().getBoolean("indicate_received", false);
+ }
+
+ 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 Account findAccountByJid(String accountJid) {
+ for (Account account : this.accounts) {
+ if (account.getJid().equals(accountJid)) {
+ return account;
+ }
+ }
+ return null;
+ }
+
+ public Conversation findConversationByUuid(String uuid) {
+ for (Conversation conversation : getConversations()) {
+ if (conversation.getUuid().equals(uuid)) {
+ return conversation;
+ }
+ }
+ return null;
+ }
+
+ public void markRead(Conversation conversation, boolean calledByUi) {
+ mNotificationService.clear(conversation);
+ String id = conversation.getLatestMarkableMessageId();
+ conversation.markRead();
+ if (confirmMessages() && id != null && calledByUi) {
+ Log.d(Config.LOGTAG, conversation.getAccount().getJid()
+ + ": sending read marker for " + conversation.getName());
+ Account account = conversation.getAccount();
+ String to = conversation.getContactJid();
+ this.sendMessagePacket(conversation.getAccount(),
+ mMessageGenerator.confirm(account, to, id));
+ }
+ if (!calledByUi) {
+ updateConversationUi();
+ }
+ }
+
+ public void failWaitingOtrMessages(Conversation conversation) {
+ for (Message message : conversation.getMessages()) {
+ if (message.getEncryption() == Message.ENCRYPTION_OTR
+ && message.getStatus() == Message.STATUS_WAITING) {
+ markMessage(message, Message.STATUS_SEND_FAILED);
+ }
+ }
+ }
+
+ public SecureRandom getRNG() {
+ return this.mRandom;
+ }
+
+ public MemorizingTrustManager getMemorizingTrustManager() {
+ return this.mMemorizingTrustManager;
+ }
+
+ public PowerManager getPowerManager() {
+ return this.pm;
+ }
+
+ public LruCache<String, Bitmap> getBitmapCache() {
+ return this.mBitmapCache;
+ }
+
+ public void replyWithNotAcceptable(Account account, MessagePacket packet) {
+ if (account.getStatus() == Account.STATUS_ONLINE) {
+ MessagePacket error = this.mMessageGenerator
+ .generateNotAcceptable(packet);
+ sendMessagePacket(account, error);
+ }
+ }
+
+ public void syncRosterToDisk(final Account account) {
+ new Thread(new Runnable() {
+
+ @Override
+ public void run() {
+ databaseBackend.writeRoster(account.getRoster());
+ }
+ }).start();
+
+ }
+
+ public List<String> getKnownHosts() {
+ List<String> hosts = new ArrayList<String>();
+ for (Account account : getAccounts()) {
+ if (!hosts.contains(account.getServer())) {
+ hosts.add(account.getServer());
+ }
+ for (Contact contact : account.getRoster().getContacts()) {
+ if (contact.showInRoster()) {
+ String server = contact.getServer();
+ if (server != null && !hosts.contains(server)) {
+ hosts.add(server);
+ }
+ }
+ }
+ }
+ return hosts;
+ }
+
+ public List<String> getKnownConferenceHosts() {
+ ArrayList<String> mucServers = new ArrayList<String>();
+ for (Account account : accounts) {
+ if (account.getXmppConnection() != null) {
+ String server = account.getXmppConnection().getMucServer();
+ if (server != null && !mucServers.contains(server)) {
+ mucServers.add(server);
+ }
+ }
+ }
+ return mucServers;
+ }
+
+ public void sendMessagePacket(Account account, MessagePacket packet) {
+ XmppConnection connection = account.getXmppConnection();
+ if (connection != null) {
+ connection.sendMessagePacket(packet);
+ }
+ }
+
+ public void sendPresencePacket(Account account, PresencePacket packet) {
+ XmppConnection connection = account.getXmppConnection();
+ if (connection != null) {
+ connection.sendPresencePacket(packet);
+ }
+ }
+
+ public void sendIqPacket(Account account, IqPacket packet,
+ OnIqPacketReceived callback) {
+ XmppConnection connection = account.getXmppConnection();
+ if (connection != null) {
+ connection.sendIqPacket(packet, callback);
+ }
+ }
+
+ public MessageGenerator getMessageGenerator() {
+ return this.mMessageGenerator;
+ }
+
+ public PresenceGenerator getPresenceGenerator() {
+ return this.mPresenceGenerator;
+ }
+
+ public IqGenerator getIqGenerator() {
+ return this.mIqGenerator;
+ }
+
+ public JingleConnectionManager getJingleConnectionManager() {
+ return this.mJingleConnectionManager;
+ }
+
+ public interface OnConversationUpdate {
+ public void onConversationUpdate();
+ }
+
+ public interface OnAccountUpdate {
+ public void onAccountUpdate();
+ }
+
+ public interface OnRosterUpdate {
+ public void onRosterUpdate();
+ }
+
+ public List<Contact> findContacts(String jid) {
+ ArrayList<Contact> contacts = new ArrayList<Contact>();
+ 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 HttpConnectionManager getHttpConnectionManager() {
+ return this.mHttpConnectionManager;
+ }
+
+ private class DeletedDownloadable implements Downloadable {
+
+ @Override
+ public boolean start() {
+ return false;
+ }
+
+ @Override
+ public int getStatus() {
+ return Downloadable.STATUS_DELETED;
+ }
+
+ @Override
+ public long getFileSize() {
+ return 0;
+ }
+
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java b/conversations/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java
new file mode 100644
index 00000000..62a2cbe1
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java
@@ -0,0 +1,145 @@
+package eu.siacs.conversations.ui;
+
+import java.util.ArrayList;
+import java.util.Collections;
+
+import android.content.Context;
+import android.content.Intent;
+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.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.ListView;
+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.ui.adapter.ListItemAdapter;
+
+public class ChooseContactActivity extends XmppActivity {
+
+ private ListView mListView;
+ private ArrayList<ListItem> contacts = new ArrayList<ListItem>();
+ private ArrayAdapter<ListItem> mContactsAdapter;
+
+ private EditText mSearchEditText;
+
+ private TextWatcher mSearchTextWatcher = new TextWatcher() {
+
+ @Override
+ public void afterTextChanged(Editable editable) {
+ filterContacts(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.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("");
+ filterContacts(null);
+ return true;
+ }
+ };
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_choose_contact);
+ mListView = (ListView) findViewById(R.id.choose_contact_list);
+ mListView.setFastScrollEnabled(true);
+ mContactsAdapter = new ListItemAdapter(this, contacts);
+ mListView.setAdapter(mContactsAdapter);
+ mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+
+ @Override
+ public void onItemClick(AdapterView<?> arg0, View arg1,
+ int position, long arg3) {
+ InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.hideSoftInputFromWindow(mSearchEditText.getWindowToken(),
+ InputMethodManager.HIDE_IMPLICIT_ONLY);
+ Intent request = getIntent();
+ Intent data = new Intent();
+ ListItem mListItem = contacts.get(position);
+ data.putExtra("contact", mListItem.getJid());
+ String account = request.getStringExtra("account");
+ if (account == null && mListItem instanceof Contact) {
+ account = ((Contact) mListItem).getAccount().getJid();
+ }
+ data.putExtra("account", account);
+ data.putExtra("conversation",
+ request.getStringExtra("conversation"));
+ setResult(RESULT_OK, data);
+ finish();
+ }
+ });
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.choose_contact, menu);
+ MenuItem menuSearchView = (MenuItem) menu.findItem(R.id.action_search);
+ View mSearchView = menuSearchView.getActionView();
+ mSearchEditText = (EditText) mSearchView
+ .findViewById(R.id.search_field);
+ mSearchEditText.addTextChangedListener(mSearchTextWatcher);
+ menuSearchView.setOnActionExpandListener(mOnActionExpandListener);
+ return true;
+ }
+
+ @Override
+ void onBackendConnected() {
+ filterContacts(null);
+ }
+
+ protected void filterContacts(String needle) {
+ this.contacts.clear();
+ for (Account account : xmppConnectionService.getAccounts()) {
+ if (account.getStatus() != Account.STATUS_DISABLED) {
+ for (Contact contact : account.getRoster().getContacts()) {
+ if (contact.showInRoster() && contact.match(needle)) {
+ this.contacts.add(contact);
+ }
+ }
+ }
+ }
+ Collections.sort(this.contacts);
+ mContactsAdapter.notifyDataSetChanged();
+ }
+
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/conversations/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java
new file mode 100644
index 00000000..6b4642cb
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java
@@ -0,0 +1,280 @@
+package eu.siacs.conversations.ui;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.openintents.openpgp.util.OpenPgpUtils;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.crypto.PgpEngine;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.MucOptions;
+import eu.siacs.conversations.entities.MucOptions.OnRenameListener;
+import eu.siacs.conversations.entities.MucOptions.User;
+import eu.siacs.conversations.services.XmppConnectionService.OnConversationUpdate;
+import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.IntentSender.SendIntentException;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+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.TextView;
+import android.widget.Toast;
+
+public class ConferenceDetailsActivity extends XmppActivity {
+ public static final String ACTION_VIEW_MUC = "view_muc";
+ private Conversation conversation;
+ 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 Button mInviteButton;
+ private String uuid = null;
+
+ private OnClickListener inviteListener = new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ inviteToConversation(conversation);
+ }
+ };
+
+ private List<User> users = new ArrayList<MucOptions.User>();
+ private OnConversationUpdate onConvChanged = new OnConversationUpdate() {
+
+ @Override
+ public void onConversationUpdate() {
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ populateView();
+ }
+ });
+ }
+ };
+
+ @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);
+ mInviteButton = (Button) findViewById(R.id.invite);
+ mInviteButton.setOnClickListener(inviteListener);
+ getActionBar().setHomeButtonEnabled(true);
+ getActionBar().setDisplayHomeAsUpEnabled(true);
+ mEditNickButton.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ quickEdit(conversation.getMucOptions().getActualNick(),
+ new OnValueEdited() {
+
+ @Override
+ public void onValueEdited(String value) {
+ xmppConnectionService.renameInMuc(conversation,
+ value);
+ }
+ });
+ }
+ });
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem menuItem) {
+ switch (menuItem.getItemId()) {
+ case android.R.id.home:
+ finish();
+ break;
+ case R.id.action_edit_subject:
+ if (conversation != null) {
+ quickEdit(conversation.getName(), new OnValueEdited() {
+
+ @Override
+ public void onValueEdited(String value) {
+ MessagePacket packet = xmppConnectionService
+ .getMessageGenerator().conferenceSubject(
+ conversation, value);
+ xmppConnectionService.sendMessagePacket(
+ conversation.getAccount(), packet);
+ }
+ });
+ }
+ break;
+ }
+ return super.onOptionsItemSelected(menuItem);
+ }
+
+ public String getReadableRole(int role) {
+ switch (role) {
+ case User.ROLE_MODERATOR:
+ return getString(R.string.moderator);
+ case User.ROLE_PARTICIPANT:
+ return getString(R.string.participant);
+ case User.ROLE_VISITOR:
+ return getString(R.string.visitor);
+ default:
+ return "";
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.muc_details, menu);
+ return true;
+ }
+
+ @Override
+ void onBackendConnected() {
+ registerListener();
+ if (getIntent().getAction().equals(ACTION_VIEW_MUC)) {
+ this.uuid = getIntent().getExtras().getString("uuid");
+ }
+ if (uuid != null) {
+ this.conversation = xmppConnectionService
+ .findConversationByUuid(uuid);
+ if (this.conversation != null) {
+ populateView();
+ }
+ }
+ }
+
+ @Override
+ protected void onStop() {
+ if (xmppConnectionServiceBound) {
+ xmppConnectionService.removeOnConversationListChangedListener();
+ }
+ super.onStop();
+ }
+
+ protected void registerListener() {
+ xmppConnectionService
+ .setOnConversationListChangedListener(this.onConvChanged);
+ xmppConnectionService.setOnRenameListener(new OnRenameListener() {
+
+ @Override
+ public void onRename(final boolean success) {
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ populateView();
+ if (success) {
+ Toast.makeText(
+ ConferenceDetailsActivity.this,
+ getString(R.string.your_nick_has_been_changed),
+ Toast.LENGTH_SHORT).show();
+ } else {
+ Toast.makeText(ConferenceDetailsActivity.this,
+ getString(R.string.nick_in_use),
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+ });
+ }
+ });
+ }
+
+ private void populateView() {
+ mAccountJid.setText(getString(R.string.using_account, conversation
+ .getAccount().getJid()));
+ mYourPhoto.setImageBitmap(avatarService().get(
+ conversation.getAccount(), getPixel(48)));
+ setTitle(conversation.getName());
+ mFullJid.setText(conversation.getContactJid().split("/", 2)[0]);
+ mYourNick.setText(conversation.getMucOptions().getActualNick());
+ mRoleAffiliaton = (TextView) findViewById(R.id.muc_role);
+ if (conversation.getMucOptions().online()) {
+ mMoreDetails.setVisibility(View.VISIBLE);
+ User self = conversation.getMucOptions().getSelf();
+ switch (self.getAffiliation()) {
+ case User.AFFILIATION_ADMIN:
+ mRoleAffiliaton.setText(getReadableRole(self.getRole()) + " ("
+ + getString(R.string.admin) + ")");
+ break;
+ case User.AFFILIATION_OWNER:
+ mRoleAffiliaton.setText(getReadableRole(self.getRole()) + " ("
+ + getString(R.string.owner) + ")");
+ break;
+ default:
+ mRoleAffiliaton.setText(getReadableRole(self.getRole()));
+ break;
+ }
+ }
+ this.users.clear();
+ this.users.addAll(conversation.getMucOptions().getUsers());
+ LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ membersView.removeAllViews();
+ for (final User user : conversation.getMucOptions().getUsers()) {
+ View view = (View) inflater.inflate(R.layout.contact, membersView,
+ false);
+ TextView name = (TextView) view
+ .findViewById(R.id.contact_display_name);
+ TextView key = (TextView) view.findViewById(R.id.key);
+ TextView role = (TextView) view.findViewById(R.id.contact_jid);
+ if (user.getPgpKeyId() != 0) {
+ key.setVisibility(View.VISIBLE);
+ key.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ viewPgpKey(user);
+ }
+ });
+ key.setText(OpenPgpUtils.convertKeyIdToHex(user.getPgpKeyId()));
+ }
+ Bitmap bm;
+ Contact contact = user.getContact();
+ if (contact != null) {
+ bm = avatarService().get(contact, getPixel(48));
+ name.setText(contact.getDisplayName());
+ role.setText(user.getName() + " \u2022 "
+ + getReadableRole(user.getRole()));
+ } else {
+ bm = avatarService().get(user.getName(), getPixel(48));
+ name.setText(user.getName());
+ role.setText(getReadableRole(user.getRole()));
+ }
+ ImageView iv = (ImageView) view.findViewById(R.id.contact_photo);
+ iv.setImageBitmap(bm);
+ membersView.addView(view);
+ }
+ }
+
+ private void viewPgpKey(User user) {
+ PgpEngine pgp = xmppConnectionService.getPgpEngine();
+ if (pgp != null) {
+ PendingIntent intent = pgp.getIntentForKey(
+ conversation.getAccount(), user.getPgpKeyId());
+ if (intent != null) {
+ try {
+ startIntentSenderForResult(intent.getIntentSender(), 0,
+ null, 0, 0, 0);
+ } catch (SendIntentException e) {
+
+ }
+ }
+ }
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/conversations/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java
new file mode 100644
index 00000000..ae26466e
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java
@@ -0,0 +1,436 @@
+package eu.siacs.conversations.ui;
+
+import java.util.Iterator;
+
+import org.openintents.openpgp.util.OpenPgpUtils;
+
+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.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.CheckBox;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+import android.widget.CompoundButton;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.QuickContactBadge;
+import android.widget.TextView;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.crypto.PgpEngine;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Presences;
+import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
+import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate;
+import eu.siacs.conversations.utils.UIHelper;
+
+public class ContactDetailsActivity extends XmppActivity {
+ public static final String ACTION_VIEW_CONTACT = "view_contact";
+
+ private Contact contact;
+
+ private String accountJid;
+ private String contactJid;
+
+ private TextView contactJidTv;
+ private TextView accountJidTv;
+ private TextView status;
+ private TextView lastseen;
+ private CheckBox send;
+ private CheckBox receive;
+ private QuickContactBadge badge;
+
+ private DialogInterface.OnClickListener removeFromRoster = new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ ContactDetailsActivity.this.xmppConnectionService
+ .deleteContactOnServer(contact);
+ ContactDetailsActivity.this.finish();
+ }
+ };
+
+ 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());
+ 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) {
+ 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.getJid()));
+ builder.setNegativeButton(getString(R.string.cancel), null);
+ builder.setPositiveButton(getString(R.string.add), addToPhonebook);
+ builder.create().show();
+ }
+ };
+
+ private LinearLayout keys;
+
+ private OnRosterUpdate rosterUpdate = new OnRosterUpdate() {
+
+ @Override
+ public void onRosterUpdate() {
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ populateView();
+ }
+ });
+ }
+ };
+
+ 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 OnAccountUpdate accountUpdate = new OnAccountUpdate() {
+
+ @Override
+ public void onAccountUpdate() {
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ populateView();
+ }
+ });
+ }
+ };
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (getIntent().getAction().equals(ACTION_VIEW_CONTACT)) {
+ this.accountJid = getIntent().getExtras().getString("account");
+ this.contactJid = getIntent().getExtras().getString("contact");
+ }
+ setContentView(R.layout.activity_contact_details);
+
+ contactJidTv = (TextView) findViewById(R.id.details_contactjid);
+ accountJidTv = (TextView) findViewById(R.id.details_account);
+ status = (TextView) findViewById(R.id.details_contactstatus);
+ 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);
+ keys = (LinearLayout) findViewById(R.id.details_contact_keys);
+ getActionBar().setHomeButtonEnabled(true);
+ getActionBar().setDisplayHomeAsUpEnabled(true);
+
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem menuItem) {
+ 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.getJid()))
+ .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;
+ }
+ return super.onOptionsItemSelected(menuItem);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.contact_details, menu);
+ return true;
+ }
+
+ private void populateView() {
+ send.setOnCheckedChangeListener(null);
+ receive.setOnCheckedChangeListener(null);
+ setTitle(contact.getDisplayName());
+ 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().getStatus() == Account.STATUS_ONLINE) {
+ receive.setEnabled(true);
+ send.setEnabled(true);
+ } else {
+ receive.setEnabled(false);
+ send.setEnabled(false);
+ }
+
+ send.setOnCheckedChangeListener(this.mOnSendCheckedChange);
+ receive.setOnCheckedChangeListener(this.mOnReceiveCheckedChange);
+
+ lastseen.setText(UIHelper.lastseen(getApplicationContext(),
+ contact.lastseen.time));
+
+ switch (contact.getMostAvailableStatus()) {
+ case Presences.CHAT:
+ status.setText(R.string.contact_status_free_to_chat);
+ status.setTextColor(mColorGreen);
+ break;
+ case Presences.ONLINE:
+ status.setText(R.string.contact_status_online);
+ status.setTextColor(mColorGreen);
+ break;
+ case Presences.AWAY:
+ status.setText(R.string.contact_status_away);
+ status.setTextColor(mColorOrange);
+ break;
+ case Presences.XA:
+ status.setText(R.string.contact_status_extended_away);
+ status.setTextColor(mColorOrange);
+ break;
+ case Presences.DND:
+ status.setText(R.string.contact_status_do_not_disturb);
+ status.setTextColor(mColorRed);
+ break;
+ case Presences.OFFLINE:
+ status.setText(R.string.contact_status_offline);
+ status.setTextColor(mSecondaryTextColor);
+ break;
+ default:
+ status.setText(R.string.contact_status_offline);
+ status.setTextColor(mSecondaryTextColor);
+ break;
+ }
+ if (contact.getPresences().size() > 1) {
+ contactJidTv.setText(contact.getJid() + " ("
+ + contact.getPresences().size() + ")");
+ } else {
+ contactJidTv.setText(contact.getJid());
+ }
+ accountJidTv.setText(getString(R.string.using_account, contact
+ .getAccount().getJid()));
+ prepareContactBadge(badge, contact);
+ if (contact.getSystemAccount() == null) {
+ badge.setOnClickListener(onBadgeClick);
+ }
+
+ keys.removeAllViews();
+ boolean hasKeys = false;
+ LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ for (Iterator<String> iterator = contact.getOtrFingerprints()
+ .iterator(); iterator.hasNext();) {
+ hasKeys = true;
+ final String otrFingerprint = iterator.next();
+ View 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 remove = (ImageButton) view
+ .findViewById(R.id.button_remove);
+ remove.setVisibility(View.VISIBLE);
+ keyType.setText("OTR Fingerprint");
+ key.setText(otrFingerprint);
+ keys.addView(view);
+ remove.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ confirmToDeleteFingerprint(otrFingerprint);
+ }
+ });
+ }
+ if (contact.getPgpKeyId() != 0) {
+ hasKeys = true;
+ View 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);
+ }
+ }
+
+ private void prepareContactBadge(QuickContactBadge badge, Contact contact) {
+ if (contact.getSystemAccount() != null) {
+ String[] systemAccount = contact.getSystemAccount().split("#");
+ long id = Long.parseLong(systemAccount[0]);
+ badge.assignContactUri(Contacts.getLookupUri(id, systemAccount[1]));
+ }
+ badge.setImageBitmap(avatarService().get(contact, getPixel(72)));
+ }
+
+ 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() {
+ xmppConnectionService.setOnRosterUpdateListener(this.rosterUpdate);
+ xmppConnectionService
+ .setOnAccountListChangedListener(this.accountUpdate);
+ if ((accountJid != null) && (contactJid != null)) {
+ Account account = xmppConnectionService
+ .findAccountByJid(accountJid);
+ if (account == null) {
+ return;
+ }
+ this.contact = account.getRoster().getContact(contactJid);
+ populateView();
+ }
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ xmppConnectionService.removeOnRosterUpdateListener();
+ xmppConnectionService.removeOnAccountListChangedListener();
+ }
+
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java b/conversations/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java
new file mode 100644
index 00000000..91e1c81f
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java
@@ -0,0 +1,947 @@
+package eu.siacs.conversations.ui;
+
+import java.util.ArrayList;
+import java.util.List;
+
+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.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 android.net.Uri;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.provider.MediaStore;
+import android.annotation.SuppressLint;
+import android.app.ActionBar;
+import android.app.AlertDialog;
+import android.app.FragmentTransaction;
+import android.app.PendingIntent;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.IntentSender.SendIntentException;
+import android.content.Intent;
+import android.support.v4.widget.SlidingPaneLayout;
+import android.support.v4.widget.SlidingPaneLayout.PanelSlideListener;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.CheckBox;
+import android.widget.ListView;
+import android.widget.PopupMenu;
+import android.widget.PopupMenu.OnMenuItemClickListener;
+import android.widget.Toast;
+
+public class ConversationActivity extends XmppActivity implements
+ OnAccountUpdate, OnConversationUpdate, OnRosterUpdate {
+
+ public static final String VIEW_CONVERSATION = "viewConversation";
+ public static final String CONVERSATION = "conversationUuid";
+ public static final String TEXT = "text";
+ public static final String PRESENCE = "eu.siacs.conversations.presence";
+
+ public static final int REQUEST_SEND_MESSAGE = 0x0201;
+ public static final int REQUEST_DECRYPT_PGP = 0x0202;
+ private static final int REQUEST_ATTACH_FILE_DIALOG = 0x0203;
+ private static final int REQUEST_IMAGE_CAPTURE = 0x0204;
+ private static final int REQUEST_RECORD_AUDIO = 0x0205;
+ private static final int REQUEST_SEND_PGP_IMAGE = 0x0206;
+ public static final int REQUEST_ENCRYPT_MESSAGE = 0x0207;
+
+ private static final int ATTACHMENT_CHOICE_CHOOSE_IMAGE = 0x0301;
+ private static final int ATTACHMENT_CHOICE_TAKE_PHOTO = 0x0302;
+ private static final int ATTACHMENT_CHOICE_RECORD_VOICE = 0x0303;
+ private static final String STATE_OPEN_CONVERSATION = "state_open_conversation";
+ private static final String STATE_PANEL_OPEN = "state_panel_open";
+
+ private String mOpenConverstaion = null;
+ private boolean mPanelOpen = true;
+
+ private View mContentView;
+
+ private List<Conversation> conversationList = new ArrayList<Conversation>();
+ private Conversation selectedConversation = null;
+ private ListView listView;
+
+ private boolean paneShouldBeOpen = true;
+ private ArrayAdapter<Conversation> listAdapter;
+
+ private Toast prepareImageToast;
+
+ private Uri pendingImageUri = null;
+
+ public List<Conversation> getConversationList() {
+ return this.conversationList;
+ }
+
+ public Conversation getSelectedConversation() {
+ return this.selectedConversation;
+ }
+
+ public void setSelectedConversation(Conversation conversation) {
+ this.selectedConversation = conversation;
+ }
+
+ public ListView getConversationListView() {
+ return this.listView;
+ }
+
+ public boolean shouldPaneBeOpen() {
+ return paneShouldBeOpen;
+ }
+
+ public void showConversationsOverview() {
+ if (mContentView instanceof SlidingPaneLayout) {
+ SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView;
+ mSlidingPaneLayout.openPane();
+ }
+ }
+
+ public void hideConversationsOverview() {
+ if (mContentView instanceof SlidingPaneLayout) {
+ SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView;
+ mSlidingPaneLayout.closePane();
+ }
+ }
+
+ public boolean isConversationsOverviewHideable() {
+ if (mContentView instanceof SlidingPaneLayout) {
+ SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView;
+ return mSlidingPaneLayout.isSlideable();
+ } else {
+ return false;
+ }
+ }
+
+ public boolean isConversationsOverviewVisable() {
+ if (mContentView instanceof SlidingPaneLayout) {
+ SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView;
+ return mSlidingPaneLayout.isOpen();
+ } else {
+ return true;
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ if (savedInstanceState != null) {
+ mOpenConverstaion = savedInstanceState.getString(
+ STATE_OPEN_CONVERSATION, null);
+ mPanelOpen = savedInstanceState.getBoolean(STATE_PANEL_OPEN, true);
+ }
+
+ setContentView(R.layout.fragment_conversations_overview);
+
+ listView = (ListView) findViewById(R.id.list);
+
+ getActionBar().setDisplayHomeAsUpEnabled(false);
+ getActionBar().setHomeButtonEnabled(false);
+
+ this.listAdapter = new ConversationAdapter(this, conversationList);
+ listView.setAdapter(this.listAdapter);
+
+ listView.setOnItemClickListener(new OnItemClickListener() {
+
+ @Override
+ public void onItemClick(AdapterView<?> arg0, View clickedView,
+ int position, long arg3) {
+ paneShouldBeOpen = false;
+ if (getSelectedConversation() != conversationList.get(position)) {
+ setSelectedConversation(conversationList.get(position));
+ swapConversationFragment();
+ } else {
+ hideConversationsOverview();
+ }
+ }
+ });
+ 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;
+ mSlidingPaneLayout.setParallaxDistance(150);
+ mSlidingPaneLayout
+ .setShadowResource(R.drawable.es_slidingpane_shadow);
+ mSlidingPaneLayout.setSliderFadeColor(0);
+ mSlidingPaneLayout.setPanelSlideListener(new PanelSlideListener() {
+
+ @Override
+ public void onPanelOpened(View arg0) {
+ paneShouldBeOpen = true;
+ ActionBar ab = getActionBar();
+ if (ab != null) {
+ ab.setDisplayHomeAsUpEnabled(false);
+ ab.setHomeButtonEnabled(false);
+ ab.setTitle(R.string.app_name);
+ }
+ invalidateOptionsMenu();
+ hideKeyboard();
+ if (xmppConnectionServiceBound) {
+ xmppConnectionService.getNotificationService()
+ .setOpenConversation(null);
+ }
+ }
+
+ @Override
+ public void onPanelClosed(View arg0) {
+ paneShouldBeOpen = false;
+ if ((conversationList.size() > 0)
+ && (getSelectedConversation() != null)) {
+ openConversation(getSelectedConversation());
+ if (!getSelectedConversation().isRead()) {
+ xmppConnectionService.markRead(
+ getSelectedConversation(), true);
+ listView.invalidateViews();
+ }
+ }
+ }
+
+ @Override
+ public void onPanelSlide(View arg0, float arg1) {
+ // TODO Auto-generated method stub
+
+ }
+ });
+ }
+ }
+
+ public void openConversation(Conversation conversation) {
+ ActionBar ab = getActionBar();
+ if (ab != null) {
+ ab.setDisplayHomeAsUpEnabled(true);
+ ab.setHomeButtonEnabled(true);
+ if (getSelectedConversation().getMode() == Conversation.MODE_SINGLE
+ || ConversationActivity.this
+ .useSubjectToIdentifyConference()) {
+ ab.setTitle(getSelectedConversation().getName());
+ } else {
+ ab.setTitle(getSelectedConversation().getContactJid()
+ .split("/")[0]);
+ }
+ }
+ invalidateOptionsMenu();
+ if (xmppConnectionServiceBound) {
+ xmppConnectionService.getNotificationService().setOpenConversation(
+ conversation);
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.conversations, menu);
+ MenuItem menuSecure = (MenuItem) menu.findItem(R.id.action_security);
+ MenuItem menuArchive = (MenuItem) menu.findItem(R.id.action_archive);
+ MenuItem menuMucDetails = (MenuItem) menu
+ .findItem(R.id.action_muc_details);
+ MenuItem menuContactDetails = (MenuItem) menu
+ .findItem(R.id.action_contact_details);
+ MenuItem menuAttach = (MenuItem) menu.findItem(R.id.action_attach_file);
+ MenuItem menuClearHistory = (MenuItem) menu
+ .findItem(R.id.action_clear_history);
+ MenuItem menuAdd = (MenuItem) menu.findItem(R.id.action_add);
+ MenuItem menuInviteContact = (MenuItem) menu
+ .findItem(R.id.action_invite);
+ MenuItem menuMute = (MenuItem) menu.findItem(R.id.action_mute);
+
+ 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);
+ } else {
+ menuAdd.setVisible(!isConversationsOverviewHideable());
+ if (this.getSelectedConversation() != null) {
+ if (this.getSelectedConversation().getLatestMessage()
+ .getEncryption() != Message.ENCRYPTION_NONE) {
+ menuSecure.setIcon(R.drawable.ic_action_secure);
+ }
+ if (this.getSelectedConversation().getMode() == Conversation.MODE_MULTI) {
+ menuContactDetails.setVisible(false);
+ menuAttach.setVisible(false);
+ } else {
+ menuMucDetails.setVisible(false);
+ menuInviteContact.setVisible(false);
+ }
+ }
+ }
+ return true;
+ }
+
+ private void selectPresenceToAttachFile(final int attachmentChoice) {
+ selectPresence(getSelectedConversation(), new OnPresenceSelected() {
+
+ @Override
+ public void onPresenceSelected() {
+ if (attachmentChoice == ATTACHMENT_CHOICE_TAKE_PHOTO) {
+ pendingImageUri = xmppConnectionService.getFileBackend()
+ .getTakePhotoUri();
+ Intent takePictureIntent = new Intent(
+ MediaStore.ACTION_IMAGE_CAPTURE);
+ takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT,
+ pendingImageUri);
+ if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
+ startActivityForResult(takePictureIntent,
+ REQUEST_IMAGE_CAPTURE);
+ }
+ } else if (attachmentChoice == ATTACHMENT_CHOICE_CHOOSE_IMAGE) {
+ 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, REQUEST_ATTACH_FILE_DIALOG);
+ } else if (attachmentChoice == ATTACHMENT_CHOICE_RECORD_VOICE) {
+ Intent intent = new Intent(
+ MediaStore.Audio.Media.RECORD_SOUND_ACTION);
+ startActivityForResult(intent, REQUEST_RECORD_AUDIO);
+ }
+ }
+ });
+ }
+
+ private void attachFile(final int attachmentChoice) {
+ final Conversation conversation = getSelectedConversation();
+ if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) {
+ if (hasPgp()) {
+ if (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);
+ }
+
+ @Override
+ public void error(int error, Contact contact) {
+ displayErrorDialog(error);
+ }
+ });
+ } 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);
+ }
+ });
+ }
+ }
+ } else {
+ showInstallPgpDialog();
+ }
+ } else if (getSelectedConversation().getNextEncryption(
+ forceEncryption()) == Message.ENCRYPTION_NONE) {
+ selectPresenceToAttachFile(attachmentChoice);
+ } else {
+ selectPresenceToAttachFile(attachmentChoice);
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(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:
+ Contact contact = this.getSelectedConversation().getContact();
+ if (contact.showInRoster()) {
+ switchToContactDetails(contact);
+ } else {
+ showAddToRosterDialog(getSelectedConversation());
+ }
+ 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;
+ default:
+ break;
+ }
+ return super.onOptionsItemSelected(item);
+ } else {
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ public void endConversation(Conversation conversation) {
+ conversation.setStatus(Conversation.STATUS_ARCHIVED);
+ paneShouldBeOpen = true;
+ showConversationsOverview();
+ xmppConnectionService.archiveConversation(conversation);
+ if (conversationList.size() > 0) {
+ setSelectedConversation(conversationList.get(0));
+ } else {
+ setSelectedConversation(null);
+ }
+ }
+
+ @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);
+ }
+ }
+ });
+ 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);
+ 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_record_voice:
+ attachFile(ATTACHMENT_CHOICE_RECORD_VOICE);
+ break;
+ }
+ return false;
+ }
+ });
+ attachFilePopup.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().getKeys()
+ .has("pgp_signature")) {
+ conversation
+ .setNextEncryption(Message.ENCRYPTION_PGP);
+ item.setChecked(true);
+ } else {
+ announcePgp(conversation.getAccount(),
+ conversation);
+ }
+ } else {
+ showInstallPgpDialog();
+ }
+ break;
+ default:
+ conversation.setNextEncryption(Message.ENCRYPTION_NONE);
+ break;
+ }
+ xmppConnectionService.databaseBackend
+ .updateConversation(conversation);
+ fragment.updateChatMsgHint();
+ 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);
+ if (conversation.getMode() == Conversation.MODE_MULTI) {
+ otr.setEnabled(false);
+ } else {
+ if (forceEncryption()) {
+ none.setVisible(false);
+ }
+ }
+ switch (conversation.getNextEncryption(forceEncryption())) {
+ case Message.ENCRYPTION_NONE:
+ none.setChecked(true);
+ break;
+ case Message.ENCRYPTION_OTR:
+ otr.setChecked(true);
+ break;
+ case Message.ENCRYPTION_PGP:
+ popup.getMenu().findItem(R.id.encryption_choice_pgp)
+ .setChecked(true);
+ break;
+ default:
+ popup.getMenu().findItem(R.id.encryption_choice_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_for_this_conversation);
+ final int[] durations = getResources().getIntArray(
+ R.array.mute_options_durations);
+ builder.setItems(R.array.mute_options_descriptions,
+ new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ long till;
+ if (durations[which] == -1) {
+ till = Long.MAX_VALUE;
+ } else {
+ till = SystemClock.elapsedRealtime()
+ + (durations[which] * 1000);
+ }
+ conversation.setMutedTill(till);
+ ConversationActivity.this.xmppConnectionService.databaseBackend
+ .updateConversation(conversation);
+ ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager()
+ .findFragmentByTag("conversation");
+ if (selectedFragment != null) {
+ selectedFragment.updateMessages();
+ }
+ }
+ });
+ builder.create().show();
+ }
+
+ protected ConversationFragment swapConversationFragment() {
+ ConversationFragment selectedFragment = new ConversationFragment();
+ if (!isFinishing()) {
+
+ FragmentTransaction transaction = getFragmentManager()
+ .beginTransaction();
+ transaction.replace(R.id.selected_conversation, selectedFragment,
+ "conversation");
+ try {
+ transaction.commitAllowingStateLoss();
+ } catch (IllegalStateException e) {
+ return selectedFragment;
+ }
+ }
+ return selectedFragment;
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ if (!isConversationsOverviewVisable()) {
+ showConversationsOverview();
+ return false;
+ }
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ if (xmppConnectionServiceBound) {
+ if ((Intent.ACTION_VIEW.equals(intent.getAction()) && (VIEW_CONVERSATION
+ .equals(intent.getType())))) {
+ String convToView = (String) intent.getExtras().get(
+ CONVERSATION);
+ updateConversationList();
+ for (int i = 0; i < conversationList.size(); ++i) {
+ if (conversationList.get(i).getUuid().equals(convToView)) {
+ setSelectedConversation(conversationList.get(i));
+ break;
+ }
+ }
+ paneShouldBeOpen = false;
+ String text = intent.getExtras().getString(TEXT, null);
+ swapConversationFragment().setText(text);
+ }
+ } else {
+ handledViewIntent = false;
+ setIntent(intent);
+ }
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ if (this.xmppConnectionServiceBound) {
+ this.onBackendConnected();
+ }
+ if (conversationList.size() >= 1) {
+ this.onConversationUpdate();
+ }
+ }
+
+ @Override
+ protected void onStop() {
+ if (xmppConnectionServiceBound) {
+ xmppConnectionService.removeOnConversationListChangedListener();
+ xmppConnectionService.removeOnAccountListChangedListener();
+ xmppConnectionService.removeOnRosterUpdateListener();
+ xmppConnectionService.getNotificationService().setOpenConversation(
+ null);
+ }
+ super.onStop();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle savedInstanceState) {
+ Conversation conversation = getSelectedConversation();
+ if (conversation != null) {
+ savedInstanceState.putString(STATE_OPEN_CONVERSATION,
+ conversation.getUuid());
+ }
+ savedInstanceState.putBoolean(STATE_PANEL_OPEN,
+ isConversationsOverviewVisable());
+ super.onSaveInstanceState(savedInstanceState);
+ }
+
+ @Override
+ void onBackendConnected() {
+ this.registerListener();
+ updateConversationList();
+
+ if (xmppConnectionService.getAccounts().size() == 0) {
+ startActivity(new Intent(this, EditAccountActivity.class));
+ } else if (conversationList.size() <= 0) {
+ startActivity(new Intent(this, StartConversationActivity.class));
+ finish();
+ } else if (mOpenConverstaion != null) {
+ selectConversationByUuid(mOpenConverstaion);
+ paneShouldBeOpen = mPanelOpen;
+ if (paneShouldBeOpen) {
+ showConversationsOverview();
+ }
+ swapConversationFragment();
+ mOpenConverstaion = null;
+ } else if (getIntent() != null
+ && VIEW_CONVERSATION.equals(getIntent().getType())) {
+ String uuid = (String) getIntent().getExtras().get(CONVERSATION);
+ String text = getIntent().getExtras().getString(TEXT, null);
+ selectConversationByUuid(uuid);
+ paneShouldBeOpen = false;
+ swapConversationFragment().setText(text);
+ setIntent(null);
+ } else {
+ showConversationsOverview();
+ ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager()
+ .findFragmentByTag("conversation");
+ if (selectedFragment != null) {
+ selectedFragment.onBackendConnected();
+ } else {
+ pendingImageUri = null;
+ setSelectedConversation(conversationList.get(0));
+ swapConversationFragment();
+ }
+ }
+
+ if (pendingImageUri != null) {
+ attachImageToConversation(getSelectedConversation(),
+ pendingImageUri);
+ pendingImageUri = null;
+ }
+ ExceptionHelper.checkForCrash(this, this.xmppConnectionService);
+ }
+
+ private void selectConversationByUuid(String uuid) {
+ for (int i = 0; i < conversationList.size(); ++i) {
+ if (conversationList.get(i).getUuid().equals(uuid)) {
+ setSelectedConversation(conversationList.get(i));
+ }
+ }
+ }
+
+ public void registerListener() {
+ xmppConnectionService.setOnConversationListChangedListener(this);
+ xmppConnectionService.setOnAccountListChangedListener(this);
+ xmppConnectionService.setOnRosterUpdateListener(this);
+ }
+
+ @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) {
+ ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager()
+ .findFragmentByTag("conversation");
+ if (selectedFragment != null) {
+ selectedFragment.hideSnackbar();
+ selectedFragment.updateMessages();
+ }
+ } else if (requestCode == REQUEST_ATTACH_FILE_DIALOG) {
+ pendingImageUri = data.getData();
+ if (xmppConnectionServiceBound) {
+ attachImageToConversation(getSelectedConversation(),
+ pendingImageUri);
+ pendingImageUri = null;
+ }
+ } else if (requestCode == REQUEST_SEND_PGP_IMAGE) {
+
+ } else if (requestCode == ATTACHMENT_CHOICE_CHOOSE_IMAGE) {
+ attachFile(ATTACHMENT_CHOICE_CHOOSE_IMAGE);
+ } else if (requestCode == ATTACHMENT_CHOICE_TAKE_PHOTO) {
+ attachFile(ATTACHMENT_CHOICE_TAKE_PHOTO);
+ } else if (requestCode == REQUEST_ANNOUNCE_PGP) {
+ announcePgp(getSelectedConversation().getAccount(),
+ getSelectedConversation());
+ } else if (requestCode == REQUEST_ENCRYPT_MESSAGE) {
+ // encryptTextMessage();
+ } else if (requestCode == REQUEST_IMAGE_CAPTURE) {
+ if (xmppConnectionServiceBound) {
+ attachImageToConversation(getSelectedConversation(),
+ pendingImageUri);
+ pendingImageUri = null;
+ }
+ Intent intent = new Intent(
+ Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
+ intent.setData(pendingImageUri);
+ sendBroadcast(intent);
+ } else if (requestCode == REQUEST_RECORD_AUDIO) {
+ attachAudioToConversation(getSelectedConversation(),
+ data.getData());
+ }
+ } else {
+ if (requestCode == REQUEST_IMAGE_CAPTURE) {
+ pendingImageUri = null;
+ }
+ }
+ }
+
+ private void attachAudioToConversation(Conversation conversation, Uri uri) {
+
+ }
+
+ private void attachImageToConversation(Conversation conversation, Uri uri) {
+ prepareImageToast = Toast.makeText(getApplicationContext(),
+ getText(R.string.preparing_image), Toast.LENGTH_LONG);
+ prepareImageToast.show();
+ xmppConnectionService.attachImageToConversation(conversation, uri,
+ new UiCallback<Message>() {
+
+ @Override
+ public void userInputRequried(PendingIntent pi,
+ Message object) {
+ hidePrepareImageToast();
+ ConversationActivity.this.runIntent(pi,
+ ConversationActivity.REQUEST_SEND_PGP_IMAGE);
+ }
+
+ @Override
+ public void success(Message message) {
+ xmppConnectionService.sendMessage(message);
+ }
+
+ @Override
+ public void error(int error, Message message) {
+ hidePrepareImageToast();
+ displayErrorDialog(error);
+ }
+ });
+ }
+
+ private void hidePrepareImageToast() {
+ if (prepareImageToast != null) {
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ prepareImageToast.cancel();
+ }
+ });
+ }
+ }
+
+ public void updateConversationList() {
+ xmppConnectionService
+ .populateWithOrderedConversations(conversationList);
+ listAdapter.notifyDataSetChanged();
+ }
+
+ public void runIntent(PendingIntent pi, int requestCode) {
+ try {
+ this.startIntentSenderForResult(pi.getIntentSender(), requestCode,
+ null, 0, 0, 0);
+ } catch (SendIntentException e1) {
+ }
+ }
+
+ 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) {
+
+ }
+ });
+ }
+
+ public boolean forceEncryption() {
+ return getPreferences().getBoolean("force_encryption", false);
+ }
+
+ public boolean useSendButtonToIndicateStatus() {
+ return getPreferences().getBoolean("send_button_status", false);
+ }
+
+ public boolean indicateReceived() {
+ return getPreferences().getBoolean("indicate_received", false);
+ }
+
+ @Override
+ public void onAccountUpdate() {
+ final ConversationFragment fragment = (ConversationFragment) getFragmentManager()
+ .findFragmentByTag("conversation");
+ if (fragment != null) {
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ fragment.updateMessages();
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onConversationUpdate() {
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ updateConversationList();
+ if (paneShouldBeOpen) {
+ if (conversationList.size() >= 1) {
+ swapConversationFragment();
+ } else {
+ startActivity(new Intent(getApplicationContext(),
+ StartConversationActivity.class));
+ finish();
+ }
+ }
+ ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager()
+ .findFragmentByTag("conversation");
+ if (selectedFragment != null) {
+ selectedFragment.updateMessages();
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onRosterUpdate() {
+ final ConversationFragment fragment = (ConversationFragment) getFragmentManager()
+ .findFragmentByTag("conversation");
+ if (fragment != null) {
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ fragment.updateMessages();
+ }
+ });
+ }
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/conversations/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
new file mode 100644
index 00000000..0e71801b
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
@@ -0,0 +1,781 @@
+package eu.siacs.conversations.ui;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+import net.java.otr4j.session.SessionStatus;
+import eu.siacs.conversations.R;
+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.Presences;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.ui.EditMessage.OnEnterPressed;
+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.utils.UIHelper;
+import android.app.AlertDialog;
+import android.app.Fragment;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.content.IntentSender.SendIntentException;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.Selection;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+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.AbsListView.OnScrollListener;
+import android.widget.TextView.OnEditorActionListener;
+import android.widget.AbsListView;
+
+import android.widget.ListView;
+import android.widget.ImageButton;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+
+public class ConversationFragment extends Fragment {
+
+ protected Conversation conversation;
+ protected ListView messagesView;
+ protected LayoutInflater inflater;
+ protected List<Message> messageList = new ArrayList<Message>();
+ protected MessageAdapter messageListAdapter;
+ protected Contact contact;
+
+ protected String queuedPqpMessage = null;
+
+ private EditMessage mEditMessage;
+ private ImageButton mSendButton;
+ private String pastedText = null;
+ private RelativeLayout snackbar;
+ private TextView snackbarMessage;
+ private TextView snackbarAction;
+
+ private boolean messagesLoaded = false;
+
+ private IntentSender askForPassphraseIntent = null;
+
+ private ConcurrentLinkedQueue<Message> mEncryptedMessages = new ConcurrentLinkedQueue<Message>();
+ private boolean mDecryptJobRunning = false;
+
+ 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);
+ imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
+ sendMessage();
+ return true;
+ } else {
+ return false;
+ }
+ }
+ };
+
+ private OnClickListener mSendButtonListener = new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ sendMessage();
+ }
+ };
+ protected OnClickListener clickToDecryptListener = new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (activity.hasPgp() && askForPassphraseIntent != null) {
+ try {
+ getActivity().startIntentSenderForResult(
+ askForPassphraseIntent,
+ ConversationActivity.REQUEST_DECRYPT_PGP, null, 0,
+ 0, 0);
+ } catch (SendIntentException e) {
+ //
+ }
+ }
+ }
+ };
+
+ 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 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);
+ }
+ });
+ }
+ };
+
+ private OnScrollListener mOnScrollListener = new OnScrollListener() {
+
+ @Override
+ public void onScrollStateChanged(AbsListView view, int scrollState) {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void onScroll(AbsListView view, int firstVisibleItem,
+ int visibleItemCount, int totalItemCount) {
+ if (firstVisibleItem == 0 && messagesLoaded) {
+ long timestamp = messageList.get(0).getTimeSent();
+ messagesLoaded = false;
+ int size = activity.xmppConnectionService.loadMoreMessages(
+ conversation, timestamp);
+ messageList.clear();
+ messageList.addAll(conversation.getMessages());
+ updateStatusMessages();
+ messageListAdapter.notifyDataSetChanged();
+ if (size != 0) {
+ messagesLoaded = true;
+ }
+ messagesView.setSelectionFromTop(size + 1, 0);
+ }
+ }
+ };
+
+ private ConversationActivity activity;
+
+ private void sendMessage() {
+ if (this.conversation == null) {
+ return;
+ }
+ if (mEditMessage.getText().length() < 1) {
+ if (this.conversation.getMode() == Conversation.MODE_MULTI) {
+ conversation.setNextPresence(null);
+ updateChatMsgHint();
+ }
+ return;
+ }
+ Message message = new Message(conversation, mEditMessage.getText()
+ .toString(), conversation.getNextEncryption(activity
+ .forceEncryption()));
+ if (conversation.getMode() == Conversation.MODE_MULTI) {
+ if (conversation.getNextPresence() != null) {
+ message.setPresence(conversation.getNextPresence());
+ message.setType(Message.TYPE_PRIVATE);
+ conversation.setNextPresence(null);
+ }
+ }
+ if (conversation.getNextEncryption(activity.forceEncryption()) == Message.ENCRYPTION_OTR) {
+ sendOtrMessage(message);
+ } else if (conversation.getNextEncryption(activity.forceEncryption()) == Message.ENCRYPTION_PGP) {
+ sendPgpMessage(message);
+ } else {
+ sendPlainTextMessage(message);
+ }
+ }
+
+ public void updateChatMsgHint() {
+ if (conversation.getMode() == Conversation.MODE_MULTI
+ && conversation.getNextPresence() != null) {
+ this.mEditMessage.setHint(getString(
+ R.string.send_private_message_to,
+ conversation.getNextPresence()));
+ } else {
+ switch (conversation.getNextEncryption(activity.forceEncryption())) {
+ case Message.ENCRYPTION_NONE:
+ mEditMessage
+ .setHint(getString(R.string.send_plain_text_message));
+ break;
+ case Message.ENCRYPTION_OTR:
+ mEditMessage.setHint(getString(R.string.send_otr_message));
+ break;
+ case Message.ENCRYPTION_PGP:
+ mEditMessage.setHint(getString(R.string.send_pgp_message));
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ @Override
+ public View onCreateView(final LayoutInflater inflater,
+ ViewGroup container, Bundle savedInstanceState) {
+ final View view = inflater.inflate(R.layout.fragment_conversation,
+ container, false);
+ mEditMessage = (EditMessage) view.findViewById(R.id.textinput);
+ mEditMessage.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ activity.hideConversationsOverview();
+ }
+ });
+ mEditMessage.setOnEditorActionListener(mEditorActionListener);
+ mEditMessage.setOnEnterPressedListener(new OnEnterPressed() {
+
+ @Override
+ public void onEnterPressed() {
+ sendMessage();
+ }
+ });
+
+ 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.setOnScrollListener(mOnScrollListener);
+ 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.getPresence() != null) {
+ highlightInConference(message.getPresence());
+ } else {
+ highlightInConference(message
+ .getCounterpart());
+ }
+ } else {
+ Contact contact = message.getConversation()
+ .getContact();
+ if (contact.showInRoster()) {
+ activity.switchToContactDetails(contact);
+ } else {
+ activity.showAddToRosterDialog(message
+ .getConversation());
+ }
+ }
+ }
+ }
+ });
+ 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.getPresence() != null) {
+ privateMessageWith(message.getPresence());
+ } else {
+ privateMessageWith(message.getCounterpart());
+ }
+ }
+ }
+ }
+ });
+ messagesView.setAdapter(messageListAdapter);
+
+ return view;
+ }
+
+ protected void privateMessageWith(String counterpart) {
+ this.mEditMessage.setText("");
+ this.conversation.setNextPresence(counterpart);
+ updateChatMsgHint();
+ }
+
+ protected void highlightInConference(String nick) {
+ String oldString = mEditMessage.getText().toString().trim();
+ 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 onStart() {
+ super.onStart();
+ this.activity = (ConversationActivity) getActivity();
+ if (activity.xmppConnectionServiceBound) {
+ this.onBackendConnected();
+ }
+ }
+
+ @Override
+ public void onStop() {
+ mDecryptJobRunning = false;
+ super.onStop();
+ if (this.conversation != null) {
+ this.conversation.setNextMessage(mEditMessage.getText().toString());
+ }
+ }
+
+ public void onBackendConnected() {
+ this.activity = (ConversationActivity) getActivity();
+ this.conversation = activity.getSelectedConversation();
+ if (this.conversation == null) {
+ return;
+ }
+ String oldString = conversation.getNextMessage().trim();
+ if (this.pastedText == null) {
+ this.mEditMessage.setText(oldString);
+ } else {
+
+ if (oldString.isEmpty()) {
+ mEditMessage.setText(pastedText);
+ } else {
+ mEditMessage.setText(oldString + " " + pastedText);
+ }
+ pastedText = null;
+ }
+ int position = mEditMessage.length();
+ Editable etext = mEditMessage.getText();
+ Selection.setSelection(etext, position);
+ if (activity.isConversationsOverviewHideable()) {
+ if (!activity.shouldPaneBeOpen()) {
+ activity.hideConversationsOverview();
+ activity.openConversation(conversation);
+ }
+ }
+ if (this.conversation.getMode() == Conversation.MODE_MULTI) {
+ conversation.setNextPresence(null);
+ }
+ updateMessages();
+ }
+
+ public void updateMessages() {
+ if (getView() == null) {
+ return;
+ }
+ hideSnackbar();
+ final ConversationActivity activity = (ConversationActivity) getActivity();
+ if (this.conversation != null) {
+ final Contact contact = this.conversation.getContact();
+ if (this.conversation.isMuted()) {
+ showSnackbar(R.string.notifications_disabled, R.string.enable,
+ new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ conversation.setMutedTill(0);
+ activity.xmppConnectionService.databaseBackend
+ .updateConversation(conversation);
+ updateMessages();
+ }
+ });
+ } else if (!contact.showInRoster()
+ && contact
+ .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
+ showSnackbar(R.string.contact_added_you, R.string.add_back,
+ new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ activity.xmppConnectionService
+ .createContact(contact);
+ activity.switchToContactDetails(contact);
+ }
+ });
+ }
+ for (Message message : this.conversation.getMessages()) {
+ if ((message.getEncryption() == Message.ENCRYPTION_PGP)
+ && ((message.getStatus() == Message.STATUS_RECEIVED) || (message
+ .getStatus() == Message.STATUS_SEND))) {
+ if (!mEncryptedMessages.contains(message)) {
+ mEncryptedMessages.add(message);
+ }
+ }
+ }
+ decryptNext();
+ this.messageList.clear();
+ if (this.conversation.getMessages().size() == 0) {
+ messagesLoaded = false;
+ } else {
+ this.messageList.addAll(this.conversation.getMessages());
+ messagesLoaded = true;
+ updateStatusMessages();
+ }
+ this.messageListAdapter.notifyDataSetChanged();
+ if (conversation.getMode() == Conversation.MODE_SINGLE) {
+ if (messageList.size() >= 1) {
+ makeFingerprintWarning(conversation.getLatestEncryption());
+ }
+ } else {
+ if (!conversation.getMucOptions().online()
+ && conversation.getAccount().getStatus() == Account.STATUS_ONLINE) {
+ int error = conversation.getMucOptions().getError();
+ switch (error) {
+ case MucOptions.ERROR_NICK_IN_USE:
+ showSnackbar(R.string.nick_in_use, R.string.edit,
+ clickToMuc);
+ break;
+ case MucOptions.ERROR_ROOM_NOT_FOUND:
+ showSnackbar(R.string.conference_not_found,
+ R.string.leave, leaveMuc);
+ break;
+ case MucOptions.ERROR_PASSWORD_REQUIRED:
+ showSnackbar(R.string.conference_requires_password,
+ R.string.enter_password, enterPassword);
+ break;
+ case MucOptions.ERROR_BANNED:
+ showSnackbar(R.string.conference_banned,
+ R.string.leave, leaveMuc);
+ break;
+ case MucOptions.ERROR_MEMBERS_ONLY:
+ showSnackbar(R.string.conference_members_only,
+ R.string.leave, leaveMuc);
+ break;
+ case MucOptions.KICKED_FROM_ROOM:
+ showSnackbar(R.string.conference_kicked, R.string.join,
+ joinMuc);
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ getActivity().invalidateOptionsMenu();
+ updateChatMsgHint();
+ if (!activity.shouldPaneBeOpen()) {
+ activity.xmppConnectionService.markRead(conversation, true);
+ activity.updateConversationList();
+ }
+ this.updateSendButton();
+ }
+ }
+
+ private void decryptNext() {
+ Message next = this.mEncryptedMessages.peek();
+ PgpEngine engine = activity.xmppConnectionService.getPgpEngine();
+
+ if (next != null && engine != null && !mDecryptJobRunning) {
+ mDecryptJobRunning = true;
+ engine.decrypt(next, new UiCallback<Message>() {
+
+ @Override
+ public void userInputRequried(PendingIntent pi, Message message) {
+ mDecryptJobRunning = false;
+ askForPassphraseIntent = pi.getIntentSender();
+ showSnackbar(R.string.openpgp_messages_found,
+ R.string.decrypt, clickToDecryptListener);
+ }
+
+ @Override
+ public void success(Message message) {
+ mDecryptJobRunning = false;
+ mEncryptedMessages.remove();
+ activity.xmppConnectionService.updateMessage(message);
+ }
+
+ @Override
+ public void error(int error, Message message) {
+ message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
+ mDecryptJobRunning = false;
+ mEncryptedMessages.remove();
+ activity.xmppConnectionService.updateConversationUi();
+ }
+ });
+ }
+ }
+
+ private void messageSent() {
+ int size = this.messageList.size();
+ messagesView.setSelection(size - 1);
+ mEditMessage.setText("");
+ updateChatMsgHint();
+ }
+
+ public void updateSendButton() {
+ Conversation c = this.conversation;
+ if (activity.useSendButtonToIndicateStatus() && c != null
+ && c.getAccount().getStatus() == Account.STATUS_ONLINE) {
+ if (c.getMode() == Conversation.MODE_SINGLE) {
+ switch (c.getContact().getMostAvailableStatus()) {
+ case Presences.CHAT:
+ this.mSendButton
+ .setImageResource(R.drawable.ic_action_send_now_online);
+ break;
+ case Presences.ONLINE:
+ this.mSendButton
+ .setImageResource(R.drawable.ic_action_send_now_online);
+ break;
+ case Presences.AWAY:
+ this.mSendButton
+ .setImageResource(R.drawable.ic_action_send_now_away);
+ break;
+ case Presences.XA:
+ this.mSendButton
+ .setImageResource(R.drawable.ic_action_send_now_away);
+ break;
+ case Presences.DND:
+ this.mSendButton
+ .setImageResource(R.drawable.ic_action_send_now_dnd);
+ break;
+ default:
+ this.mSendButton
+ .setImageResource(R.drawable.ic_action_send_now_offline);
+ break;
+ }
+ } else if (c.getMode() == Conversation.MODE_MULTI) {
+ if (c.getMucOptions().online()) {
+ this.mSendButton
+ .setImageResource(R.drawable.ic_action_send_now_online);
+ } else {
+ this.mSendButton
+ .setImageResource(R.drawable.ic_action_send_now_offline);
+ }
+ } else {
+ this.mSendButton
+ .setImageResource(R.drawable.ic_action_send_now_offline);
+ }
+ } else {
+ this.mSendButton
+ .setImageResource(R.drawable.ic_action_send_now_offline);
+ }
+ }
+
+ protected void updateStatusMessages() {
+ if (conversation.getMode() == Conversation.MODE_SINGLE) {
+ 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));
+ return;
+ }
+ }
+ }
+ }
+ }
+
+ protected void makeFingerprintWarning(int latestEncryption) {
+ Set<String> knownFingerprints = conversation.getContact()
+ .getOtrFingerprints();
+ if ((latestEncryption == Message.ENCRYPTION_OTR)
+ && (conversation.hasValidOtrSession()
+ && (!conversation.isMuted())
+ && (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) && (!knownFingerprints
+ .contains(conversation.getOtrFingerprint())))) {
+ showSnackbar(R.string.unknown_otr_fingerprint, R.string.verify,
+ new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (conversation.getOtrFingerprint() != null) {
+ AlertDialog dialog = UIHelper
+ .getVerifyFingerprintDialog(
+ (ConversationActivity) getActivity(),
+ conversation, snackbar);
+ dialog.show();
+ }
+ }
+ });
+ }
+ }
+
+ protected void showSnackbar(int message, int action,
+ OnClickListener clickListener) {
+ snackbar.setVisibility(View.VISIBLE);
+ snackbar.setOnClickListener(null);
+ snackbarMessage.setText(message);
+ snackbarMessage.setOnClickListener(null);
+ 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()) {
+ 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) {
+
+ }
+ });
+
+ } 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();
+ }
+ });
+ }
+ }
+ } else {
+ activity.showInstallPgpDialog();
+ }
+ }
+
+ 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 sendOtrMessage(final Message message) {
+ final ConversationActivity activity = (ConversationActivity) getActivity();
+ final XmppConnectionService xmppService = activity.xmppConnectionService;
+ if (conversation.hasValidOtrSession()) {
+ activity.xmppConnectionService.sendMessage(message);
+ messageSent();
+ } else {
+ activity.selectPresence(message.getConversation(),
+ new OnPresenceSelected() {
+
+ @Override
+ public void onPresenceSelected() {
+ message.setPresence(conversation.getNextPresence());
+ xmppService.sendMessage(message);
+ messageSent();
+ }
+ });
+ }
+ }
+
+ public void setText(String text) {
+ this.pastedText = text;
+ }
+
+ public void clearInputField() {
+ this.mEditMessage.setText("");
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/conversations/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java
new file mode 100644
index 00000000..1543d740
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java
@@ -0,0 +1,423 @@
+package eu.siacs.conversations.ui;
+
+import android.app.PendingIntent;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Intent;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.TextWatcher;
+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.EditText;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
+import eu.siacs.conversations.ui.adapter.KnownHostsAdapter;
+import eu.siacs.conversations.utils.UIHelper;
+import eu.siacs.conversations.utils.Validator;
+import eu.siacs.conversations.xmpp.XmppConnection.Features;
+import eu.siacs.conversations.xmpp.pep.Avatar;
+
+public class EditAccountActivity extends XmppActivity {
+
+ private AutoCompleteTextView mAccountJid;
+ private EditText mPassword;
+ private EditText mPasswordConfirm;
+ private CheckBox mRegisterNew;
+ private Button mCancelButton;
+ private Button mSaveButton;
+
+ private LinearLayout mStats;
+ private TextView mServerInfoSm;
+ private TextView mServerInfoCarbons;
+ private TextView mServerInfoPep;
+ private TextView mSessionEst;
+ private TextView mOtrFingerprint;
+ private RelativeLayout mOtrFingerprintBox;
+ private ImageButton mOtrFingerprintToClipboardButton;
+
+ private String jidToEdit;
+ private Account mAccount;
+
+ private boolean mFetchingAvatar = false;
+
+ private OnClickListener mSaveButtonClickListener = new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (mAccount != null
+ && mAccount.getStatus() == Account.STATUS_DISABLED) {
+ mAccount.setOption(Account.OPTION_DISABLED, false);
+ xmppConnectionService.updateAccount(mAccount);
+ return;
+ }
+ if (!Validator.isValidJid(mAccountJid.getText().toString())) {
+ mAccountJid.setError(getString(R.string.invalid_jid));
+ mAccountJid.requestFocus();
+ return;
+ }
+ boolean registerNewAccount = mRegisterNew.isChecked();
+ String[] jidParts = mAccountJid.getText().toString().split("@");
+ String username = jidParts[0];
+ String server;
+ if (jidParts.length >= 2) {
+ server = jidParts[1];
+ } else {
+ server = "";
+ }
+ String password = mPassword.getText().toString();
+ 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.setPassword(password);
+ mAccount.setUsername(username);
+ mAccount.setServer(server);
+ mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount);
+ xmppConnectionService.updateAccount(mAccount);
+ } else {
+ if (xmppConnectionService.findAccountByJid(mAccountJid
+ .getText().toString()) != null) {
+ mAccountJid
+ .setError(getString(R.string.account_already_exists));
+ mAccountJid.requestFocus();
+ return;
+ }
+ mAccount = new Account(username, server, password);
+ mAccount.setOption(Account.OPTION_USETLS, true);
+ mAccount.setOption(Account.OPTION_USECOMPRESSION, true);
+ mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount);
+ xmppConnectionService.createAccount(mAccount);
+ }
+ if (jidToEdit != null) {
+ finish();
+ } else {
+ updateSaveButton();
+ updateAccountInformation();
+ }
+
+ }
+ };
+ private OnClickListener mCancelButtonClickListener = new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ finish();
+ }
+ };
+ private OnAccountUpdate mOnAccountUpdateListener = new OnAccountUpdate() {
+
+ @Override
+ public void onAccountUpdate() {
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ if (mAccount != null
+ && mAccount.getStatus() != Account.STATUS_ONLINE
+ && mFetchingAvatar) {
+ startActivity(new Intent(getApplicationContext(),
+ ManageAccountActivity.class));
+ finish();
+ } else if (jidToEdit == null && mAccount != null
+ && mAccount.getStatus() == Account.STATUS_ONLINE) {
+ if (!mFetchingAvatar) {
+ mFetchingAvatar = true;
+ xmppConnectionService.checkForAvatar(mAccount,
+ mAvatarFetchCallback);
+ }
+ } else {
+ updateSaveButton();
+ }
+ if (mAccount != null) {
+ updateAccountInformation();
+ }
+ }
+ });
+ }
+ };
+ private UiCallback<Avatar> mAvatarFetchCallback = new UiCallback<Avatar>() {
+
+ @Override
+ public void userInputRequried(PendingIntent pi, Avatar avatar) {
+ finishInitialSetup(avatar);
+ }
+
+ @Override
+ public void success(Avatar avatar) {
+ finishInitialSetup(avatar);
+ }
+
+ @Override
+ public void error(int errorCode, Avatar avatar) {
+ finishInitialSetup(avatar);
+ }
+ };
+ private KnownHostsAdapter mKnownHostsAdapter;
+ private TextWatcher mTextWatcher = new TextWatcher() {
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before,
+ int count) {
+ updateSaveButton();
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count,
+ int after) {
+
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+
+ }
+ };
+
+ protected void finishInitialSetup(final Avatar avatar) {
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ Intent intent;
+ if (avatar != null) {
+ intent = new Intent(getApplicationContext(),
+ StartConversationActivity.class);
+ } else {
+ intent = new Intent(getApplicationContext(),
+ PublishProfilePictureActivity.class);
+ intent.putExtra("account", mAccount.getJid());
+ intent.putExtra("setup", true);
+ }
+ startActivity(intent);
+ finish();
+ }
+ });
+ }
+
+ protected boolean inputDataDiffersFromAccount() {
+ if (mAccount == null) {
+ return true;
+ } else {
+ return (!mAccount.getJid().equals(mAccountJid.getText().toString()))
+ || (!mAccount.getPassword().equals(
+ mPassword.getText().toString()) || mAccount
+ .isOptionSet(Account.OPTION_REGISTER) != mRegisterNew
+ .isChecked());
+ }
+ }
+
+ protected void updateSaveButton() {
+ if (mAccount != null
+ && mAccount.getStatus() == Account.STATUS_CONNECTING) {
+ this.mSaveButton.setEnabled(false);
+ this.mSaveButton.setTextColor(getSecondaryTextColor());
+ this.mSaveButton.setText(R.string.account_status_connecting);
+ } else if (mAccount != null
+ && mAccount.getStatus() == Account.STATUS_DISABLED) {
+ this.mSaveButton.setEnabled(true);
+ this.mSaveButton.setTextColor(getPrimaryTextColor());
+ this.mSaveButton.setText(R.string.enable);
+ } else {
+ this.mSaveButton.setEnabled(true);
+ this.mSaveButton.setTextColor(getPrimaryTextColor());
+ if (jidToEdit != null) {
+ if (mAccount != null
+ && mAccount.getStatus() == Account.STATUS_ONLINE) {
+ this.mSaveButton.setText(R.string.save);
+ if (!accountInfoEdited()) {
+ this.mSaveButton.setEnabled(false);
+ this.mSaveButton.setTextColor(getSecondaryTextColor());
+ }
+ } else {
+ this.mSaveButton.setText(R.string.connect);
+ }
+ } else {
+ this.mSaveButton.setText(R.string.next);
+ }
+ }
+ }
+
+ protected boolean accountInfoEdited() {
+ return (!this.mAccount.getJid().equals(
+ this.mAccountJid.getText().toString()))
+ || (!this.mAccount.getPassword().equals(
+ this.mPassword.getText().toString()));
+ }
+
+ @Override
+ protected void onCreate(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.mPassword = (EditText) findViewById(R.id.account_password);
+ this.mPassword.addTextChangedListener(this.mTextWatcher);
+ this.mPasswordConfirm = (EditText) findViewById(R.id.account_password_confirm);
+ this.mRegisterNew = (CheckBox) findViewById(R.id.account_register_new);
+ this.mStats = (LinearLayout) findViewById(R.id.stats);
+ this.mSessionEst = (TextView) findViewById(R.id.session_est);
+ this.mServerInfoCarbons = (TextView) findViewById(R.id.server_info_carbons);
+ this.mServerInfoSm = (TextView) findViewById(R.id.server_info_sm);
+ this.mServerInfoPep = (TextView) findViewById(R.id.server_info_pep);
+ 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.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.mRegisterNew
+ .setOnCheckedChangeListener(new OnCheckedChangeListener() {
+
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView,
+ boolean isChecked) {
+ if (isChecked) {
+ mPasswordConfirm.setVisibility(View.VISIBLE);
+ } else {
+ mPasswordConfirm.setVisibility(View.GONE);
+ }
+ updateSaveButton();
+ }
+ });
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ if (getIntent() != null) {
+ this.jidToEdit = getIntent().getStringExtra("jid");
+ if (this.jidToEdit != null) {
+ this.mRegisterNew.setVisibility(View.GONE);
+ getActionBar().setTitle(jidToEdit);
+ } else {
+ getActionBar().setTitle(R.string.action_add_account);
+ }
+ }
+ }
+
+ @Override
+ protected void onStop() {
+ if (xmppConnectionServiceBound) {
+ xmppConnectionService.removeOnAccountListChangedListener();
+ }
+ super.onStop();
+ }
+
+ @Override
+ protected void onBackendConnected() {
+ this.mKnownHostsAdapter = new KnownHostsAdapter(this,
+ android.R.layout.simple_list_item_1,
+ xmppConnectionService.getKnownHosts());
+ this.xmppConnectionService
+ .setOnAccountListChangedListener(this.mOnAccountUpdateListener);
+ if (this.jidToEdit != null) {
+ this.mAccount = xmppConnectionService.findAccountByJid(jidToEdit);
+ updateAccountInformation();
+ } else if (this.xmppConnectionService.getAccounts().size() == 0) {
+ getActionBar().setDisplayHomeAsUpEnabled(false);
+ getActionBar().setDisplayShowHomeEnabled(false);
+ this.mCancelButton.setEnabled(false);
+ this.mCancelButton.setTextColor(getSecondaryTextColor());
+ }
+ this.mAccountJid.setAdapter(this.mKnownHostsAdapter);
+ updateSaveButton();
+ }
+
+ private void updateAccountInformation() {
+ this.mAccountJid.setText(this.mAccount.getJid());
+ this.mPassword.setText(this.mAccount.getPassword());
+ 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.getStatus() == Account.STATUS_ONLINE
+ && !this.mFetchingAvatar) {
+ this.mStats.setVisibility(View.VISIBLE);
+ this.mSessionEst.setText(UIHelper.readableTimeDifference(
+ getApplicationContext(), this.mAccount.getXmppConnection()
+ .getLastSessionEstablished()));
+ Features features = this.mAccount.getXmppConnection().getFeatures();
+ if (features.carbons()) {
+ this.mServerInfoCarbons.setText(R.string.server_info_available);
+ } else {
+ this.mServerInfoCarbons
+ .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.pubsub()) {
+ this.mServerInfoPep.setText(R.string.server_info_available);
+ } else {
+ this.mServerInfoPep.setText(R.string.server_info_unavailable);
+ }
+ final String fingerprint = this.mAccount
+ .getOtrFingerprint(xmppConnectionService);
+ if (fingerprint != null) {
+ this.mOtrFingerprintBox.setVisibility(View.VISIBLE);
+ this.mOtrFingerprint.setText(fingerprint);
+ this.mOtrFingerprintToClipboardButton
+ .setVisibility(View.VISIBLE);
+ this.mOtrFingerprintToClipboardButton
+ .setOnClickListener(new View.OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+
+ if (OtrFingerprintToClipBoard(fingerprint)) {
+ Toast.makeText(
+ EditAccountActivity.this,
+ R.string.toast_message_otr_fingerprint,
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+ });
+ } else {
+ this.mOtrFingerprintBox.setVisibility(View.GONE);
+ }
+ } else {
+ if (this.mAccount.errorStatus()) {
+ this.mAccountJid.setError(getString(this.mAccount
+ .getReadableStatusId()));
+ this.mAccountJid.requestFocus();
+ }
+ this.mStats.setVisibility(View.GONE);
+ }
+ }
+
+ private boolean OtrFingerprintToClipBoard(String fingerprint) {
+ ClipboardManager mClipBoardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
+ String label = getResources().getString(R.string.otr_fingerprint);
+ if (mClipBoardManager != null) {
+ ClipData mClipData = ClipData.newPlainText(label, fingerprint);
+ mClipBoardManager.setPrimaryClip(mClipData);
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/EditMessage.java b/conversations/src/main/java/eu/siacs/conversations/ui/EditMessage.java
new file mode 100644
index 00000000..f8302050
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/ui/EditMessage.java
@@ -0,0 +1,39 @@
+package eu.siacs.conversations.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.widget.EditText;
+
+public class EditMessage extends EditText {
+
+ public EditMessage(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public EditMessage(Context context) {
+ super(context);
+ }
+
+ protected OnEnterPressed mOnEnterPressed;
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_ENTER) {
+ if (mOnEnterPressed != null) {
+ mOnEnterPressed.onEnterPressed();
+ }
+ return true;
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+ public void setOnEnterPressedListener(OnEnterPressed listener) {
+ this.mOnEnterPressed = listener;
+ }
+
+ public interface OnEnterPressed {
+ public void onEnterPressed();
+ }
+
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java b/conversations/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java
new file mode 100644
index 00000000..5b5b0608
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java
@@ -0,0 +1,217 @@
+package eu.siacs.conversations.ui;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
+import eu.siacs.conversations.ui.adapter.AccountAdapter;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.ContextMenu;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ListView;
+
+public class ManageAccountActivity extends XmppActivity {
+
+ protected Account selectedAccount = null;
+
+ protected List<Account> accountList = new ArrayList<Account>();
+ protected ListView accountListView;
+ protected AccountAdapter mAccountAdapter;
+ protected OnAccountUpdate accountChanged = new OnAccountUpdate() {
+
+ @Override
+ public void onAccountUpdate() {
+ accountList.clear();
+ accountList.addAll(xmppConnectionService.getAccounts());
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ mAccountAdapter.notifyDataSetChanged();
+ }
+ });
+ }
+ };
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.manage_accounts);
+
+ 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 onCreateContextMenu(ContextMenu menu, View v,
+ ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ ManageAccountActivity.this.getMenuInflater().inflate(
+ R.menu.manageaccounts_context, menu);
+ AdapterView.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.setHeaderTitle(this.selectedAccount.getJid());
+ }
+
+ @Override
+ protected void onStop() {
+ if (xmppConnectionServiceBound) {
+ xmppConnectionService.removeOnAccountListChangedListener();
+ }
+ super.onStop();
+ }
+
+ @Override
+ void onBackendConnected() {
+ xmppConnectionService.setOnAccountListChangedListener(accountChanged);
+ this.accountList.clear();
+ this.accountList.addAll(xmppConnectionService.getAccounts());
+ mAccountAdapter.notifyDataSetChanged();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.manageaccounts, menu);
+ 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);
+ 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;
+ 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();
+ }
+ }
+
+ private void publishAvatar(Account account) {
+ Intent intent = new Intent(getApplicationContext(),
+ PublishProfilePictureActivity.class);
+ intent.putExtra("account", account.getJid());
+ startActivity(intent);
+ }
+
+ 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()) {
+ announcePgp(account, null);
+ } 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 (requestCode == REQUEST_ANNOUNCE_PGP) {
+ announcePgp(selectedAccount, null);
+ }
+ }
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java b/conversations/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java
new file mode 100644
index 00000000..6aa40c41
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java
@@ -0,0 +1,242 @@
+package eu.siacs.conversations.ui;
+
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Bundle;
+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 eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+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 = 0xac23;
+
+ 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 boolean mInitialAccountSetup;
+
+ private UiCallback<Avatar> avatarPublication = new UiCallback<Avatar>() {
+
+ @Override
+ public void success(Avatar object) {
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ if (mInitialAccountSetup) {
+ startActivity(new Intent(getApplicationContext(),
+ StartConversationActivity.class));
+ }
+ finish();
+ }
+ });
+ }
+
+ @Override
+ public void error(final int errorCode, Avatar object) {
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ hintOrWarning.setText(errorCode);
+ hintOrWarning.setTextColor(getWarningTextColor());
+ publishButton.setText(R.string.publish);
+ enablePublishButton();
+ }
+ });
+
+ }
+
+ @Override
+ public void userInputRequried(PendingIntent pi, Avatar object) {
+ }
+ };
+
+ private OnLongClickListener backToDefaultListener = new OnLongClickListener() {
+
+ @Override
+ public boolean onLongClick(View v) {
+ avatarUri = defaultUri;
+ loadImageIntoPreview(defaultUri);
+ return true;
+ }
+ };
+
+ @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) {
+ publishButton.setText(R.string.publishing);
+ disablePublishButton();
+ xmppConnectionService.publishAvatar(account, avatarUri,
+ avatarPublication);
+ }
+ }
+ });
+ this.cancelButton.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (mInitialAccountSetup) {
+ startActivity(new Intent(getApplicationContext(),
+ StartConversationActivity.class));
+ }
+ finish();
+ }
+ });
+ this.avatar.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ 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, REQUEST_CHOOSE_FILE);
+ }
+ });
+ this.defaultUri = PhoneHelper.getSefliUri(getApplicationContext());
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode,
+ final Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (resultCode == RESULT_OK) {
+ if (requestCode == REQUEST_CHOOSE_FILE) {
+ this.avatarUri = data.getData();
+ if (xmppConnectionServiceBound) {
+ loadImageIntoPreview(this.avatarUri);
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void onBackendConnected() {
+ if (getIntent() != null) {
+ String jid = getIntent().getStringExtra("account");
+ if (jid != null) {
+ this.account = xmppConnectionService.findAccountByJid(jid);
+ if (this.account.getXmppConnection() != null) {
+ this.support = this.account.getXmppConnection()
+ .getFeatures().pubsub();
+ }
+ if (this.avatarUri == null) {
+ if (this.account.getAvatar() != null
+ || this.defaultUri == null) {
+ this.avatar.setImageBitmap(avatarService().get(account,
+ getPixel(194)));
+ if (this.defaultUri != null) {
+ this.avatar
+ .setOnLongClickListener(this.backToDefaultListener);
+ } else {
+ this.secondaryHint.setVisibility(View.INVISIBLE);
+ }
+ if (!support) {
+ this.hintOrWarning
+ .setTextColor(getWarningTextColor());
+ 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);
+ }
+ this.accountTextView.setText(this.account.getJid());
+ }
+ }
+
+ }
+
+ @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 = xmppConnectionService.getFileBackend().cropCenterSquare(
+ uri, 384);
+ if (bm == null) {
+ disablePublishButton();
+ this.hintOrWarning.setTextColor(getWarningTextColor());
+ this.hintOrWarning
+ .setText(R.string.error_publish_avatar_converting);
+ return;
+ }
+ this.avatar.setImageBitmap(bm);
+ if (support) {
+ enablePublishButton();
+ this.publishButton.setText(R.string.publish);
+ this.hintOrWarning.setText(R.string.publish_avatar_explanation);
+ this.hintOrWarning.setTextColor(getPrimaryTextColor());
+ } else {
+ disablePublishButton();
+ this.hintOrWarning.setTextColor(getWarningTextColor());
+ 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);
+ }
+ }
+
+ protected void enablePublishButton() {
+ this.publishButton.setEnabled(true);
+ this.publishButton.setTextColor(getPrimaryTextColor());
+ }
+
+ protected void disablePublishButton() {
+ this.publishButton.setEnabled(false);
+ this.publishButton.setTextColor(getSecondaryTextColor());
+ }
+
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java b/conversations/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java
new file mode 100644
index 00000000..fc6308fc
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java
@@ -0,0 +1,74 @@
+package eu.siacs.conversations.ui;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Locale;
+
+import eu.siacs.conversations.entities.Account;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.os.Build;
+import android.os.Bundle;
+import android.preference.ListPreference;
+import android.preference.PreferenceManager;
+
+public class SettingsActivity extends XmppActivity implements
+ OnSharedPreferenceChangeListener {
+ private SettingsFragment mSettingsFragment;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mSettingsFragment = new SettingsFragment();
+ getFragmentManager().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<CharSequence>(
+ Arrays.asList(resources.getEntries()));
+ entries.add(0, Build.MODEL);
+ resources.setEntries(entries.toArray(new CharSequence[entries
+ .size()]));
+ resources.setEntryValues(entries.toArray(new CharSequence[entries
+ .size()]));
+ }
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ PreferenceManager.getDefaultSharedPreferences(this)
+ .unregisterOnSharedPreferenceChangeListener(this);
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences preferences,
+ String name) {
+ if (name.equals("resource")) {
+ String resource = preferences.getString("resource", "mobile")
+ .toLowerCase(Locale.US);
+ if (xmppConnectionServiceBound) {
+ for (Account account : xmppConnectionService.getAccounts()) {
+ account.setResource(resource);
+ if (!account.isOptionSet(Account.OPTION_DISABLED)) {
+ xmppConnectionService.reconnectAccount(account, false);
+ }
+ }
+ }
+ }
+ }
+
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/SettingsFragment.java b/conversations/src/main/java/eu/siacs/conversations/ui/SettingsFragment.java
new file mode 100644
index 00000000..7e1c3698
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/ui/SettingsFragment.java
@@ -0,0 +1,15 @@
+package eu.siacs.conversations.ui;
+
+import eu.siacs.conversations.R;
+import android.os.Bundle;
+import android.preference.PreferenceFragment;
+
+public class SettingsFragment extends PreferenceFragment {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Load the preferences from an XML resource
+ addPreferencesFromResource(R.xml.preferences);
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java b/conversations/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java
new file mode 100644
index 00000000..9fbc3db1
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java
@@ -0,0 +1,185 @@
+package eu.siacs.conversations.ui;
+
+import java.util.ArrayList;
+import java.util.List;
+
+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.adapter.ConversationAdapter;
+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;
+
+public class ShareWithActivity extends XmppActivity {
+
+ private class Share {
+ public Uri uri;
+ public String account;
+ public String contact;
+ public String text;
+ }
+
+ private Share share;
+
+ private static final int REQUEST_START_NEW_CONVERSATION = 0x0501;
+ private ListView mListView;
+ private List<Conversation> mConversations = new ArrayList<Conversation>();
+
+ private UiCallback<Message> attachImageCallback = new UiCallback<Message>() {
+
+ @Override
+ public void userInputRequried(PendingIntent pi, Message object) {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void success(Message message) {
+ xmppConnectionService.sendMessage(message);
+ }
+
+ @Override
+ public void error(int errorCode, Message object) {
+ // TODO Auto-generated method stub
+
+ }
+ };
+
+ 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("account");
+ Log.d(Config.LOGTAG, "contact: " + share.contact + " account:"
+ + share.account);
+ }
+ if (xmppConnectionServiceBound && share != null
+ && share.contact != null && share.account != null) {
+ share();
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+
+ super.onCreate(savedInstanceState);
+
+ 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);
+ ConversationAdapter 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) {
+ Conversation conversation = mConversations.get(position);
+ if (conversation.getMode() == Conversation.MODE_SINGLE
+ || share.uri == null) {
+ 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(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.action_add:
+ Intent intent = new Intent(getApplicationContext(),
+ ChooseContactActivity.class);
+ startActivityForResult(intent, REQUEST_START_NEW_CONVERSATION);
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void onStart() {
+ if (getIntent().getType() != null
+ && getIntent().getType().startsWith("image/")) {
+ this.share.uri = (Uri) getIntent().getParcelableExtra(
+ Intent.EXTRA_STREAM);
+ } else {
+ this.share.text = getIntent().getStringExtra(Intent.EXTRA_TEXT);
+ }
+ if (xmppConnectionServiceBound) {
+ xmppConnectionService.populateWithOrderedConversations(
+ mConversations, this.share.uri == null);
+ }
+ super.onStart();
+ }
+
+ @Override
+ void onBackendConnected() {
+ if (xmppConnectionServiceBound && share != null
+ && share.contact != null && share.account != null) {
+ share();
+ return;
+ }
+ xmppConnectionService.populateWithOrderedConversations(mConversations,
+ this.share != null && this.share.uri == null);
+ }
+
+ private void share() {
+ Account account = xmppConnectionService.findAccountByJid(share.account);
+ if (account == null) {
+ return;
+ }
+ Conversation conversation = xmppConnectionService
+ .findOrCreateConversation(account, share.contact, false);
+ share(conversation);
+ }
+
+ private void share(final Conversation conversation) {
+ if (share.uri != null) {
+ selectPresence(conversation, new OnPresenceSelected() {
+ @Override
+ public void onPresenceSelected() {
+ Toast.makeText(getApplicationContext(),
+ getText(R.string.preparing_image),
+ Toast.LENGTH_LONG).show();
+ ShareWithActivity.this.xmppConnectionService
+ .attachImageToConversation(conversation, share.uri,
+ attachImageCallback);
+ switchToConversation(conversation, null, true);
+ finish();
+ }
+ });
+
+ } else {
+ switchToConversation(conversation, this.share.text, true);
+ finish();
+ }
+
+ }
+
+} \ No newline at end of file
diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/conversations/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java
new file mode 100644
index 00000000..a1a2d4c2
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java
@@ -0,0 +1,677 @@
+package eu.siacs.conversations.ui;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import android.annotation.SuppressLint;
+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.net.Uri;
+import android.os.Bundle;
+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.EditText;
+import android.widget.ListView;
+import android.widget.Spinner;
+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.services.XmppConnectionService.OnRosterUpdate;
+import eu.siacs.conversations.ui.adapter.KnownHostsAdapter;
+import eu.siacs.conversations.ui.adapter.ListItemAdapter;
+import eu.siacs.conversations.utils.Validator;
+
+public class StartConversationActivity extends XmppActivity {
+
+ private Tab mContactsTab;
+ private Tab mConferencesTab;
+ private ViewPager mViewPager;
+
+ private MyListFragment mContactsListFragment = new MyListFragment();
+ private List<ListItem> contacts = new ArrayList<ListItem>();
+ 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 Menu mOptionsMenu;
+ private EditText mSearchEditText;
+
+ public int conference_context_id;
+ public int contact_context_id;
+
+ 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) {
+ getActionBar().setSelectedNavigationItem(position);
+ onTabChanged();
+ }
+ };
+
+ 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 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 OnRosterUpdate onRosterUpdate = new OnRosterUpdate() {
+
+ @Override
+ public void onRosterUpdate() {
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ if (mSearchEditText != null) {
+ filter(mSearchEditText.getText().toString());
+ }
+ }
+ });
+ }
+ };
+ private MenuItem mMenuSearchView;
+ private String mInitialJid;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_start_conversation);
+ 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);
+ 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 onStop() {
+ super.onStop();
+ xmppConnectionService.removeOnRosterUpdateListener();
+ }
+
+ 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);
+ Conversation conversation = xmppConnectionService
+ .findOrCreateConversation(bookmark.getAccount(),
+ bookmark.getJid(), true);
+ conversation.setBookmark(bookmark);
+ if (!conversation.getMucOptions().online()) {
+ xmppConnectionService.joinMuc(conversation);
+ }
+ if (!bookmark.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 deleteContact() {
+ int position = contact_context_id;
+ final Contact contact = (Contact) contacts.get(position);
+ 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(String prefilledJid) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(R.string.create_contact);
+ View dialogView = getLayoutInflater().inflate(
+ R.layout.create_contact_dialog, null);
+ final Spinner spinner = (Spinner) dialogView.findViewById(R.id.account);
+ final AutoCompleteTextView jid = (AutoCompleteTextView) dialogView
+ .findViewById(R.id.jid);
+ jid.setAdapter(new KnownHostsAdapter(this,
+ android.R.layout.simple_list_item_1, mKnownHosts));
+ if (prefilledJid != null) {
+ jid.append(prefilledJid);
+ }
+ populateAccountSpinner(spinner);
+ builder.setView(dialogView);
+ builder.setNegativeButton(R.string.cancel, null);
+ builder.setPositiveButton(R.string.create, null);
+ final AlertDialog dialog = builder.create();
+ dialog.show();
+ dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(
+ new View.OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (!xmppConnectionServiceBound) {
+ return;
+ }
+ if (Validator.isValidJid(jid.getText().toString())) {
+ String accountJid = (String) spinner
+ .getSelectedItem();
+ String contactJid = jid.getText().toString();
+ Account account = xmppConnectionService
+ .findAccountByJid(accountJid);
+ if (account == null) {
+ dialog.dismiss();
+ return;
+ }
+ Contact contact = account.getRoster().getContact(
+ contactJid);
+ if (contact.showInRoster()) {
+ jid.setError(getString(R.string.contact_already_exists));
+ } else {
+ xmppConnectionService.createContact(contact);
+ dialog.dismiss();
+ switchToConversation(contact);
+ }
+ } else {
+ jid.setError(getString(R.string.invalid_jid));
+ }
+ }
+ });
+
+ }
+
+ @SuppressLint("InflateParams")
+ protected void showJoinConferenceDialog() {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(R.string.join_conference);
+ 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);
+ jid.setAdapter(new KnownHostsAdapter(this,
+ android.R.layout.simple_list_item_1, mKnownConferenceHosts));
+ populateAccountSpinner(spinner);
+ final CheckBox 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(View v) {
+ if (!xmppConnectionServiceBound) {
+ return;
+ }
+ if (Validator.isValidJid(jid.getText().toString())) {
+ String accountJid = (String) spinner
+ .getSelectedItem();
+ String conferenceJid = jid.getText().toString();
+ Account account = xmppConnectionService
+ .findAccountByJid(accountJid);
+ if (account == null) {
+ dialog.dismiss();
+ return;
+ }
+ if (bookmarkCheckBox.isChecked()) {
+ if (account.hasBookmarkFor(conferenceJid)) {
+ jid.setError(getString(R.string.bookmark_already_exists));
+ } else {
+ Bookmark bookmark = new Bookmark(account,
+ conferenceJid);
+ bookmark.setAutojoin(true);
+ account.getBookmarks().add(bookmark);
+ xmppConnectionService
+ .pushBookmarks(account);
+ Conversation conversation = xmppConnectionService
+ .findOrCreateConversation(account,
+ conferenceJid, true);
+ conversation.setBookmark(bookmark);
+ if (!conversation.getMucOptions().online()) {
+ xmppConnectionService
+ .joinMuc(conversation);
+ }
+ dialog.dismiss();
+ switchToConversation(conversation);
+ }
+ } else {
+ Conversation conversation = xmppConnectionService
+ .findOrCreateConversation(account,
+ conferenceJid, true);
+ if (!conversation.getMucOptions().online()) {
+ xmppConnectionService.joinMuc(conversation);
+ }
+ dialog.dismiss();
+ switchToConversation(conversation);
+ }
+ } else {
+ jid.setError(getString(R.string.invalid_jid));
+ }
+ }
+ });
+ }
+
+ protected void switchToConversation(Contact contact) {
+ Conversation conversation = xmppConnectionService
+ .findOrCreateConversation(contact.getAccount(),
+ contact.getJid(), false);
+ switchToConversation(conversation);
+ }
+
+ private void populateAccountSpinner(Spinner spinner) {
+ ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
+ android.R.layout.simple_spinner_item, mActivatedAccounts);
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ spinner.setAdapter(adapter);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ this.mOptionsMenu = menu;
+ getMenuInflater().inflate(R.menu.start_conversation, menu);
+ MenuItem menuCreateContact = (MenuItem) menu
+ .findItem(R.id.action_create_contact);
+ MenuItem menuCreateConference = (MenuItem) menu
+ .findItem(R.id.action_join_conference);
+ mMenuSearchView = (MenuItem) 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 true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.action_create_contact:
+ showCreateContactDialog(null);
+ break;
+ case R.id.action_join_conference:
+ showJoinConferenceDialog();
+ break;
+ }
+ 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
+ protected void onBackendConnected() {
+ xmppConnectionService.setOnRosterUpdateListener(this.onRosterUpdate);
+ this.mActivatedAccounts.clear();
+ for (Account account : xmppConnectionService.getAccounts()) {
+ if (account.getStatus() != Account.STATUS_DISABLED) {
+ this.mActivatedAccounts.add(account.getJid());
+ }
+ }
+ this.mKnownHosts = xmppConnectionService.getKnownHosts();
+ this.mKnownConferenceHosts = xmppConnectionService
+ .getKnownConferenceHosts();
+ if (!startByIntent()) {
+ if (mSearchEditText != null) {
+ filter(mSearchEditText.getText().toString());
+ } else {
+ filter(null);
+ }
+ }
+ }
+
+ protected boolean startByIntent() {
+ if (getIntent() != null
+ && Intent.ACTION_SENDTO.equals(getIntent().getAction())) {
+ try {
+ String jid = URLDecoder.decode(
+ getIntent().getData().getEncodedPath(), "UTF-8").split(
+ "/")[1];
+ setIntent(null);
+ return handleJid(jid);
+ } catch (UnsupportedEncodingException e) {
+ setIntent(null);
+ return false;
+ }
+ } else if (getIntent() != null
+ && Intent.ACTION_VIEW.equals(getIntent().getAction())) {
+ Uri uri = getIntent().getData();
+ String jid = uri.getSchemeSpecificPart().split("\\?")[0];
+ return handleJid(jid);
+ }
+ return false;
+ }
+
+ private boolean handleJid(String jid) {
+ List<Contact> contacts = xmppConnectionService.findContacts(jid);
+ if (contacts.size() == 0) {
+ showCreateContactDialog(jid);
+ return false;
+ } else if (contacts.size() == 1) {
+ switchToConversation(contacts.get(0));
+ return true;
+ } else {
+ if (mMenuSearchView != null) {
+ mMenuSearchView.expandActionView();
+ mSearchEditText.setText(jid);
+ filter(jid);
+ } else {
+ mInitialJid = jid;
+ }
+ 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.STATUS_DISABLED) {
+ for (Contact contact : account.getRoster().getContacts()) {
+ if (contact.showInRoster() && contact.match(needle)) {
+ 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.STATUS_DISABLED) {
+ for (Bookmark bookmark : account.getBookmarks()) {
+ if (bookmark.match(needle)) {
+ this.conferences.add(bookmark);
+ }
+ }
+ }
+ }
+ Collections.sort(this.conferences);
+ mConferenceAdapter.notifyDataSetChanged();
+ }
+
+ private void onTabChanged() {
+ invalidateOptionsMenu();
+ }
+
+ public static class MyListFragment extends ListFragment {
+ private AdapterView.OnItemClickListener mOnItemClickListener;
+ private int mResContextMenu;
+
+ public void setContextMenu(int res) {
+ this.mResContextMenu = res;
+ }
+
+ @Override
+ public void onListItemClick(ListView l, View v, int position, long id) {
+ if (mOnItemClickListener != null) {
+ mOnItemClickListener.onItemClick(l, v, position, id);
+ }
+ }
+
+ public void setOnListItemClickListener(AdapterView.OnItemClickListener l) {
+ this.mOnItemClickListener = l;
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ registerForContextMenu(getListView());
+ getListView().setFastScrollEnabled(true);
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v,
+ ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ StartConversationActivity activity = (StartConversationActivity) getActivity();
+ activity.getMenuInflater().inflate(mResContextMenu, menu);
+ AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo;
+ if (mResContextMenu == R.menu.conference_context) {
+ activity.conference_context_id = acmi.position;
+ } else {
+ activity.contact_context_id = acmi.position;
+ }
+ }
+
+ @Override
+ public boolean onContextItemSelected(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_delete_contact:
+ activity.deleteContact();
+ break;
+ case R.id.context_join_conference:
+ activity.openConversationForBookmark();
+ break;
+ case R.id.context_delete_conference:
+ activity.deleteConference();
+ }
+ return true;
+ }
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/UiCallback.java b/conversations/src/main/java/eu/siacs/conversations/ui/UiCallback.java
new file mode 100644
index 00000000..c80199e1
--- /dev/null
+++ b/conversations/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> {
+ public void success(T object);
+
+ public void error(int errorCode, T object);
+
+ public void userInputRequried(PendingIntent pi, T object);
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/conversations/src/main/java/eu/siacs/conversations/ui/XmppActivity.java
new file mode 100644
index 00000000..d26f0e31
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/ui/XmppActivity.java
@@ -0,0 +1,637 @@
+package eu.siacs.conversations.ui;
+
+import java.io.FileNotFoundException;
+import java.lang.ref.WeakReference;
+import java.util.List;
+import java.util.concurrent.RejectedExecutionException;
+
+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.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.Presences;
+import eu.siacs.conversations.services.AvatarService;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder;
+import eu.siacs.conversations.utils.ExceptionHelper;
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.PendingIntent;
+import android.app.AlertDialog.Builder;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.SharedPreferences;
+import android.content.DialogInterface.OnClickListener;
+import android.content.IntentSender.SendIntentException;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.preference.PreferenceManager;
+import android.text.InputType;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+import android.widget.ImageView;
+
+public abstract class XmppActivity extends Activity {
+
+ protected static final int REQUEST_ANNOUNCE_PGP = 0x0101;
+ protected static final int REQUEST_INVITE_TO_CONVERSATION = 0x0102;
+
+ public XmppConnectionService xmppConnectionService;
+ public boolean xmppConnectionServiceBound = false;
+ protected boolean handledViewIntent = false;
+
+ protected int mPrimaryTextColor;
+ protected int mSecondaryTextColor;
+ protected int mSecondaryBackgroundColor;
+ protected int mColorRed;
+ protected int mColorOrange;
+ protected int mColorGreen;
+ protected int mPrimaryColor;
+
+ protected boolean mUseSubject = true;
+
+ private DisplayMetrics metrics;
+
+ 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;
+ onBackendConnected();
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName arg0) {
+ xmppConnectionServiceBound = false;
+ }
+ };
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ if (!xmppConnectionServiceBound) {
+ connectToBackend();
+ }
+ }
+
+ 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) {
+ 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();
+
+ public boolean onOptionsItemSelected(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;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ metrics = getResources().getDisplayMetrics();
+ ExceptionHelper.init(getApplicationContext());
+ mPrimaryTextColor = getResources().getColor(R.color.primarytext);
+ mSecondaryTextColor = getResources().getColor(R.color.secondarytext);
+ mColorRed = getResources().getColor(R.color.red);
+ mColorOrange = getResources().getColor(R.color.orange);
+ mColorGreen = getResources().getColor(R.color.green);
+ mPrimaryColor = getResources().getColor(R.color.primary);
+ mSecondaryBackgroundColor = getResources().getColor(
+ R.color.secondarybackground);
+ if (getPreferences().getBoolean("use_larger_font", false)) {
+ setTheme(R.style.ConversationsTheme_LargerText);
+ }
+ mUseSubject = getPreferences().getBoolean("use_subject", true);
+ }
+
+ protected SharedPreferences getPreferences() {
+ return PreferenceManager
+ .getDefaultSharedPreferences(getApplicationContext());
+ }
+
+ public boolean useSubjectToIdentifyConference() {
+ return mUseSubject;
+ }
+
+ public void switchToConversation(Conversation conversation) {
+ switchToConversation(conversation, null, false);
+ }
+
+ public void switchToConversation(Conversation conversation, String text,
+ 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);
+ }
+ 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) {
+ Intent intent = new Intent(this, ContactDetailsActivity.class);
+ intent.setAction(ContactDetailsActivity.ACTION_VIEW_CONTACT);
+ intent.putExtra("account", contact.getAccount().getJid());
+ intent.putExtra("contact", contact.getJid());
+ startActivity(intent);
+ }
+
+ public void switchToAccount(Account account) {
+ Intent intent = new Intent(this, EditAccountActivity.class);
+ intent.putExtra("jid", account.getJid());
+ startActivity(intent);
+ }
+
+ protected void inviteToConversation(Conversation conversation) {
+ Intent intent = new Intent(getApplicationContext(),
+ ChooseContactActivity.class);
+ intent.putExtra("conversation", conversation.getUuid());
+ startActivityForResult(intent, REQUEST_INVITE_TO_CONVERSATION);
+ }
+
+ protected void announcePgp(Account account, final Conversation conversation) {
+ xmppConnectionService.getPgpEngine().generateSignature(account,
+ "online", new UiCallback<Account>() {
+
+ @Override
+ public void userInputRequried(PendingIntent pi,
+ Account account) {
+ try {
+ startIntentSenderForResult(pi.getIntentSender(),
+ REQUEST_ANNOUNCE_PGP, null, 0, 0, 0);
+ } catch (SendIntentException e) {
+ }
+ }
+
+ @Override
+ public void success(Account account) {
+ xmppConnectionService.databaseBackend
+ .updateAccount(account);
+ xmppConnectionService.sendPresencePacket(account,
+ xmppConnectionService.getPresenceGenerator()
+ .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 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) {
+ String jid = conversation.getContactJid();
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(jid);
+ 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) {
+ String jid = conversation.getContactJid();
+ Account account = conversation.getAccount();
+ Contact contact = account.getRoster().getContact(jid);
+ xmppConnectionService.createContact(contact);
+ switchToContactDetails(contact);
+ }
+ });
+ builder.create().show();
+ }
+
+ private void showAskForPresenceDialog(final Contact contact) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(contact.getJid());
+ 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());
+ 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.setNextPresence(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 = (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();
+ }
+
+ public void selectPresence(final Conversation conversation,
+ final OnPresenceSelected listener) {
+ Contact contact = conversation.getContact();
+ 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.STATUS_ONLINE) {
+ showAskForPresenceDialog(contact);
+ } else if (!contact.getOption(Contact.Options.TO)
+ || !contact.getOption(Contact.Options.FROM)) {
+ warnMutalPresenceSubscription(conversation, listener);
+ } else {
+ conversation.setNextPresence(null);
+ listener.onPresenceSelected();
+ }
+ } else if (presences.size() == 1) {
+ String presence = (String) presences.asStringArray()[0];
+ conversation.setNextPresence(presence);
+ 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) {
+ conversation.setNextPresence(presence.toString());
+ 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) {
+ String contactJid = data.getStringExtra("contact");
+ String conversationUuid = data.getStringExtra("conversation");
+ Conversation conversation = xmppConnectionService
+ .findConversationByUuid(conversationUuid);
+ if (conversation.getMode() == Conversation.MODE_MULTI) {
+ xmppConnectionService.invite(conversation, contactJid);
+ }
+ Log.d(Config.LOGTAG, "inviting " + contactJid + " to "
+ + conversation.getName());
+ }
+ }
+
+ public int getSecondaryTextColor() {
+ return this.mSecondaryTextColor;
+ }
+
+ public int getPrimaryTextColor() {
+ return this.mPrimaryTextColor;
+ }
+
+ public int getWarningTextColor() {
+ return this.mColorRed;
+ }
+
+ public int getPrimaryColor() {
+ return this.mPrimaryColor;
+ }
+
+ public int getSecondaryBackgroundColor() {
+ return this.mSecondaryBackgroundColor;
+ }
+
+ public int getPixel(int dp) {
+ DisplayMetrics metrics = getResources().getDisplayMetrics();
+ return ((int) (dp * metrics.density));
+ }
+
+ public AvatarService avatarService() {
+ return xmppConnectionService.getAvatarService();
+ }
+
+ class BitmapWorkerTask extends AsyncTask<Message, Void, Bitmap> {
+ private final WeakReference<ImageView> imageViewReference;
+ private Message message = null;
+
+ public BitmapWorkerTask(ImageView imageView) {
+ imageViewReference = new WeakReference<ImageView>(imageView);
+ }
+
+ @Override
+ protected Bitmap doInBackground(Message... params) {
+ message = params[0];
+ try {
+ return xmppConnectionService.getFileBackend().getThumbnail(
+ message, (int) (metrics.density * 288), false);
+ } catch (FileNotFoundException e) {
+ return null;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(Bitmap bitmap) {
+ if (imageViewReference != null && bitmap != null) {
+ final ImageView imageView = imageViewReference.get();
+ if (imageView != null) {
+ imageView.setImageBitmap(bitmap);
+ imageView.setBackgroundColor(0x00000000);
+ }
+ }
+ }
+ }
+
+ public void loadBitmap(Message message, ImageView imageView) {
+ Bitmap bm;
+ try {
+ bm = xmppConnectionService.getFileBackend().getThumbnail(message,
+ (int) (metrics.density * 288), true);
+ } catch (FileNotFoundException e) {
+ bm = null;
+ }
+ if (bm != null) {
+ imageView.setImageBitmap(bm);
+ imageView.setBackgroundColor(0x00000000);
+ } else {
+ if (cancelPotentialWork(message, imageView)) {
+ imageView.setBackgroundColor(0xff333333);
+ final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
+ final AsyncDrawable asyncDrawable = new AsyncDrawable(
+ getResources(), null, task);
+ imageView.setImageDrawable(asyncDrawable);
+ try {
+ task.execute(message);
+ } catch (RejectedExecutionException e) {
+ return;
+ }
+ }
+ }
+ }
+
+ 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>(
+ bitmapWorkerTask);
+ }
+
+ public BitmapWorkerTask getBitmapWorkerTask() {
+ return bitmapWorkerTaskReference.get();
+ }
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java b/conversations/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java
new file mode 100644
index 00000000..4ca21a3b
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java
@@ -0,0 +1,102 @@
+package eu.siacs.conversations.ui.adapter;
+
+import java.util.List;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.ui.XmppActivity;
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+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) {
+ Account account = getItem(position);
+ if (view == null) {
+ LayoutInflater inflater = (LayoutInflater) getContext()
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ view = (View) inflater.inflate(R.layout.account_row, parent, false);
+ }
+ TextView jid = (TextView) view.findViewById(R.id.account_jid);
+ jid.setText(account.getJid());
+ TextView statusView = (TextView) view.findViewById(R.id.account_status);
+ ImageView imageView = (ImageView) view.findViewById(R.id.account_image);
+ imageView.setImageBitmap(activity.avatarService().get(account,
+ activity.getPixel(48)));
+ switch (account.getStatus()) {
+ case Account.STATUS_DISABLED:
+ statusView.setText(getContext().getString(
+ R.string.account_status_disabled));
+ statusView.setTextColor(activity.getSecondaryTextColor());
+ break;
+ case Account.STATUS_ONLINE:
+ statusView.setText(getContext().getString(
+ R.string.account_status_online));
+ statusView.setTextColor(activity.getPrimaryColor());
+ break;
+ case Account.STATUS_CONNECTING:
+ statusView.setText(getContext().getString(
+ R.string.account_status_connecting));
+ statusView.setTextColor(activity.getSecondaryTextColor());
+ break;
+ case Account.STATUS_OFFLINE:
+ statusView.setText(getContext().getString(
+ R.string.account_status_offline));
+ statusView.setTextColor(activity.getWarningTextColor());
+ break;
+ case Account.STATUS_UNAUTHORIZED:
+ statusView.setText(getContext().getString(
+ R.string.account_status_unauthorized));
+ statusView.setTextColor(activity.getWarningTextColor());
+ break;
+ case Account.STATUS_SERVER_NOT_FOUND:
+ statusView.setText(getContext().getString(
+ R.string.account_status_not_found));
+ statusView.setTextColor(activity.getWarningTextColor());
+ break;
+ case Account.STATUS_NO_INTERNET:
+ statusView.setText(getContext().getString(
+ R.string.account_status_no_internet));
+ statusView.setTextColor(activity.getWarningTextColor());
+ break;
+ case Account.STATUS_REGISTRATION_FAILED:
+ statusView.setText(getContext().getString(
+ R.string.account_status_regis_fail));
+ statusView.setTextColor(activity.getWarningTextColor());
+ break;
+ case Account.STATUS_REGISTRATION_CONFLICT:
+ statusView.setText(getContext().getString(
+ R.string.account_status_regis_conflict));
+ statusView.setTextColor(activity.getWarningTextColor());
+ break;
+ case Account.STATUS_REGISTRATION_SUCCESSFULL:
+ statusView.setText(getContext().getString(
+ R.string.account_status_regis_success));
+ statusView.setTextColor(activity.getSecondaryTextColor());
+ break;
+ case Account.STATUS_REGISTRATION_NOT_SUPPORTED:
+ statusView.setText(getContext().getString(
+ R.string.account_status_regis_not_sup));
+ statusView.setTextColor(activity.getWarningTextColor());
+ break;
+ default:
+ statusView.setText("");
+ break;
+ }
+
+ return view;
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java b/conversations/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java
new file mode 100644
index 00000000..183c89fa
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java
@@ -0,0 +1,135 @@
+package eu.siacs.conversations.ui.adapter;
+
+import java.util.List;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Downloadable;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.ui.ConversationActivity;
+import eu.siacs.conversations.ui.XmppActivity;
+import eu.siacs.conversations.utils.UIHelper;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Typeface;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+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 = (View) inflater.inflate(R.layout.conversation_list_row,
+ parent, false);
+ }
+ Conversation conversation = getItem(position);
+ if (this.activity instanceof ConversationActivity) {
+ ConversationActivity activity = (ConversationActivity) this.activity;
+ if (!activity.isConversationsOverviewHideable()) {
+ if (conversation == activity.getSelectedConversation()) {
+ view.setBackgroundColor(activity
+ .getSecondaryBackgroundColor());
+ } else {
+ view.setBackgroundColor(Color.TRANSPARENT);
+ }
+ } else {
+ view.setBackgroundColor(Color.TRANSPARENT);
+ }
+ }
+ TextView convName = (TextView) view
+ .findViewById(R.id.conversation_name);
+ if (conversation.getMode() == Conversation.MODE_SINGLE
+ || activity.useSubjectToIdentifyConference()) {
+ convName.setText(conversation.getName());
+ } else {
+ convName.setText(conversation.getContactJid().split("/")[0]);
+ }
+ 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);
+
+ Message message = conversation.getLatestMessage();
+
+ if (!conversation.isRead()) {
+ convName.setTypeface(null, Typeface.BOLD);
+ } else {
+ convName.setTypeface(null, Typeface.NORMAL);
+ }
+
+ if (message.getType() == Message.TYPE_IMAGE
+ || message.getDownloadable() != null) {
+ Downloadable d = message.getDownloadable();
+ if (d != null) {
+ mLastMessage.setVisibility(View.VISIBLE);
+ imagePreview.setVisibility(View.GONE);
+ if (conversation.isRead()) {
+ mLastMessage.setTypeface(null, Typeface.ITALIC);
+ } else {
+ mLastMessage.setTypeface(null, Typeface.BOLD_ITALIC);
+ }
+ if (d.getStatus() == Downloadable.STATUS_CHECKING) {
+ mLastMessage.setText(R.string.checking_image);
+ } else if (d.getStatus() == Downloadable.STATUS_DOWNLOADING) {
+ mLastMessage.setText(R.string.receiving_image);
+ } else if (d.getStatus() == Downloadable.STATUS_OFFER) {
+ mLastMessage.setText(R.string.image_offered_for_download);
+ } else if (d.getStatus() == Downloadable.STATUS_OFFER_CHECK_FILESIZE) {
+ mLastMessage.setText(R.string.image_offered_for_download);
+ } else if (d.getStatus() == Downloadable.STATUS_DELETED) {
+ mLastMessage.setText(R.string.image_file_deleted);
+ } else {
+ mLastMessage.setText("");
+ }
+ } else {
+ mLastMessage.setVisibility(View.GONE);
+ imagePreview.setVisibility(View.VISIBLE);
+ activity.loadBitmap(message, imagePreview);
+ }
+ } else {
+ if ((message.getEncryption() != Message.ENCRYPTION_PGP)
+ && (message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED)) {
+ String body = Config.PARSE_EMOTICONS ? UIHelper
+ .transformAsciiEmoticons(message.getBody()) : message
+ .getBody();
+ mLastMessage.setText(body);
+ } else {
+ mLastMessage.setText(R.string.encrypted_message_received);
+ }
+ if (!conversation.isRead()) {
+ mLastMessage.setTypeface(null, Typeface.BOLD);
+ } else {
+ mLastMessage.setTypeface(null, Typeface.NORMAL);
+ }
+ mLastMessage.setVisibility(View.VISIBLE);
+ imagePreview.setVisibility(View.GONE);
+ }
+ mTimestamp.setText(UIHelper.readableTimeDifference(getContext(),
+ conversation.getLatestMessage().getTimeSent()));
+
+ ImageView profilePicture = (ImageView) view
+ .findViewById(R.id.conversation_image);
+ profilePicture.setImageBitmap(activity.avatarService().get(
+ conversation, activity.getPixel(56)));
+
+ return view;
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java b/conversations/src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java
new file mode 100644
index 00000000..143dfda1
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java
@@ -0,0 +1,74 @@
+package eu.siacs.conversations.ui.adapter;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+import android.content.Context;
+import android.widget.ArrayAdapter;
+import android.widget.Filter;
+
+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<String>();
+ 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, mKnownHosts);
+ domains = new ArrayList<String>(mKnownHosts.size());
+ for (String domain : mKnownHosts) {
+ domains.add(new String(domain));
+ }
+ }
+
+ @Override
+ public Filter getFilter() {
+ return domainFilter;
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java b/conversations/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java
new file mode 100644
index 00000000..977aa7b5
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java
@@ -0,0 +1,44 @@
+package eu.siacs.conversations.ui.adapter;
+
+import java.util.List;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.ListItem;
+import eu.siacs.conversations.ui.XmppActivity;
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+public class ListItemAdapter extends ArrayAdapter<ListItem> {
+
+ protected XmppActivity activity;
+
+ public ListItemAdapter(XmppActivity activity, List<ListItem> objects) {
+ super(activity, 0, objects);
+ this.activity = activity;
+ }
+
+ @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 = (View) inflater.inflate(R.layout.contact, parent, false);
+ }
+ TextView name = (TextView) view.findViewById(R.id.contact_display_name);
+ TextView jid = (TextView) view.findViewById(R.id.contact_jid);
+ ImageView picture = (ImageView) view.findViewById(R.id.contact_photo);
+
+ jid.setText(item.getJid());
+ name.setText(item.getDisplayName());
+ picture.setImageBitmap(activity.avatarService().get(item,
+ activity.getPixel(48)));
+ return view;
+ }
+
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/conversations/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java
new file mode 100644
index 00000000..a9a55cbf
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java
@@ -0,0 +1,560 @@
+package eu.siacs.conversations.ui.adapter;
+
+import java.util.List;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Downloadable;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.Message.ImageParams;
+import eu.siacs.conversations.ui.ConversationActivity;
+import eu.siacs.conversations.utils.UIHelper;
+import android.content.Intent;
+import android.graphics.Typeface;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.StyleSpan;
+import android.util.DisplayMetrics;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.View.OnClickListener;
+import android.view.View.OnLongClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+
+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 ConversationActivity activity;
+
+ private DisplayMetrics metrics;
+
+ private OnContactPictureClicked mOnContactPictureClickedListener;
+ private OnContactPictureLongClicked mOnContactPictureLongClickedListener;
+
+ public MessageAdapter(ConversationActivity activity, List<Message> messages) {
+ super(activity, 0, messages);
+ this.activity = activity;
+ metrics = getContext().getResources().getDisplayMetrics();
+ }
+
+ public void setOnContactPictureClicked(OnContactPictureClicked listener) {
+ this.mOnContactPictureClickedListener = listener;
+ }
+
+ public void setOnContactPictureLongClicked(
+ OnContactPictureLongClicked listener) {
+ this.mOnContactPictureLongClickedListener = listener;
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return 4;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ if (getItem(position).wasMergedIntoPrevious()) {
+ return NULL;
+ } else if (getItem(position).getType() == Message.TYPE_STATUS) {
+ return STATUS;
+ } else if (getItem(position).getStatus() <= Message.STATUS_RECEIVED) {
+ return RECEIVED;
+ } else {
+ return SENT;
+ }
+ }
+
+ private void displayStatus(ViewHolder viewHolder, Message message) {
+ 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.getMergedStatus() <= Message.STATUS_RECEIVED;
+ if (message.getType() == Message.TYPE_IMAGE
+ || message.getDownloadable() != null) {
+ ImageParams params = message.getImageParams();
+ if (params.size != 0) {
+ filesize = params.size / 1024 + " KB";
+ }
+ }
+ switch (message.getMergedStatus()) {
+ case Message.STATUS_WAITING:
+ info = getContext().getString(R.string.waiting);
+ break;
+ case Message.STATUS_UNSEND:
+ info = getContext().getString(R.string.sending);
+ break;
+ case Message.STATUS_OFFERED:
+ info = getContext().getString(R.string.offering);
+ break;
+ case Message.STATUS_SEND_RECEIVED:
+ if (activity.indicateReceived()) {
+ viewHolder.indicatorReceived.setVisibility(View.VISIBLE);
+ }
+ break;
+ case Message.STATUS_SEND_DISPLAYED:
+ if (activity.indicateReceived()) {
+ viewHolder.indicatorReceived.setVisibility(View.VISIBLE);
+ }
+ break;
+ case Message.STATUS_SEND_FAILED:
+ info = getContext().getString(R.string.send_failed);
+ error = true;
+ break;
+ case Message.STATUS_SEND_REJECTED:
+ info = getContext().getString(R.string.send_rejected);
+ error = true;
+ break;
+ default:
+ if (multiReceived) {
+ Contact contact = message.getContact();
+ if (contact != null) {
+ info = contact.getDisplayName();
+ } else {
+ if (message.getPresence() != null) {
+ info = message.getPresence();
+ } else {
+ info = message.getCounterpart();
+ }
+ }
+ }
+ break;
+ }
+ if (error) {
+ viewHolder.time.setTextColor(activity.getWarningTextColor());
+ } else {
+ viewHolder.time.setTextColor(activity.getSecondaryTextColor());
+ }
+ if (message.getEncryption() == Message.ENCRYPTION_NONE) {
+ viewHolder.indicator.setVisibility(View.GONE);
+ } else {
+ viewHolder.indicator.setVisibility(View.VISIBLE);
+ }
+
+ String formatedTime = UIHelper.readableTimeDifferenceFull(getContext(),
+ message.getMergedTimeSent());
+ if (message.getStatus() <= Message.STATUS_RECEIVED) {
+ if ((filesize != null) && (info != null)) {
+ viewHolder.time.setText(filesize + " \u00B7 " + info);
+ } else if ((filesize == null) && (info != null)) {
+ viewHolder.time.setText(formatedTime + " \u00B7 " + info);
+ } else if ((filesize != null) && (info == null)) {
+ viewHolder.time.setText(formatedTime + " \u00B7 " + filesize);
+ } else {
+ viewHolder.time.setText(formatedTime);
+ }
+ } 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);
+ }
+ }
+ }
+
+ private void displayInfoMessage(ViewHolder viewHolder, int r) {
+ 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));
+ viewHolder.messageBody.setTextColor(activity.getSecondaryTextColor());
+ viewHolder.messageBody.setTypeface(null, Typeface.ITALIC);
+ viewHolder.messageBody.setTextIsSelectable(false);
+ }
+
+ private void displayDecryptionFailed(ViewHolder viewHolder) {
+ 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(activity.getWarningTextColor());
+ viewHolder.messageBody.setTypeface(null, Typeface.NORMAL);
+ viewHolder.messageBody.setTextIsSelectable(false);
+ }
+
+ private void displayTextMessage(ViewHolder viewHolder, Message message) {
+ if (viewHolder.download_button != null) {
+ viewHolder.download_button.setVisibility(View.GONE);
+ }
+ viewHolder.image.setVisibility(View.GONE);
+ viewHolder.messageBody.setVisibility(View.VISIBLE);
+ if (message.getBody() != null) {
+ if (message.getType() != Message.TYPE_PRIVATE) {
+ String body = Config.PARSE_EMOTICONS ? UIHelper
+ .transformAsciiEmoticons(message.getMergedBody())
+ : message.getMergedBody();
+ viewHolder.messageBody.setText(body);
+ } else {
+ String privateMarker;
+ if (message.getStatus() <= Message.STATUS_RECEIVED) {
+ privateMarker = activity
+ .getString(R.string.private_message);
+ } else {
+ String to;
+ if (message.getPresence() != null) {
+ to = message.getPresence();
+ } else {
+ to = message.getCounterpart();
+ }
+ privateMarker = activity.getString(
+ R.string.private_message_to, to);
+ }
+ SpannableString span = new SpannableString(privateMarker + " "
+ + message.getBody());
+ span.setSpan(
+ new ForegroundColorSpan(activity
+ .getSecondaryTextColor()), 0, privateMarker
+ .length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ span.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), 0,
+ privateMarker.length(),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ viewHolder.messageBody.setText(span);
+ }
+ } else {
+ viewHolder.messageBody.setText("");
+ }
+ viewHolder.messageBody.setTextColor(activity.getPrimaryTextColor());
+ viewHolder.messageBody.setTypeface(null, Typeface.NORMAL);
+ viewHolder.messageBody.setTextIsSelectable(true);
+ }
+
+ private void displayDownloadableMessage(ViewHolder viewHolder,
+ final Message message, int resid) {
+ viewHolder.image.setVisibility(View.GONE);
+ viewHolder.messageBody.setVisibility(View.GONE);
+ viewHolder.download_button.setVisibility(View.VISIBLE);
+ viewHolder.download_button.setText(resid);
+ viewHolder.download_button.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ startDonwloadable(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);
+ ImageParams params = message.getImageParams();
+ 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));
+ }
+ viewHolder.image.setLayoutParams(new LinearLayout.LayoutParams(
+ scalledW, scalledH));
+ activity.loadBitmap(message, viewHolder.image);
+ viewHolder.image.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setDataAndType(activity.xmppConnectionService
+ .getFileBackend().getJingleFileUri(message), "image/*");
+ getContext().startActivity(intent);
+ }
+ });
+ viewHolder.image.setOnLongClickListener(new OnLongClickListener() {
+
+ @Override
+ public boolean onLongClick(View v) {
+ Intent shareIntent = new Intent();
+ shareIntent.setAction(Intent.ACTION_SEND);
+ shareIntent.putExtra(Intent.EXTRA_STREAM,
+ activity.xmppConnectionService.getFileBackend()
+ .getJingleFileUri(message));
+ shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ shareIntent.setType("image/webp");
+ getContext().startActivity(
+ Intent.createChooser(shareIntent,
+ getContext().getText(R.string.share_with)));
+ return true;
+ }
+ });
+ }
+
+ @Override
+ public View getView(int position, View view, ViewGroup parent) {
+ final Message item = getItem(position);
+ int type = getItemViewType(position);
+ ViewHolder viewHolder;
+ if (view == null) {
+ viewHolder = new ViewHolder();
+ switch (type) {
+ case NULL:
+ view = (View) activity.getLayoutInflater().inflate(
+ R.layout.message_null, parent, false);
+ break;
+ case SENT:
+ view = (View) activity.getLayoutInflater().inflate(
+ R.layout.message_sent, parent, false);
+ viewHolder.message_box = (LinearLayout) view
+ .findViewById(R.id.message_box);
+ viewHolder.contact_picture = (ImageView) view
+ .findViewById(R.id.message_photo);
+ viewHolder.contact_picture.setImageBitmap(activity
+ .avatarService().get(
+ item.getConversation().getAccount(),
+ activity.getPixel(48)));
+ viewHolder.download_button = (Button) view
+ .findViewById(R.id.download_button);
+ viewHolder.indicator = (ImageView) view
+ .findViewById(R.id.security_indicator);
+ viewHolder.image = (ImageView) view
+ .findViewById(R.id.message_image);
+ 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);
+ view.setTag(viewHolder);
+ break;
+ case RECEIVED:
+ view = (View) activity.getLayoutInflater().inflate(
+ R.layout.message_received, parent, false);
+ viewHolder.message_box = (LinearLayout) view
+ .findViewById(R.id.message_box);
+ viewHolder.contact_picture = (ImageView) view
+ .findViewById(R.id.message_photo);
+ viewHolder.download_button = (Button) view
+ .findViewById(R.id.download_button);
+ if (item.getConversation().getMode() == Conversation.MODE_SINGLE) {
+ viewHolder.contact_picture.setImageBitmap(activity
+ .avatarService().get(item.getContact(),
+ activity.getPixel(48)));
+ }
+ viewHolder.indicator = (ImageView) view
+ .findViewById(R.id.security_indicator);
+ viewHolder.image = (ImageView) view
+ .findViewById(R.id.message_image);
+ viewHolder.messageBody = (TextView) view
+ .findViewById(R.id.message_body);
+ viewHolder.time = (TextView) view
+ .findViewById(R.id.message_time);
+ view.setTag(viewHolder);
+ break;
+ case STATUS:
+ view = (View) activity.getLayoutInflater().inflate(
+ R.layout.message_status, parent, false);
+ viewHolder.contact_picture = (ImageView) view
+ .findViewById(R.id.message_photo);
+ if (item.getConversation().getMode() == Conversation.MODE_SINGLE) {
+
+ viewHolder.contact_picture.setImageBitmap(activity
+ .avatarService().get(
+ item.getConversation().getContact(),
+ activity.getPixel(32)));
+ viewHolder.contact_picture.setAlpha(0.5f);
+ viewHolder.contact_picture
+ .setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ String name = item.getConversation()
+ .getName();
+ String read = getContext()
+ .getString(
+ R.string.contact_has_read_up_to_this_point,
+ name);
+ Toast.makeText(getContext(), read,
+ Toast.LENGTH_SHORT).show();
+ }
+ });
+
+ }
+ break;
+ default:
+ viewHolder = null;
+ break;
+ }
+ } else {
+ viewHolder = (ViewHolder) view.getTag();
+ }
+
+ if (type == STATUS) {
+ return view;
+ }
+ if (type == NULL) {
+ if (position == getCount() - 1) {
+ view.getLayoutParams().height = 1;
+ } else {
+ view.getLayoutParams().height = 0;
+
+ }
+ view.setLayoutParams(view.getLayoutParams());
+ return view;
+ }
+
+ if (viewHolder.contact_picture != null) {
+ viewHolder.contact_picture
+ .setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (MessageAdapter.this.mOnContactPictureClickedListener != null) {
+ MessageAdapter.this.mOnContactPictureClickedListener
+ .onContactPictureClicked(item);
+ ;
+ }
+
+ }
+ });
+ viewHolder.contact_picture
+ .setOnLongClickListener(new OnLongClickListener() {
+
+ @Override
+ public boolean onLongClick(View v) {
+ if (MessageAdapter.this.mOnContactPictureLongClickedListener != null) {
+ MessageAdapter.this.mOnContactPictureLongClickedListener
+ .onContactPictureLongClicked(item);
+ return true;
+ } else {
+ return false;
+ }
+ }
+ });
+ }
+
+ if (type == RECEIVED) {
+ if (item.getConversation().getMode() == Conversation.MODE_MULTI) {
+ Contact contact = item.getContact();
+ if (contact != null) {
+ viewHolder.contact_picture.setImageBitmap(activity
+ .avatarService()
+ .get(contact, activity.getPixel(48)));
+ } else {
+ String name = item.getPresence();
+ if (name == null) {
+ name = item.getCounterpart();
+ }
+ viewHolder.contact_picture.setImageBitmap(activity
+ .avatarService().get(name, activity.getPixel(48)));
+ }
+ }
+ }
+
+ if (item.getType() == Message.TYPE_IMAGE
+ || item.getDownloadable() != null) {
+ Downloadable d = item.getDownloadable();
+ if (d != null && d.getStatus() == Downloadable.STATUS_DOWNLOADING) {
+ displayInfoMessage(viewHolder, R.string.receiving_image);
+ } else if (d != null
+ && d.getStatus() == Downloadable.STATUS_CHECKING) {
+ displayInfoMessage(viewHolder, R.string.checking_image);
+ } else if (d != null
+ && d.getStatus() == Downloadable.STATUS_DELETED) {
+ displayInfoMessage(viewHolder, R.string.image_file_deleted);
+ } else if (d != null && d.getStatus() == Downloadable.STATUS_OFFER) {
+ displayDownloadableMessage(viewHolder, item,
+ R.string.download_image);
+ } else if (d != null
+ && d.getStatus() == Downloadable.STATUS_OFFER_CHECK_FILESIZE) {
+ displayDownloadableMessage(viewHolder, item,
+ R.string.check_image_filesize);
+ } else if ((item.getEncryption() == Message.ENCRYPTION_DECRYPTED)
+ || (item.getEncryption() == Message.ENCRYPTION_NONE)
+ || (item.getEncryption() == Message.ENCRYPTION_OTR)) {
+ displayImageMessage(viewHolder, item);
+ } else if (item.getEncryption() == Message.ENCRYPTION_PGP) {
+ displayInfoMessage(viewHolder, R.string.encrypted_message);
+ } else {
+ displayDecryptionFailed(viewHolder);
+ }
+ } else {
+ if (item.getEncryption() == Message.ENCRYPTION_PGP) {
+ if (activity.hasPgp()) {
+ displayInfoMessage(viewHolder, R.string.encrypted_message);
+ } else {
+ displayInfoMessage(viewHolder,
+ R.string.install_openkeychain);
+ viewHolder.message_box
+ .setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ activity.showInstallPgpDialog();
+ }
+ });
+ }
+ } else if (item.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) {
+ displayDecryptionFailed(viewHolder);
+ } else {
+ displayTextMessage(viewHolder, item);
+ }
+ }
+
+ displayStatus(viewHolder, item);
+
+ return view;
+ }
+
+ public void startDonwloadable(Message message) {
+ Downloadable downloadable = message.getDownloadable();
+ if (downloadable != null) {
+ if (!downloadable.start()) {
+ Toast.makeText(activity, R.string.not_connected_try_again,
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+ }
+
+ private static class ViewHolder {
+
+ 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;
+
+ }
+
+ public interface OnContactPictureClicked {
+ public void onContactPictureClicked(Message message);
+ }
+
+ public interface OnContactPictureLongClicked {
+ public void onContactPictureLongClicked(Message message);
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java b/conversations/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java
new file mode 100644
index 00000000..47595c6e
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java
@@ -0,0 +1,112 @@
+package eu.siacs.conversations.utils;
+
+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 android.util.Base64;
+
+public class CryptoHelper {
+ public static final String FILETRANSFER = "?FILETRANSFERv1:";
+ final protected static char[] hexArray = "0123456789abcdef".toCharArray();
+ final protected static char[] vowels = "aeiou".toCharArray();
+ final protected static char[] consonants = "bcdfghjklmnpqrstvwxyz"
+ .toCharArray();
+
+ 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 saslPlain(String username, String password) {
+ String sasl = '\u0000' + username + '\u0000' + password;
+ return Base64.encodeToString(sasl.getBytes(Charset.defaultCharset()),
+ Base64.NO_WRAP);
+ }
+
+ private 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;
+ }
+
+ public static String saslDigestMd5(Account account, String challenge,
+ SecureRandom random) {
+ try {
+ String[] challengeParts = new String(Base64.decode(challenge,
+ Base64.DEFAULT)).split(",");
+ String nonce = "";
+ for (int i = 0; i < challengeParts.length; ++i) {
+ String[] parts = challengeParts[i].split("=");
+ if (parts[0].equals("nonce")) {
+ nonce = parts[1].replace("\"", "");
+ } else if (parts[0].equals("rspauth")) {
+ return null;
+ }
+ }
+ String digestUri = "xmpp/" + account.getServer();
+ String nonceCount = "00000001";
+ String x = account.getUsername() + ":" + account.getServer() + ":"
+ + account.getPassword();
+ MessageDigest md = MessageDigest.getInstance("MD5");
+ byte[] y = md.digest(x.getBytes(Charset.defaultCharset()));
+ String cNonce = new BigInteger(100, random).toString(32);
+ byte[] a1 = concatenateByteArrays(y,
+ (":" + nonce + ":" + cNonce).getBytes(Charset
+ .defaultCharset()));
+ String a2 = "AUTHENTICATE:" + digestUri;
+ String ha1 = bytesToHex(md.digest(a1));
+ String ha2 = bytesToHex(md.digest(a2.getBytes(Charset
+ .defaultCharset())));
+ String kd = ha1 + ":" + nonce + ":" + nonceCount + ":" + cNonce
+ + ":auth:" + ha2;
+ String response = bytesToHex(md.digest(kd.getBytes(Charset
+ .defaultCharset())));
+ String saslString = "username=\"" + account.getUsername()
+ + "\",realm=\"" + account.getServer() + "\",nonce=\""
+ + nonce + "\",cnonce=\"" + cNonce + "\",nc=" + nonceCount
+ + ",qop=auth,digest-uri=\"" + digestUri + "\",response="
+ + response + ",charset=utf-8";
+ return Base64.encodeToString(
+ saslString.getBytes(Charset.defaultCharset()),
+ Base64.NO_WRAP);
+ } catch (NoSuchAlgorithmException e) {
+ return null;
+ }
+ }
+
+ public static String randomMucName(SecureRandom random) {
+ return randomWord(3, random) + "." + randomWord(7, random);
+ }
+
+ protected static String randomWord(int lenght, SecureRandom random) {
+ StringBuilder builder = new StringBuilder(lenght);
+ for (int i = 0; i < lenght; ++i) {
+ if (i % 2 == 0) {
+ builder.append(consonants[random.nextInt(consonants.length)]);
+ } else {
+ builder.append(vowels[random.nextInt(vowels.length)]);
+ }
+ }
+ return builder.toString();
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/utils/DNSHelper.java b/conversations/src/main/java/eu/siacs/conversations/utils/DNSHelper.java
new file mode 100644
index 00000000..c51a75ac
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/utils/DNSHelper.java
@@ -0,0 +1,185 @@
+package eu.siacs.conversations.utils;
+
+import de.measite.minidns.Client;
+import de.measite.minidns.DNSMessage;
+import de.measite.minidns.Record;
+import de.measite.minidns.Record.TYPE;
+import de.measite.minidns.Record.CLASS;
+import de.measite.minidns.record.SRV;
+import de.measite.minidns.record.A;
+import de.measite.minidns.record.AAAA;
+import de.measite.minidns.record.Data;
+import de.measite.minidns.util.NameUtil;
+import eu.siacs.conversations.Config;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.SocketTimeoutException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Random;
+import java.util.TreeMap;
+
+import android.os.Bundle;
+import android.util.Log;
+
+public class DNSHelper {
+ protected static Client client = new Client();
+
+ public static Bundle getSRVRecord(String host) throws IOException {
+ String dns[] = client.findDNS();
+
+ if (dns != null) {
+ for (String dnsserver : dns) {
+ InetAddress ip = InetAddress.getByName(dnsserver);
+ Bundle b = queryDNS(host, ip);
+ if (b.containsKey("name")) {
+ return b;
+ } else if (b.containsKey("error")
+ && "nosrv".equals(b.getString("error", null))) {
+ return b;
+ }
+ }
+ }
+ return queryDNS(host, InetAddress.getByName("8.8.8.8"));
+ }
+
+ public static Bundle queryDNS(String host, InetAddress dnsServer) {
+ Bundle namePort = new Bundle();
+ try {
+ String qname = "_xmpp-client._tcp." + host;
+ Log.d(Config.LOGTAG,
+ "using dns server: " + dnsServer.getHostAddress()
+ + " to look up " + host);
+ DNSMessage message = client.query(qname, TYPE.SRV, CLASS.IN,
+ dnsServer.getHostAddress());
+
+ // How should we handle priorities and weight?
+ // Wikipedia has a nice article about priorities vs. weights:
+ // https://en.wikipedia.org/wiki/SRV_record#Provisioning_for_high_service_availability
+
+ // we bucket the SRV records based on priority, pick per priority
+ // a random order respecting the weight, and dump that priority by
+ // priority
+
+ TreeMap<Integer, ArrayList<SRV>> priorities = new TreeMap<Integer, ArrayList<SRV>>();
+ TreeMap<String, ArrayList<String>> ips4 = new TreeMap<String, ArrayList<String>>();
+ TreeMap<String, ArrayList<String>> ips6 = new TreeMap<String, ArrayList<String>>();
+
+ for (Record[] rrset : new Record[][] { message.getAnswers(),
+ message.getAdditionalResourceRecords() }) {
+ for (Record rr : rrset) {
+ Data d = rr.getPayload();
+ if (d instanceof SRV
+ && NameUtil.idnEquals(qname, rr.getName())) {
+ SRV srv = (SRV) d;
+ if (!priorities.containsKey(srv.getPriority())) {
+ priorities.put(srv.getPriority(),
+ new ArrayList<SRV>(2));
+ }
+ priorities.get(srv.getPriority()).add(srv);
+ }
+ if (d instanceof A) {
+ A arecord = (A) d;
+ if (!ips4.containsKey(rr.getName())) {
+ ips4.put(rr.getName(), new ArrayList<String>(3));
+ }
+ ips4.get(rr.getName()).add(arecord.toString());
+ }
+ if (d instanceof AAAA) {
+ AAAA aaaa = (AAAA) d;
+ if (!ips6.containsKey(rr.getName())) {
+ ips6.put(rr.getName(), new ArrayList<String>(3));
+ }
+ ips6.get(rr.getName()).add("[" + aaaa.toString() + "]");
+ }
+ }
+ }
+
+ Random rnd = new Random();
+ ArrayList<SRV> result = new ArrayList<SRV>(
+ priorities.size() * 2 + 1);
+ for (ArrayList<SRV> s : priorities.values()) {
+
+ // trivial case
+ if (s.size() <= 1) {
+ result.addAll(s);
+ continue;
+ }
+
+ long totalweight = 0l;
+ for (SRV srv : s) {
+ totalweight += srv.getWeight();
+ }
+
+ while (totalweight > 0l && s.size() > 0) {
+ long p = (rnd.nextLong() & 0x7fffffffffffffffl)
+ % totalweight;
+ int i = 0;
+ while (p > 0) {
+ p -= s.get(i++).getPriority();
+ }
+ i--;
+ // remove is expensive, but we have only a few entries
+ // anyway
+ SRV srv = s.remove(i);
+ totalweight -= srv.getWeight();
+ result.add(srv);
+ }
+
+ Collections.shuffle(s, rnd);
+ result.addAll(s);
+
+ }
+
+ if (result.size() == 0) {
+ namePort.putString("error", "nosrv");
+ return namePort;
+ }
+ // we now have a list of servers to try :-)
+
+ // classic name/port pair
+ String resultName = result.get(0).getName();
+ namePort.putString("name", resultName);
+ namePort.putInt("port", result.get(0).getPort());
+
+ if (ips4.containsKey(resultName)) {
+ // we have an ip!
+ ArrayList<String> ip = ips4.get(resultName);
+ Collections.shuffle(ip, rnd);
+ namePort.putString("ipv4", ip.get(0));
+ }
+ if (ips6.containsKey(resultName)) {
+ ArrayList<String> ip = ips6.get(resultName);
+ Collections.shuffle(ip, rnd);
+ namePort.putString("ipv6", ip.get(0));
+ }
+
+ // add all other records
+ int i = 0;
+ for (SRV srv : result) {
+ namePort.putString("name" + i, srv.getName());
+ namePort.putInt("port" + i, srv.getPort());
+ i++;
+ }
+
+ } catch (SocketTimeoutException e) {
+ namePort.putString("error", "timeout");
+ } catch (Exception e) {
+ namePort.putString("error", "unhandled");
+ }
+ return namePort;
+ }
+
+ final protected static char[] hexArray = "0123456789ABCDEF".toCharArray();
+
+ 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);
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java b/conversations/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java
new file mode 100644
index 00000000..88fa18ff
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java
@@ -0,0 +1,44 @@
+package eu.siacs.conversations.utils;
+
+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;
+
+import android.content.Context;
+
+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());
+ } 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/conversations/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java b/conversations/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java
new file mode 100644
index 00000000..b5fc88bd
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java
@@ -0,0 +1,117 @@
+package eu.siacs.conversations.utils;
+
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.List;
+
+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 android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.SharedPreferences;
+import android.content.DialogInterface.OnClickListener;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.preference.PreferenceManager;
+import android.text.format.DateUtils;
+import android.util.Log;
+
+public class ExceptionHelper {
+ public static void init(Context context) {
+ if (!(Thread.getDefaultUncaughtExceptionHandler() instanceof ExceptionHandler)) {
+ Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler(
+ context));
+ }
+ }
+
+ public static void checkForCrash(Context context,
+ final XmppConnectionService service) {
+ try {
+ final SharedPreferences preferences = PreferenceManager
+ .getDefaultSharedPreferences(context);
+ boolean neverSend = preferences.getBoolean("never_send", false);
+ if (neverSend) {
+ return;
+ }
+ 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;
+ }
+ final Account finalAccount = account;
+ FileInputStream file = context.openFileInput("stacktrace.txt");
+ InputStreamReader inputStreamReader = new InputStreamReader(file);
+ BufferedReader stacktrace = new BufferedReader(inputStreamReader);
+ final StringBuilder report = new StringBuilder();
+ PackageManager pm = context.getPackageManager();
+ PackageInfo packageInfo = null;
+ try {
+ packageInfo = pm.getPackageInfo(context.getPackageName(), 0);
+ report.append("Version: " + packageInfo.versionName + '\n');
+ report.append("Last Update: "
+ + DateUtils.formatDateTime(context,
+ packageInfo.lastUpdateTime,
+ DateUtils.FORMAT_SHOW_TIME
+ | DateUtils.FORMAT_SHOW_DATE) + '\n');
+ } catch (NameNotFoundException e) {
+ }
+ String line;
+ while ((line = stacktrace.readLine()) != null) {
+ report.append(line);
+ report.append('\n');
+ }
+ file.close();
+ context.deleteFile("stacktrace.txt");
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(context.getString(R.string.crash_report_title));
+ builder.setMessage(context.getText(R.string.crash_report_message));
+ builder.setPositiveButton(context.getText(R.string.send_now),
+ new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+
+ Log.d(Config.LOGTAG, "using account="
+ + finalAccount.getJid()
+ + " to send in stack trace");
+ Conversation conversation = service
+ .findOrCreateConversation(finalAccount,
+ "bugs@siacs.eu", false);
+ Message message = new Message(conversation, report
+ .toString(), Message.ENCRYPTION_NONE);
+ service.sendMessage(message);
+ }
+ });
+ builder.setNegativeButton(context.getText(R.string.send_never),
+ new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ preferences.edit().putBoolean("never_send", true)
+ .commit();
+ }
+ });
+ builder.create().show();
+ } catch (FileNotFoundException e) {
+ return;
+ } catch (IOException e) {
+ return;
+ }
+
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java b/conversations/src/main/java/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java
new file mode 100644
index 00000000..9a689768
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java
@@ -0,0 +1,9 @@
+package eu.siacs.conversations.utils;
+
+import java.util.List;
+
+import android.os.Bundle;
+
+public interface OnPhoneContactsLoadedListener {
+ public void onPhoneContactsLoaded(List<Bundle> phoneContacts);
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/utils/PRNGFixes.java b/conversations/src/main/java/eu/siacs/conversations/utils/PRNGFixes.java
new file mode 100644
index 00000000..8fe67234
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/utils/PRNGFixes.java
@@ -0,0 +1,327 @@
+package eu.siacs.conversations.utils;
+
+import android.os.Build;
+import android.os.Process;
+import android.util.Log;
+
+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;
+
+/**
+ * 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.
+ Log.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/conversations/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java b/conversations/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java
new file mode 100644
index 00000000..5becc7e7
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java
@@ -0,0 +1,95 @@
+package eu.siacs.conversations.utils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.RejectedExecutionException;
+
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.Loader;
+import android.content.Loader.OnLoadCompleteListener;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Profile;
+
+public class PhoneHelper {
+
+ public static void loadPhoneContacts(Context context,
+ final OnPhoneContactsLoadedListener listener) {
+ final List<Bundle> phoneContacts = new ArrayList<Bundle>();
+
+ 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 CursorLoader(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) {
+ return;
+ }
+ 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);
+ }
+ if (listener != null) {
+ listener.onPhoneContactsLoaded(phoneContacts);
+ }
+ }
+ });
+ try {
+ mCursorLoader.startLoading();
+ } catch (RejectedExecutionException e) {
+ if (listener != null) {
+ listener.onPhoneContactsLoaded(phoneContacts);
+ }
+ }
+ }
+
+ public static Uri getSefliUri(Context context) {
+ 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);
+ if (uri == null) {
+ return null;
+ } else {
+ return Uri.parse(uri);
+ }
+ }
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/utils/UIHelper.java b/conversations/src/main/java/eu/siacs/conversations/utils/UIHelper.java
new file mode 100644
index 00000000..5141c83c
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/utils/UIHelper.java
@@ -0,0 +1,225 @@
+package eu.siacs.conversations.utils;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+import java.util.regex.Pattern;
+
+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.ui.ConversationActivity;
+import eu.siacs.conversations.ui.ManageAccountActivity;
+import android.annotation.SuppressLint;
+import android.app.AlertDialog;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.support.v4.app.NotificationCompat;
+import android.support.v4.app.TaskStackBuilder;
+import android.text.format.DateFormat;
+import android.text.format.DateUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.TextView;
+
+public class UIHelper {
+ 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) {
+ Calendar cal1 = Calendar.getInstance();
+ Calendar cal2 = Calendar.getInstance();
+ cal1.setTime(date);
+ cal2.setTimeInMillis(System.currentTimeMillis());
+ 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 void showErrorNotification(Context context,
+ List<Account> accounts) {
+ NotificationManager mNotificationManager = (NotificationManager) context
+ .getSystemService(Context.NOTIFICATION_SERVICE);
+ List<Account> accountsWproblems = new ArrayList<Account>();
+ for (Account account : accounts) {
+ if (account.hasErrorStatus()) {
+ accountsWproblems.add(account);
+ }
+ }
+ NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(
+ context);
+ if (accountsWproblems.size() == 0) {
+ mNotificationManager.cancel(1111);
+ return;
+ } else if (accountsWproblems.size() == 1) {
+ mBuilder.setContentTitle(context
+ .getString(R.string.problem_connecting_to_account));
+ mBuilder.setContentText(accountsWproblems.get(0).getJid());
+ } else {
+ mBuilder.setContentTitle(context
+ .getString(R.string.problem_connecting_to_accounts));
+ mBuilder.setContentText(context.getString(R.string.touch_to_fix));
+ }
+ mBuilder.setOngoing(true);
+ mBuilder.setLights(0xffffffff, 2000, 4000);
+ mBuilder.setSmallIcon(R.drawable.ic_notification);
+ TaskStackBuilder stackBuilder = TaskStackBuilder.create(context);
+ stackBuilder.addParentStack(ConversationActivity.class);
+
+ Intent manageAccountsIntent = new Intent(context,
+ ManageAccountActivity.class);
+ stackBuilder.addNextIntent(manageAccountsIntent);
+
+ PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+
+ mBuilder.setContentIntent(resultPendingIntent);
+ Notification notification = mBuilder.build();
+ mNotificationManager.notify(1111, notification);
+ }
+
+ @SuppressLint("InflateParams")
+ public static AlertDialog getVerifyFingerprintDialog(
+ final ConversationActivity activity,
+ final Conversation conversation, final View msg) {
+ final Contact contact = conversation.getContact();
+ final Account account = conversation.getAccount();
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+ builder.setTitle("Verify fingerprint");
+ LayoutInflater inflater = activity.getLayoutInflater();
+ View view = inflater.inflate(R.layout.dialog_verify_otr, null);
+ TextView jid = (TextView) view.findViewById(R.id.verify_otr_jid);
+ TextView fingerprint = (TextView) view
+ .findViewById(R.id.verify_otr_fingerprint);
+ TextView yourprint = (TextView) view
+ .findViewById(R.id.verify_otr_yourprint);
+
+ jid.setText(contact.getJid());
+ fingerprint.setText(conversation.getOtrFingerprint());
+ yourprint.setText(account.getOtrFingerprint());
+ builder.setNegativeButton("Cancel", null);
+ builder.setPositiveButton("Verify", new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ contact.addOtrFingerprint(conversation.getOtrFingerprint());
+ msg.setVisibility(View.GONE);
+ activity.xmppConnectionService.syncRosterToDisk(account);
+ }
+ });
+ builder.setView(view);
+ return builder.create();
+ }
+
+ private final static class EmoticonPattern {
+ Pattern pattern;
+ String replacement;
+
+ EmoticonPattern(String ascii, int unicode) {
+ this.pattern = Pattern.compile("(?<=(^|\\s))" + ascii
+ + "(?=(\\s|$))");
+ this.replacement = new String(new int[] { unicode, }, 0, 1);
+ }
+
+ String replaceAll(String body) {
+ return pattern.matcher(body).replaceAll(replacement);
+ }
+ }
+
+ private static final EmoticonPattern[] patterns = new EmoticonPattern[] {
+ new EmoticonPattern(":-?D", 0x1f600),
+ new EmoticonPattern("\\^\\^", 0x1f601),
+ new EmoticonPattern(":'D", 0x1f602),
+ new EmoticonPattern("\\]-?D", 0x1f608),
+ new EmoticonPattern(";-?\\)", 0x1f609),
+ new EmoticonPattern(":-?\\)", 0x1f60a),
+ new EmoticonPattern("[B8]-?\\)", 0x1f60e),
+ new EmoticonPattern(":-?\\|", 0x1f610),
+ new EmoticonPattern(":-?[/\\\\]", 0x1f615),
+ new EmoticonPattern(":-?\\*", 0x1f617),
+ new EmoticonPattern(":-?[Ppb]", 0x1f61b),
+ new EmoticonPattern(":-?\\(", 0x1f61e),
+ new EmoticonPattern(":-?[0Oo]", 0x1f62e),
+ new EmoticonPattern("\\\\o/", 0x1F631), };
+
+ public static String transformAsciiEmoticons(String body) {
+ if (body != null) {
+ for (EmoticonPattern p : patterns) {
+ body = p.replaceAll(body);
+ }
+ body = body.trim();
+ }
+ return body;
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/utils/Validator.java b/conversations/src/main/java/eu/siacs/conversations/utils/Validator.java
new file mode 100644
index 00000000..00130fa2
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/utils/Validator.java
@@ -0,0 +1,14 @@
+package eu.siacs.conversations.utils;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class Validator {
+ public static final Pattern VALID_JID = Pattern.compile(
+ "^[^@/<>'\"\\s]+@[^@/<>'\"\\s]+$", Pattern.CASE_INSENSITIVE);
+
+ public static boolean isValidJid(String jid) {
+ Matcher matcher = VALID_JID.matcher(jid);
+ return matcher.find();
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/utils/XmlHelper.java b/conversations/src/main/java/eu/siacs/conversations/utils/XmlHelper.java
new file mode 100644
index 00000000..4dee07cf
--- /dev/null
+++ b/conversations/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/conversations/src/main/java/eu/siacs/conversations/utils/zlib/ZLibInputStream.java b/conversations/src/main/java/eu/siacs/conversations/utils/zlib/ZLibInputStream.java
new file mode 100644
index 00000000..b777c10c
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/utils/zlib/ZLibInputStream.java
@@ -0,0 +1,54 @@
+package eu.siacs.conversations.utils.zlib;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.zip.Inflater;
+import java.util.zip.InflaterInputStream;
+
+/**
+ * ZLibInputStream is a zlib and input stream compatible version of an
+ * InflaterInputStream. This class solves the incompatibility between
+ * {@link InputStream#available()} and {@link InflaterInputStream#available()}.
+ */
+public class ZLibInputStream extends InflaterInputStream {
+
+ /**
+ * Construct a ZLibInputStream, reading data from the underlying stream.
+ *
+ * @param is
+ * The {@code InputStream} to read data from.
+ * @throws IOException
+ * If an {@code IOException} occurs.
+ */
+ public ZLibInputStream(InputStream is) throws IOException {
+ super(is, new Inflater(), 512);
+ }
+
+ /**
+ * Provide a more InputStream compatible version of available. A return
+ * value of 1 means that it is likly to read one byte without blocking, 0
+ * means that the system is known to block for more input.
+ *
+ * @return 0 if no data is available, 1 otherwise
+ * @throws IOException
+ */
+ @Override
+ public int available() throws IOException {
+ /*
+ * This is one of the funny code blocks. InflaterInputStream.available
+ * violates the contract of InputStream.available, which breaks kXML2.
+ *
+ * I'm not sure who's to blame, oracle/sun for a broken api or the
+ * google guys for mixing a sun bug with a xml reader that can't handle
+ * it....
+ *
+ * Anyway, this simple if breaks suns distorted reality, but helps to
+ * use the api as intended.
+ */
+ if (inf.needsInput()) {
+ return 0;
+ }
+ return super.available();
+ }
+
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/utils/zlib/ZLibOutputStream.java b/conversations/src/main/java/eu/siacs/conversations/utils/zlib/ZLibOutputStream.java
new file mode 100644
index 00000000..8b3f5e68
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/utils/zlib/ZLibOutputStream.java
@@ -0,0 +1,95 @@
+package eu.siacs.conversations.utils.zlib;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.security.NoSuchAlgorithmException;
+import java.util.zip.Deflater;
+import java.util.zip.DeflaterOutputStream;
+
+/**
+ * <p>
+ * Android 2.2 includes Java7 FLUSH_SYNC option, which will be used by this
+ * Implementation, preferable via reflection. The @hide was remove in API level
+ * 19. This class might thus go away in the future.
+ * </p>
+ * <p>
+ * Please use {@link ZLibOutputStream#SUPPORTED} to check for flush
+ * compatibility.
+ * </p>
+ */
+public class ZLibOutputStream extends DeflaterOutputStream {
+
+ /**
+ * The reflection based flush method.
+ */
+
+ private final static Method method;
+ /**
+ * SUPPORTED is true if a flush compatible method exists.
+ */
+ public final static boolean SUPPORTED;
+
+ /**
+ * Static block to initialize {@link #SUPPORTED} and {@link #method}.
+ */
+ static {
+ Method m = null;
+ try {
+ m = Deflater.class.getMethod("deflate", byte[].class, int.class,
+ int.class, int.class);
+ } catch (SecurityException e) {
+ } catch (NoSuchMethodException e) {
+ }
+ method = m;
+ SUPPORTED = (method != null);
+ }
+
+ /**
+ * Create a new ZLib compatible output stream wrapping the given low level
+ * stream. ZLib compatiblity means we will send a zlib header.
+ *
+ * @param os
+ * OutputStream The underlying stream.
+ * @throws IOException
+ * In case of a lowlevel transfer problem.
+ * @throws NoSuchAlgorithmException
+ * In case of a {@link Deflater} error.
+ */
+ public ZLibOutputStream(OutputStream os) throws IOException,
+ NoSuchAlgorithmException {
+ super(os, new Deflater(Deflater.BEST_COMPRESSION));
+ }
+
+ /**
+ * Flush the given stream, preferring Java7 FLUSH_SYNC if available.
+ *
+ * @throws IOException
+ * In case of a lowlevel exception.
+ */
+ @Override
+ public void flush() throws IOException {
+ if (!SUPPORTED) {
+ super.flush();
+ return;
+ }
+ try {
+ int count = 0;
+ do {
+ count = (Integer) method.invoke(def, buf, 0, buf.length, 3);
+ if (count > 0) {
+ out.write(buf, 0, count);
+ }
+ } while (count > 0);
+ } catch (IllegalArgumentException e) {
+ throw new IOException("Can't flush");
+ } catch (IllegalAccessException e) {
+ throw new IOException("Can't flush");
+ } catch (InvocationTargetException e) {
+ throw new IOException("Can't flush");
+ }
+ super.flush();
+ }
+
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/xml/Element.java b/conversations/src/main/java/eu/siacs/conversations/xml/Element.java
new file mode 100644
index 00000000..4e11ee2c
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/xml/Element.java
@@ -0,0 +1,148 @@
+package eu.siacs.conversations.xml;
+
+import java.util.ArrayList;
+import java.util.Hashtable;
+import java.util.List;
+
+import eu.siacs.conversations.utils.XmlHelper;
+
+public class Element {
+ protected String name;
+ protected Hashtable<String, String> attributes = new Hashtable<String, String>();
+ protected String content;
+ protected List<Element> children = new ArrayList<Element>();
+
+ public Element(String name) {
+ this.name = name;
+ }
+
+ 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 Element findChild(String name, String xmlns) {
+ for (Element child : this.children) {
+ if (child.getName().equals(name)
+ && (child.getAttribute("xmlns").equals(xmlns))) {
+ return child;
+ }
+ }
+ return null;
+ }
+
+ public boolean hasChild(String name) {
+ return findChild(name) != null;
+ }
+
+ public boolean hasChild(String name, 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 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 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 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));
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/xml/Tag.java b/conversations/src/main/java/eu/siacs/conversations/xml/Tag.java
new file mode 100644
index 00000000..b9ef979f
--- /dev/null
+++ b/conversations/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/conversations/src/main/java/eu/siacs/conversations/xml/TagWriter.java b/conversations/src/main/java/eu/siacs/conversations/xml/TagWriter.java
new file mode 100644
index 00000000..f11c1846
--- /dev/null
+++ b/conversations/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/conversations/src/main/java/eu/siacs/conversations/xml/XmlReader.java b/conversations/src/main/java/eu/siacs/conversations/xml/XmlReader.java
new file mode 100644
index 00000000..52d3d46a
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/xml/XmlReader.java
@@ -0,0 +1,141 @@
+package eu.siacs.conversations.xml;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import eu.siacs.conversations.Config;
+
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.util.Log;
+import android.util.Xml;
+
+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) {
+ Log.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/conversations/src/main/java/eu/siacs/conversations/xmpp/OnBindListener.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/OnBindListener.java
new file mode 100644
index 00000000..f09cf33d
--- /dev/null
+++ b/conversations/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/conversations/src/main/java/eu/siacs/conversations/xmpp/OnContactStatusChanged.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/OnContactStatusChanged.java
new file mode 100644
index 00000000..849e8e76
--- /dev/null
+++ b/conversations/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(Contact contact, boolean online);
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/OnIqPacketReceived.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/OnIqPacketReceived.java
new file mode 100644
index 00000000..a4cff986
--- /dev/null
+++ b/conversations/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/conversations/src/main/java/eu/siacs/conversations/xmpp/OnMessageAcknowledged.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/OnMessageAcknowledged.java
new file mode 100644
index 00000000..5f670d93
--- /dev/null
+++ b/conversations/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/conversations/src/main/java/eu/siacs/conversations/xmpp/OnMessagePacketReceived.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/OnMessagePacketReceived.java
new file mode 100644
index 00000000..325e945f
--- /dev/null
+++ b/conversations/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/conversations/src/main/java/eu/siacs/conversations/xmpp/OnPresencePacketReceived.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/OnPresencePacketReceived.java
new file mode 100644
index 00000000..95c1acfc
--- /dev/null
+++ b/conversations/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/conversations/src/main/java/eu/siacs/conversations/xmpp/OnStatusChanged.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/OnStatusChanged.java
new file mode 100644
index 00000000..ad1d98cb
--- /dev/null
+++ b/conversations/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/conversations/src/main/java/eu/siacs/conversations/xmpp/PacketReceived.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/PacketReceived.java
new file mode 100644
index 00000000..d4502d73
--- /dev/null
+++ b/conversations/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/conversations/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java
new file mode 100644
index 00000000..903dc59d
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java
@@ -0,0 +1,1130 @@
+package eu.siacs.conversations.xmpp;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.math.BigInteger;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map.Entry;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+
+import javax.net.ssl.X509TrustManager;
+
+import org.apache.http.conn.ssl.StrictHostnameVerifier;
+import org.xmlpull.v1.XmlPullParserException;
+
+import de.duenndns.ssl.MemorizingTrustManager;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.os.SystemClock;
+import android.preference.PreferenceManager;
+import android.util.Log;
+import android.util.SparseArray;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.utils.DNSHelper;
+import eu.siacs.conversations.utils.zlib.ZLibOutputStream;
+import eu.siacs.conversations.utils.zlib.ZLibInputStream;
+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.jingle.OnJinglePacketReceived;
+import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
+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 {
+
+ protected Account account;
+
+ private WakeLock wakeLock;
+
+ private SecureRandom mRandom;
+
+ private Socket socket;
+ private XmlReader tagReader;
+ private TagWriter tagWriter;
+
+ private Features features = new Features(this);
+
+ private boolean shouldBind = true;
+ private boolean shouldAuthenticate = true;
+ private Element streamFeatures;
+ private HashMap<String, List<String>> disco = new HashMap<String, List<String>>();
+
+ private String streamId = null;
+ private int smVersion = 3;
+ private SparseArray<String> messageReceipts = new SparseArray<String>();
+
+ private boolean usingCompression = false;
+ private boolean usingEncryption = false;
+
+ private int stanzasReceived = 0;
+ private int stanzasSent = 0;
+
+ private long lastPaketReceived = 0;
+ private long lastPingSent = 0;
+ private long lastConnect = 0;
+ private long lastSessionStarted = 0;
+
+ private int attempt = 0;
+
+ private static final int PACKET_IQ = 0;
+ private static final int PACKET_MESSAGE = 1;
+ private static final int PACKET_PRESENCE = 2;
+
+ private Hashtable<String, PacketReceived> packetCallbacks = new Hashtable<String, PacketReceived>();
+ 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 OnMessageAcknowledged acknowledgedListener = null;
+ private MemorizingTrustManager mMemorizingTrustManager;
+ private final Context applicationContext;
+
+ public XmppConnection(Account account, XmppConnectionService service) {
+ this.mRandom = service.getRNG();
+ this.mMemorizingTrustManager = service.getMemorizingTrustManager();
+ this.account = account;
+ this.wakeLock = service.getPowerManager().newWakeLock(
+ PowerManager.PARTIAL_WAKE_LOCK, account.getJid());
+ tagWriter = new TagWriter();
+ applicationContext = service.getApplicationContext();
+ }
+
+ protected void changeStatus(int nextStatus) {
+ if (account.getStatus() != nextStatus) {
+ if ((nextStatus == Account.STATUS_OFFLINE)
+ && (account.getStatus() != Account.STATUS_CONNECTING)
+ && (account.getStatus() != Account.STATUS_ONLINE)
+ && (account.getStatus() != Account.STATUS_DISABLED)) {
+ return;
+ }
+ if (nextStatus == Account.STATUS_ONLINE) {
+ this.attempt = 0;
+ }
+ account.setStatus(nextStatus);
+ if (statusListener != null) {
+ statusListener.onStatusChanged(account);
+ }
+ }
+ }
+
+ protected void connect() {
+ Log.d(Config.LOGTAG, account.getJid() + ": connecting");
+ usingCompression = false;
+ usingEncryption = false;
+ lastConnect = SystemClock.elapsedRealtime();
+ lastPingSent = SystemClock.elapsedRealtime();
+ this.attempt++;
+ try {
+ shouldAuthenticate = shouldBind = !account
+ .isOptionSet(Account.OPTION_REGISTER);
+ tagReader = new XmlReader(wakeLock);
+ tagWriter = new TagWriter();
+ packetCallbacks.clear();
+ this.changeStatus(Account.STATUS_CONNECTING);
+ Bundle namePort = DNSHelper.getSRVRecord(account.getServer());
+ if ("timeout".equals(namePort.getString("error"))) {
+ Log.d(Config.LOGTAG, account.getJid() + ": dns timeout");
+ this.changeStatus(Account.STATUS_OFFLINE);
+ return;
+ }
+ String srvRecordServer = namePort.getString("name");
+ String srvIpServer = namePort.getString("ipv4");
+ int srvRecordPort = namePort.getInt("port");
+ if (srvRecordServer != null) {
+ if (srvIpServer != null) {
+ Log.d(Config.LOGTAG, account.getJid()
+ + ": using values from dns " + srvRecordServer
+ + "[" + srvIpServer + "]:" + srvRecordPort);
+ socket = new Socket(srvIpServer, srvRecordPort);
+ } else {
+ boolean socketError = true;
+ int srvIndex = 0;
+ while (socketError
+ && namePort.containsKey("name" + srvIndex)) {
+ try {
+ srvRecordServer = namePort.getString("name"
+ + srvIndex);
+ srvRecordPort = namePort.getInt("port" + srvIndex);
+ Log.d(Config.LOGTAG, account.getJid()
+ + ": using values from dns "
+ + srvRecordServer + ":" + srvRecordPort);
+ socket = new Socket(srvRecordServer, srvRecordPort);
+ socketError = false;
+ } catch (UnknownHostException e) {
+ srvIndex++;
+ if (!namePort.containsKey("name" + srvIndex)) {
+ throw e;
+ }
+ } catch (IOException e) {
+ srvIndex++;
+ if (!namePort.containsKey("name" + srvIndex)) {
+ throw e;
+ }
+ }
+ }
+ }
+ } else if (namePort.containsKey("error")
+ && "nosrv".equals(namePort.getString("error", null))) {
+ socket = new Socket(account.getServer(), 5222);
+ } else {
+ Log.d(Config.LOGTAG, account.getJid()
+ + ": timeout in DNS resolution");
+ changeStatus(Account.STATUS_OFFLINE);
+ return;
+ }
+ OutputStream out = socket.getOutputStream();
+ tagWriter.setOutputStream(out);
+ InputStream in = socket.getInputStream();
+ tagReader.setInputStream(in);
+ tagWriter.beginDocument();
+ sendStartStream();
+ Tag nextTag;
+ while ((nextTag = tagReader.readTag()) != null) {
+ if (nextTag.isStart("stream")) {
+ processStream(nextTag);
+ break;
+ } else {
+ Log.d(Config.LOGTAG,
+ "found unexpected tag: " + nextTag.getName());
+ return;
+ }
+ }
+ if (socket.isConnected()) {
+ socket.close();
+ }
+ } catch (UnknownHostException e) {
+ this.changeStatus(Account.STATUS_SERVER_NOT_FOUND);
+ if (wakeLock.isHeld()) {
+ try {
+ wakeLock.release();
+ } catch (RuntimeException re) {
+ }
+ }
+ return;
+ } catch (IOException e) {
+ this.changeStatus(Account.STATUS_OFFLINE);
+ if (wakeLock.isHeld()) {
+ try {
+ wakeLock.release();
+ } catch (RuntimeException re) {
+ }
+ }
+ return;
+ } catch (NoSuchAlgorithmException e) {
+ this.changeStatus(Account.STATUS_OFFLINE);
+ Log.d(Config.LOGTAG, "compression exception " + e.getMessage());
+ if (wakeLock.isHeld()) {
+ try {
+ wakeLock.release();
+ } catch (RuntimeException re) {
+ }
+ }
+ return;
+ } catch (XmlPullParserException e) {
+ this.changeStatus(Account.STATUS_OFFLINE);
+ Log.d(Config.LOGTAG, "xml exception " + e.getMessage());
+ if (wakeLock.isHeld()) {
+ try {
+ wakeLock.release();
+ } catch (RuntimeException re) {
+ }
+ }
+ return;
+ }
+
+ }
+
+ @Override
+ public void run() {
+ connect();
+ }
+
+ private void processStream(Tag currentTag) 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("compressed")) {
+ switchOverToZLib(nextTag);
+ } else if (nextTag.isStart("success")) {
+ Log.d(Config.LOGTAG, account.getJid() + ": logged in");
+ tagReader.readTag();
+ tagReader.reset();
+ sendStartStream();
+ processStream(tagReader.readTag());
+ break;
+ } else if (nextTag.isStart("failure")) {
+ tagReader.readElement(nextTag);
+ changeStatus(Account.STATUS_UNAUTHORIZED);
+ } else if (nextTag.isStart("challenge")) {
+ String challange = tagReader.readElement(nextTag).getContent();
+ Element response = new Element("response");
+ response.setAttribute("xmlns",
+ "urn:ietf:params:xml:ns:xmpp-sasl");
+ response.setContent(CryptoHelper.saslDigestMd5(account,
+ challange, mRandom));
+ tagWriter.writeElement(response);
+ } else if (nextTag.isStart("enabled")) {
+ Element enabled = tagReader.readElement(nextTag);
+ if ("true".equals(enabled.getAttribute("resume"))) {
+ this.streamId = enabled.getAttribute("id");
+ Log.d(Config.LOGTAG, account.getJid()
+ + ": stream managment(" + smVersion
+ + ") enabled (resumable)");
+ } else {
+ Log.d(Config.LOGTAG, account.getJid()
+ + ": stream managment(" + smVersion + ") enabled");
+ }
+ this.lastSessionStarted = SystemClock.elapsedRealtime();
+ this.stanzasReceived = 0;
+ RequestPacket r = new RequestPacket(smVersion);
+ tagWriter.writeStanzaAsync(r);
+ } else if (nextTag.isStart("resumed")) {
+ lastPaketReceived = SystemClock.elapsedRealtime();
+ Element resumed = tagReader.readElement(nextTag);
+ String h = resumed.getAttribute("h");
+ try {
+ int serverCount = Integer.parseInt(h);
+ if (serverCount != stanzasSent) {
+ Log.d(Config.LOGTAG, account.getJid()
+ + ": session resumed with lost packages");
+ stanzasSent = serverCount;
+ } else {
+ Log.d(Config.LOGTAG, account.getJid()
+ + ": session resumed");
+ }
+ if (acknowledgedListener != null) {
+ for (int i = 0; i < messageReceipts.size(); ++i) {
+ if (serverCount >= messageReceipts.keyAt(i)) {
+ acknowledgedListener.onMessageAcknowledged(
+ account, messageReceipts.valueAt(i));
+ }
+ }
+ }
+ messageReceipts.clear();
+ } catch (NumberFormatException e) {
+
+ }
+ sendInitialPing();
+
+ } else if (nextTag.isStart("r")) {
+ tagReader.readElement(nextTag);
+ AckPacket ack = new AckPacket(this.stanzasReceived, smVersion);
+ tagWriter.writeStanzaAsync(ack);
+ } else if (nextTag.isStart("a")) {
+ Element ack = tagReader.readElement(nextTag);
+ lastPaketReceived = SystemClock.elapsedRealtime();
+ int serverSequence = Integer.parseInt(ack.getAttribute("h"));
+ String msgId = this.messageReceipts.get(serverSequence);
+ if (msgId != null) {
+ if (this.acknowledgedListener != null) {
+ this.acknowledgedListener.onMessageAcknowledged(
+ account, msgId);
+ }
+ this.messageReceipts.remove(serverSequence);
+ }
+ } else if (nextTag.isStart("failed")) {
+ tagReader.readElement(nextTag);
+ Log.d(Config.LOGTAG, account.getJid() + ": resumption failed");
+ streamId = null;
+ if (account.getStatus() != Account.STATUS_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();
+ }
+ if (account.getStatus() == Account.STATUS_ONLINE) {
+ account.setStatus(Account.STATUS_OFFLINE);
+ if (statusListener != null) {
+ statusListener.onStatusChanged(account);
+ }
+ }
+ }
+
+ private void sendInitialPing() {
+ Log.d(Config.LOGTAG, account.getJid() + ": sending intial ping");
+ IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
+ iq.setFrom(account.getFullJid());
+ iq.addChild("ping", "urn:xmpp:ping");
+ this.sendIqPacket(iq, new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ Log.d(Config.LOGTAG, account.getJid()
+ + ": online with resource " + account.getResource());
+ changeStatus(Account.STATUS_ONLINE);
+ }
+ });
+ }
+
+ private Element processPacket(Tag currentTag, 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()) {
+ Element child = tagReader.readElement(nextTag);
+ 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");
+ }
+ }
+ ++stanzasReceived;
+ lastPaketReceived = SystemClock.elapsedRealtime();
+ return element;
+ }
+
+ private void processIq(Tag currentTag) throws XmlPullParserException,
+ IOException {
+ 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 {
+ if (packetCallbacks.containsKey(packet.getId())) {
+ if (packetCallbacks.get(packet.getId()) instanceof OnIqPacketReceived) {
+ ((OnIqPacketReceived) packetCallbacks.get(packet.getId()))
+ .onIqPacketReceived(account, packet);
+ }
+
+ packetCallbacks.remove(packet.getId());
+ } else if ((packet.getType() == IqPacket.TYPE_GET || packet
+ .getType() == IqPacket.TYPE_SET)
+ && this.unregisteredIqListener != null) {
+ this.unregisteredIqListener.onIqPacketReceived(account, packet);
+ }
+ }
+ }
+
+ private void processMessage(Tag currentTag) throws XmlPullParserException,
+ IOException {
+ MessagePacket packet = (MessagePacket) processPacket(currentTag,
+ PACKET_MESSAGE);
+ String id = packet.getAttribute("id");
+ if ((id != null) && (packetCallbacks.containsKey(id))) {
+ if (packetCallbacks.get(id) instanceof OnMessagePacketReceived) {
+ ((OnMessagePacketReceived) packetCallbacks.get(id))
+ .onMessagePacketReceived(account, packet);
+ }
+ packetCallbacks.remove(id);
+ } else if (this.messageListener != null) {
+ this.messageListener.onMessagePacketReceived(account, packet);
+ }
+ }
+
+ private void processPresence(Tag currentTag) throws XmlPullParserException,
+ IOException {
+ PresencePacket packet = (PresencePacket) processPacket(currentTag,
+ PACKET_PRESENCE);
+ String id = packet.getAttribute("id");
+ if ((id != null) && (packetCallbacks.containsKey(id))) {
+ if (packetCallbacks.get(id) instanceof OnPresencePacketReceived) {
+ ((OnPresencePacketReceived) packetCallbacks.get(id))
+ .onPresencePacketReceived(account, packet);
+ }
+ packetCallbacks.remove(id);
+ } else if (this.presenceListener != null) {
+ this.presenceListener.onPresencePacketReceived(account, packet);
+ }
+ }
+
+ private void sendCompressionZlib() throws IOException {
+ Element compress = new Element("compress");
+ compress.setAttribute("xmlns", "http://jabber.org/protocol/compress");
+ compress.addChild("method").setContent("zlib");
+ tagWriter.writeElement(compress);
+ }
+
+ private void switchOverToZLib(Tag currentTag)
+ throws XmlPullParserException, IOException,
+ NoSuchAlgorithmException {
+ tagReader.readTag(); // read tag close
+ tagWriter.setOutputStream(new ZLibOutputStream(tagWriter
+ .getOutputStream()));
+ tagReader
+ .setInputStream(new ZLibInputStream(tagReader.getInputStream()));
+
+ sendStartStream();
+ Log.d(Config.LOGTAG, account.getJid() + ": compression enabled");
+ usingCompression = true;
+ processStream(tagReader.readTag());
+ }
+
+ private void sendStartTLS() throws IOException {
+ Tag startTLS = Tag.empty("starttls");
+ startTLS.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-tls");
+ tagWriter.writeTag(startTLS);
+ }
+
+ private SharedPreferences getPreferences() {
+ return PreferenceManager
+ .getDefaultSharedPreferences(applicationContext);
+ }
+
+ private boolean enableLegacySSL() {
+ return getPreferences().getBoolean("enable_legacy_ssl", false);
+ }
+
+ private void switchOverToTls(Tag currentTag) throws XmlPullParserException,
+ IOException {
+ tagReader.readTag();
+ try {
+ SSLContext sc = SSLContext.getInstance("TLS");
+ sc.init(null,
+ new X509TrustManager[] { this.mMemorizingTrustManager },
+ mRandom);
+ SSLSocketFactory factory = sc.getSocketFactory();
+
+ HostnameVerifier verifier = this.mMemorizingTrustManager
+ .wrapHostnameVerifier(new StrictHostnameVerifier());
+ SSLSocket sslSocket = (SSLSocket) factory.createSocket(socket,
+ socket.getInetAddress().getHostAddress(), socket.getPort(),
+ true);
+
+ // Support all protocols except legacy SSL.
+ // The min SDK version prevents us having to worry about SSLv2. In
+ // future, this may be
+ // true of SSLv3 as well.
+ final String[] supportProtocols;
+ if (enableLegacySSL()) {
+ supportProtocols = sslSocket.getSupportedProtocols();
+ } else {
+ final List<String> supportedProtocols = new LinkedList<String>(
+ Arrays.asList(sslSocket.getSupportedProtocols()));
+ supportedProtocols.remove("SSLv3");
+ supportProtocols = new String[supportedProtocols.size()];
+ supportedProtocols.toArray(supportProtocols);
+ }
+ sslSocket.setEnabledProtocols(supportProtocols);
+
+ if (verifier != null
+ && !verifier.verify(account.getServer(),
+ sslSocket.getSession())) {
+ Log.d(Config.LOGTAG, account.getJid()
+ + ": host mismatch in TLS connection");
+ sslSocket.close();
+ throw new IOException();
+ }
+ tagReader.setInputStream(sslSocket.getInputStream());
+ tagWriter.setOutputStream(sslSocket.getOutputStream());
+ sendStartStream();
+ Log.d(Config.LOGTAG, account.getJid()
+ + ": TLS connection established");
+ usingEncryption = true;
+ processStream(tagReader.readTag());
+ sslSocket.close();
+ } catch (NoSuchAlgorithmException e1) {
+ e1.printStackTrace();
+ } catch (KeyManagementException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void sendSaslAuthPlain() throws IOException {
+ String saslString = CryptoHelper.saslPlain(account.getUsername(),
+ account.getPassword());
+ Element auth = new Element("auth");
+ auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
+ auth.setAttribute("mechanism", "PLAIN");
+ auth.setContent(saslString);
+ tagWriter.writeElement(auth);
+ }
+
+ private void sendSaslAuthDigestMd5() throws IOException {
+ Element auth = new Element("auth");
+ auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
+ auth.setAttribute("mechanism", "DIGEST-MD5");
+ tagWriter.writeElement(auth);
+ }
+
+ private void processStreamFeatures(Tag currentTag)
+ throws XmlPullParserException, IOException {
+ this.streamFeatures = tagReader.readElement(currentTag);
+ if (this.streamFeatures.hasChild("starttls") && !usingEncryption) {
+ sendStartTLS();
+ } else if (compressionAvailable()) {
+ sendCompressionZlib();
+ } else if (this.streamFeatures.hasChild("register")
+ && account.isOptionSet(Account.OPTION_REGISTER)
+ && usingEncryption) {
+ sendRegistryRequest();
+ } else if (!this.streamFeatures.hasChild("register")
+ && account.isOptionSet(Account.OPTION_REGISTER)) {
+ changeStatus(Account.STATUS_REGISTRATION_NOT_SUPPORTED);
+ disconnect(true);
+ } else if (this.streamFeatures.hasChild("mechanisms")
+ && shouldAuthenticate && usingEncryption) {
+ List<String> mechanisms = extractMechanisms(streamFeatures
+ .findChild("mechanisms"));
+ if (mechanisms.contains("PLAIN")) {
+ sendSaslAuthPlain();
+ } else if (mechanisms.contains("DIGEST-MD5")) {
+ sendSaslAuthDigestMd5();
+ }
+ } else if (this.streamFeatures.hasChild("sm", "urn:xmpp:sm:"
+ + smVersion)
+ && streamId != null) {
+ ResumePacket resume = new ResumePacket(this.streamId,
+ stanzasReceived, smVersion);
+ this.tagWriter.writeStanzaAsync(resume);
+ } else if (this.streamFeatures.hasChild("bind") && shouldBind) {
+ sendBindRequest();
+ } else {
+ Log.d(Config.LOGTAG, account.getJid()
+ + ": incompatible server. disconnecting");
+ disconnect(true);
+ }
+ }
+
+ private boolean compressionAvailable() {
+ if (!this.streamFeatures.hasChild("compression",
+ "http://jabber.org/features/compress"))
+ return false;
+ if (!ZLibOutputStream.SUPPORTED)
+ return false;
+ if (!account.isOptionSet(Account.OPTION_USECOMPRESSION))
+ return false;
+
+ Element compression = this.streamFeatures.findChild("compression",
+ "http://jabber.org/features/compress");
+ for (Element child : compression.getChildren()) {
+ if (!"method".equals(child.getName()))
+ continue;
+
+ if ("zlib".equalsIgnoreCase(child.getContent())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private List<String> extractMechanisms(Element stream) {
+ ArrayList<String> mechanisms = new ArrayList<String>(stream
+ .getChildren().size());
+ for (Element child : stream.getChildren()) {
+ mechanisms.add(child.getContent());
+ }
+ return mechanisms;
+ }
+
+ private void sendRegistryRequest() {
+ IqPacket register = new IqPacket(IqPacket.TYPE_GET);
+ register.query("jabber:iq:register");
+ register.setTo(account.getServer());
+ sendIqPacket(register, new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ Element instructions = packet.query().findChild("instructions");
+ if (packet.query().hasChild("username")
+ && (packet.query().hasChild("password"))) {
+ IqPacket register = new IqPacket(IqPacket.TYPE_SET);
+ Element username = new Element("username")
+ .setContent(account.getUsername());
+ Element password = new Element("password")
+ .setContent(account.getPassword());
+ register.query("jabber:iq:register").addChild(username);
+ register.query().addChild(password);
+ sendIqPacket(register, new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account,
+ IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE_RESULT) {
+ account.setOption(Account.OPTION_REGISTER,
+ false);
+ changeStatus(Account.STATUS_REGISTRATION_SUCCESSFULL);
+ } else if (packet.hasChild("error")
+ && (packet.findChild("error")
+ .hasChild("conflict"))) {
+ changeStatus(Account.STATUS_REGISTRATION_CONFLICT);
+ } else {
+ changeStatus(Account.STATUS_REGISTRATION_FAILED);
+ Log.d(Config.LOGTAG, packet.toString());
+ }
+ disconnect(true);
+ }
+ });
+ } else {
+ changeStatus(Account.STATUS_REGISTRATION_FAILED);
+ disconnect(true);
+ Log.d(Config.LOGTAG, account.getJid()
+ + ": could not register. instructions are"
+ + instructions.getContent());
+ }
+ }
+ });
+ }
+
+ private void sendBindRequest() throws IOException {
+ IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
+ iq.addChild("bind", "urn:ietf:params:xml:ns:xmpp-bind")
+ .addChild("resource").setContent(account.getResource());
+ this.sendUnboundIqPacket(iq, new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ Element bind = packet.findChild("bind");
+ if (bind != null) {
+ Element jid = bind.findChild("jid");
+ if (jid != null && jid.getContent() != null) {
+ account.setResource(jid.getContent().split("/", 2)[1]);
+ if (streamFeatures.hasChild("sm", "urn:xmpp:sm:3")) {
+ smVersion = 3;
+ EnablePacket enable = new EnablePacket(smVersion);
+ tagWriter.writeStanzaAsync(enable);
+ stanzasSent = 0;
+ messageReceipts.clear();
+ } else if (streamFeatures.hasChild("sm",
+ "urn:xmpp:sm:2")) {
+ smVersion = 2;
+ EnablePacket enable = new EnablePacket(smVersion);
+ tagWriter.writeStanzaAsync(enable);
+ stanzasSent = 0;
+ messageReceipts.clear();
+ }
+ sendServiceDiscoveryInfo(account.getServer());
+ sendServiceDiscoveryItems(account.getServer());
+ if (bindListener != null) {
+ bindListener.onBind(account);
+ }
+ sendInitialPing();
+ } else {
+ disconnect(true);
+ }
+ } else {
+ disconnect(true);
+ }
+ }
+ });
+ if (this.streamFeatures.hasChild("session")) {
+ Log.d(Config.LOGTAG, account.getJid()
+ + ": sending deprecated session");
+ IqPacket startSession = new IqPacket(IqPacket.TYPE_SET);
+ startSession.addChild("session",
+ "urn:ietf:params:xml:ns:xmpp-session");
+ this.sendUnboundIqPacket(startSession, null);
+ }
+ }
+
+ private void sendServiceDiscoveryInfo(final String server) {
+ IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
+ iq.setTo(server);
+ iq.query("http://jabber.org/protocol/disco#info");
+ this.sendIqPacket(iq, new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ List<Element> elements = packet.query().getChildren();
+ List<String> features = new ArrayList<String>();
+ for (int i = 0; i < elements.size(); ++i) {
+ if (elements.get(i).getName().equals("feature")) {
+ features.add(elements.get(i).getAttribute("var"));
+ }
+ }
+ disco.put(server, features);
+
+ if (account.getServer().equals(server)) {
+ enableAdvancedStreamFeatures();
+ }
+ }
+ });
+ }
+
+ private void enableAdvancedStreamFeatures() {
+ if (getFeatures().carbons()) {
+ sendEnableCarbons();
+ }
+ }
+
+ private void sendServiceDiscoveryItems(final String server) {
+ IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
+ iq.setTo(server);
+ iq.query("http://jabber.org/protocol/disco#items");
+ this.sendIqPacket(iq, new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ List<Element> elements = packet.query().getChildren();
+ for (int i = 0; i < elements.size(); ++i) {
+ if (elements.get(i).getName().equals("item")) {
+ String jid = elements.get(i).getAttribute("jid");
+ sendServiceDiscoveryInfo(jid);
+ }
+ }
+ }
+ });
+ }
+
+ private void sendEnableCarbons() {
+ IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
+ iq.addChild("enable", "urn:xmpp:carbons:2");
+ this.sendIqPacket(iq, new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ if (!packet.hasChild("error")) {
+ Log.d(Config.LOGTAG, account.getJid()
+ + ": successfully enabled carbons");
+ } else {
+ Log.d(Config.LOGTAG, account.getJid()
+ + ": error enableing carbons " + packet.toString());
+ }
+ }
+ });
+ }
+
+ private void processStreamError(Tag currentTag)
+ throws XmlPullParserException, IOException {
+ Element streamError = tagReader.readElement(currentTag);
+ if (streamError != null && streamError.hasChild("conflict")) {
+ String resource = account.getResource().split("\\.")[0];
+ account.setResource(resource + "." + nextRandomId());
+ Log.d(Config.LOGTAG,
+ account.getJid() + ": switching resource due to conflict ("
+ + account.getResource() + ")");
+ }
+ }
+
+ private void sendStartStream() throws IOException {
+ Tag stream = Tag.start("stream:stream");
+ stream.setAttribute("from", account.getJid());
+ stream.setAttribute("to", account.getServer());
+ 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, mRandom).toString(32);
+ }
+
+ public void sendIqPacket(IqPacket packet, OnIqPacketReceived callback) {
+ if (packet.getId() == null) {
+ String id = nextRandomId();
+ packet.setAttribute("id", id);
+ }
+ packet.setFrom(account.getFullJid());
+ this.sendPacket(packet, callback);
+ }
+
+ public void sendUnboundIqPacket(IqPacket packet, OnIqPacketReceived callback) {
+ if (packet.getId() == null) {
+ String id = nextRandomId();
+ packet.setAttribute("id", id);
+ }
+ this.sendPacket(packet, callback);
+ }
+
+ public void sendMessagePacket(MessagePacket packet) {
+ this.sendPacket(packet, null);
+ }
+
+ public void sendPresencePacket(PresencePacket packet) {
+ this.sendPacket(packet, null);
+ }
+
+ private synchronized void sendPacket(final AbstractStanza packet,
+ PacketReceived callback) {
+ if (packet.getName().equals("iq") || packet.getName().equals("message")
+ || packet.getName().equals("presence")) {
+ ++stanzasSent;
+ }
+ tagWriter.writeStanzaAsync(packet);
+ if (packet instanceof MessagePacket && packet.getId() != null
+ && this.streamId != null) {
+ Log.d(Config.LOGTAG, "request delivery report for stanza "
+ + stanzasSent);
+ this.messageReceipts.put(stanzasSent, packet.getId());
+ tagWriter.writeStanzaAsync(new RequestPacket(this.smVersion));
+ }
+ if (callback != null) {
+ if (packet.getId() == null) {
+ packet.setId(nextRandomId());
+ }
+ packetCallbacks.put(packet.getId(), callback);
+ }
+ }
+
+ public void sendPing() {
+ if (streamFeatures.hasChild("sm")) {
+ tagWriter.writeStanzaAsync(new RequestPacket(smVersion));
+ } else {
+ IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
+ iq.setFrom(account.getFullJid());
+ iq.addChild("ping", "urn:xmpp:ping");
+ this.sendIqPacket(iq, null);
+ }
+ this.lastPingSent = SystemClock.elapsedRealtime();
+ }
+
+ public void setOnMessagePacketReceivedListener(
+ OnMessagePacketReceived listener) {
+ this.messageListener = listener;
+ }
+
+ public void setOnUnregisteredIqPacketReceivedListener(
+ OnIqPacketReceived listener) {
+ this.unregisteredIqListener = listener;
+ }
+
+ public void setOnPresencePacketReceivedListener(
+ OnPresencePacketReceived listener) {
+ this.presenceListener = listener;
+ }
+
+ public void setOnJinglePacketReceivedListener(
+ OnJinglePacketReceived listener) {
+ this.jingleListener = listener;
+ }
+
+ public void setOnStatusChangedListener(OnStatusChanged listener) {
+ this.statusListener = listener;
+ }
+
+ public void setOnBindListener(OnBindListener listener) {
+ this.bindListener = listener;
+ }
+
+ public void setOnMessageAcknowledgeListener(OnMessageAcknowledged listener) {
+ this.acknowledgedListener = listener;
+ }
+
+ public void disconnect(boolean force) {
+ Log.d(Config.LOGTAG, account.getJid() + ": disconnecting");
+ try {
+ if (force) {
+ socket.close();
+ return;
+ }
+ new Thread(new Runnable() {
+
+ @Override
+ public void run() {
+ if (tagWriter.isActive()) {
+ tagWriter.finish();
+ try {
+ while (!tagWriter.finished()) {
+ Log.d(Config.LOGTAG, "not yet finished");
+ Thread.sleep(100);
+ }
+ tagWriter.writeTag(Tag.end("stream:stream"));
+ socket.close();
+ } catch (IOException e) {
+ Log.d(Config.LOGTAG,
+ "io exception during disconnect");
+ } catch (InterruptedException e) {
+ Log.d(Config.LOGTAG, "interrupted");
+ }
+ }
+ }
+ }).start();
+ } catch (IOException e) {
+ Log.d(Config.LOGTAG, "io exception during disconnect");
+ }
+ }
+
+ public List<String> findDiscoItemsByFeature(String feature) {
+ List<String> items = new ArrayList<String>();
+ for (Entry<String, List<String>> cursor : disco.entrySet()) {
+ if (cursor.getValue().contains(feature)) {
+ items.add(cursor.getKey());
+ }
+ }
+ return items;
+ }
+
+ public String findDiscoItemByFeature(String feature) {
+ List<String> items = findDiscoItemsByFeature(feature);
+ if (items.size() >= 1) {
+ return items.get(0);
+ }
+ return null;
+ }
+
+ public void r() {
+ this.tagWriter.writeStanzaAsync(new RequestPacket(smVersion));
+ }
+
+ public String getMucServer() {
+ return findDiscoItemByFeature("http://jabber.org/protocol/muc");
+ }
+
+ public int getTimeToNextAttempt() {
+ int interval = (int) (25 * Math.pow(1.5, attempt));
+ int secondsSinceLast = (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000);
+ return interval - secondsSinceLast;
+ }
+
+ public int getAttempt() {
+ return this.attempt;
+ }
+
+ public Features getFeatures() {
+ return this.features;
+ }
+
+ public class Features {
+ XmppConnection connection;
+
+ public Features(XmppConnection connection) {
+ this.connection = connection;
+ }
+
+ private boolean hasDiscoFeature(String server, String feature) {
+ if (!connection.disco.containsKey(server)) {
+ return false;
+ }
+ return connection.disco.get(server).contains(feature);
+ }
+
+ public boolean carbons() {
+ return hasDiscoFeature(account.getServer(), "urn:xmpp:carbons:2");
+ }
+
+ public boolean sm() {
+ return streamId != null;
+ }
+
+ public boolean csi() {
+ if (connection.streamFeatures == null) {
+ return false;
+ } else {
+ return connection.streamFeatures.hasChild("csi",
+ "urn:xmpp:csi:0");
+ }
+ }
+
+ public boolean pubsub() {
+ return hasDiscoFeature(account.getServer(),
+ "http://jabber.org/protocol/pubsub#publish");
+ }
+
+ public boolean rosterVersioning() {
+ if (connection.streamFeatures == null) {
+ return false;
+ } else {
+ return connection.streamFeatures.hasChild("ver");
+ }
+ }
+
+ public boolean streamhost() {
+ return connection
+ .findDiscoItemByFeature("http://jabber.org/protocol/bytestreams") != null;
+ }
+
+ public boolean compression() {
+ return connection.usingCompression;
+ }
+ }
+
+ public long getLastSessionEstablished() {
+ 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 getLastPacketReceived() {
+ return this.lastPaketReceived;
+ }
+
+ public void sendActive() {
+ this.sendPacket(new ActivePacket(), null);
+ }
+
+ public void sendInactive() {
+ this.sendPacket(new InactivePacket(), null);
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java
new file mode 100644
index 00000000..3e7c7b68
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java
@@ -0,0 +1,143 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import eu.siacs.conversations.xml.Element;
+
+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 String 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(String jid) {
+ this.jid = jid;
+ }
+
+ public String 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) {
+ if ("proxy".equals(type)) {
+ this.type = TYPE_PROXY;
+ } else if ("direct".equals(type)) {
+ this.type = TYPE_DIRECT;
+ } else {
+ this.type = TYPE_UNKNOWN;
+ }
+ }
+
+ 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.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<JingleCandidate>();
+ 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.getAttribute("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());
+ 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/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java
new file mode 100644
index 00000000..a0b2feb2
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java
@@ -0,0 +1,910 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map.Entry;
+import java.util.concurrent.ConcurrentHashMap;
+
+import android.content.Intent;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.util.Log;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Downloadable;
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.OnIqPacketReceived;
+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 Downloadable {
+
+ private final String[] extensions = { "webp", "jpeg", "jpg", "png" };
+ private final String[] cryptoExtensions = { "pgp", "gpg", "otr" };
+
+ 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_TERMINATED = 2;
+ protected static final int JINGLE_STATUS_CANCELED = 3;
+ 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 = 4096;
+
+ private int mJingleStatus = -1;
+ private int mStatus = -1;
+ private Message message;
+ private String sessionId;
+ private Account account;
+ private String initiator;
+ private String responder;
+ private List<JingleCandidate> candidates = new ArrayList<JingleCandidate>();
+ private ConcurrentHashMap<String, JingleSocks5Transport> connections = new ConcurrentHashMap<String, JingleSocks5Transport>();
+
+ private String transportId;
+ private Element fileOffer;
+ private DownloadableFile file = null;
+
+ private String contentName;
+ private String contentCreator;
+
+ private boolean receivedCandidate = false;
+ private boolean sentCandidate = false;
+
+ private boolean acceptedAutomatically = false;
+
+ private JingleTransport transport = null;
+
+ private OnIqPacketReceived responseListener = new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE_ERROR) {
+ if (initiator.equals(account.getFullJid())) {
+ mXmppConnectionService.markMessage(message,
+ Message.STATUS_SEND_FAILED);
+ }
+ mJingleStatus = JINGLE_STATUS_FAILED;
+ }
+ }
+ };
+
+ final OnFileTransmissionStatusChanged onFileTransmissionSatusChanged = new OnFileTransmissionStatusChanged() {
+
+ @Override
+ public void onFileTransmitted(DownloadableFile file) {
+ if (responder.equals(account.getFullJid())) {
+ sendSuccess();
+ if (acceptedAutomatically) {
+ message.markUnread();
+ JingleConnection.this.mXmppConnectionService
+ .getNotificationService().push(message);
+ }
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(file.getAbsolutePath(), options);
+ int imageHeight = options.outHeight;
+ int imageWidth = options.outWidth;
+ message.setBody(Long.toString(file.getSize()) + ','
+ + imageWidth + ',' + imageHeight);
+ mXmppConnectionService.databaseBackend.createMessage(message);
+ mXmppConnectionService.markMessage(message,
+ Message.STATUS_RECEIVED);
+ }
+ Log.d(Config.LOGTAG,
+ "sucessfully transmitted file:" + file.getAbsolutePath());
+ if (message.getEncryption() != Message.ENCRYPTION_PGP) {
+ Intent intent = new Intent(
+ Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
+ intent.setData(Uri.fromFile(file));
+ mXmppConnectionService.sendBroadcast(intent);
+ }
+ }
+
+ @Override
+ public void onFileTransferAborted() {
+ JingleConnection.this.sendCancel();
+ JingleConnection.this.cancel();
+ }
+ };
+
+ private OnProxyActivated onProxyActivated = new OnProxyActivated() {
+
+ @Override
+ public void success() {
+ if (initiator.equals(account.getFullJid())) {
+ Log.d(Config.LOGTAG, "we were initiating. sending file");
+ transport.send(file, onFileTransmissionSatusChanged);
+ } else {
+ transport.receive(file, onFileTransmissionSatusChanged);
+ Log.d(Config.LOGTAG, "we were responding. receiving file");
+ }
+ }
+
+ @Override
+ public void failed() {
+ Log.d(Config.LOGTAG, "proxy activation failed");
+ }
+ };
+
+ public JingleConnection(JingleConnectionManager mJingleConnectionManager) {
+ this.mJingleConnectionManager = mJingleConnectionManager;
+ this.mXmppConnectionService = mJingleConnectionManager
+ .getXmppConnectionService();
+ }
+
+ public String getSessionId() {
+ return this.sessionId;
+ }
+
+ public Account getAccount() {
+ return this.account;
+ }
+
+ public String 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.cancel();
+ } else if (reason.hasChild("success")) {
+ this.receiveSuccess();
+ } else {
+ this.cancel();
+ }
+ } else {
+ this.cancel();
+ }
+ } 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;
+ Log.d(Config.LOGTAG, "trying to fallback to something unknown"
+ + packet.toString());
+ }
+ } else if (packet.isAction("transport-accept")) {
+ returnResult = this.receiveTransportAccept(packet);
+ } else {
+ Log.d(Config.LOGTAG, "packet arrived in connection. action was "
+ + packet.getAction());
+ returnResult = false;
+ }
+ IqPacket response;
+ if (returnResult) {
+ response = packet.generateRespone(IqPacket.TYPE_RESULT);
+
+ } else {
+ response = packet.generateRespone(IqPacket.TYPE_ERROR);
+ }
+ account.getXmppConnection().sendIqPacket(response, null);
+ }
+
+ public void init(Message message) {
+ this.contentCreator = "initiator";
+ this.contentName = this.mJingleConnectionManager.nextRandomId();
+ this.message = message;
+ this.account = message.getConversation().getAccount();
+ this.initiator = this.account.getFullJid();
+ 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() {
+ Log.d(Config.LOGTAG,
+ "connection to our own primary candidete failed");
+ sendInitRequest();
+ }
+
+ @Override
+ public void established() {
+ Log.d(Config.LOGTAG,
+ "succesfully connected to our own primary candidate");
+ mergeCandidate(candidate);
+ sendInitRequest();
+ }
+ });
+ mergeCandidate(candidate);
+ } else {
+ Log.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().split("/", 2)[0], false);
+ this.message = new Message(conversation, "", Message.ENCRYPTION_NONE);
+ this.message.setStatus(Message.STATUS_RECEIVED);
+ this.message.setType(Message.TYPE_IMAGE);
+ this.mStatus = Downloadable.STATUS_OFFER;
+ this.message.setDownloadable(this);
+ String[] fromParts = packet.getFrom().split("/", 2);
+ this.message.setPresence(fromParts[1]);
+ this.account = account;
+ this.initiator = packet.getFrom();
+ this.responder = this.account.getFullJid();
+ 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();
+ if (fileOffer != null) {
+ Element fileSize = fileOffer.findChild("size");
+ Element fileNameElement = fileOffer.findChild("name");
+ if (fileNameElement != null) {
+ boolean supportedFile = false;
+ String[] filename = fileNameElement.getContent()
+ .toLowerCase(Locale.US).split("\\.");
+ if (Arrays.asList(this.extensions).contains(
+ filename[filename.length - 1])) {
+ supportedFile = true;
+ } else if (Arrays.asList(this.cryptoExtensions).contains(
+ filename[filename.length - 1])) {
+ if (filename.length == 3) {
+ if (Arrays.asList(this.extensions).contains(
+ filename[filename.length - 2])) {
+ supportedFile = true;
+ if (filename[filename.length - 1].equals("otr")) {
+ Log.d(Config.LOGTAG, "receiving otr file");
+ this.message
+ .setEncryption(Message.ENCRYPTION_OTR);
+ } else {
+ this.message
+ .setEncryption(Message.ENCRYPTION_PGP);
+ }
+ }
+ }
+ }
+ if (supportedFile) {
+ long size = Long.parseLong(fileSize.getContent());
+ message.setBody(Long.toString(size));
+ conversation.add(message);
+ mXmppConnectionService.updateConversationUi();
+ if (size <= this.mJingleConnectionManager
+ .getAutoAcceptFileSize()) {
+ Log.d(Config.LOGTAG, "auto accepting file from "
+ + packet.getFrom());
+ this.acceptedAutomatically = true;
+ this.sendAccept();
+ } else {
+ message.markUnread();
+ Log.d(Config.LOGTAG,
+ "not auto accepting new file offer with size: "
+ + size
+ + " allowed size:"
+ + this.mJingleConnectionManager
+ .getAutoAcceptFileSize());
+ this.mXmppConnectionService.getNotificationService()
+ .push(message);
+ }
+ this.file = this.mXmppConnectionService.getFileBackend()
+ .getFile(message, false);
+ if (message.getEncryption() == Message.ENCRYPTION_OTR) {
+ byte[] key = conversation.getSymmetricKey();
+ if (key == null) {
+ this.sendCancel();
+ this.cancel();
+ return;
+ } else {
+ this.file.setKey(key);
+ }
+ }
+ this.file.setExpectedSize(size);
+ } else {
+ this.sendCancel();
+ this.cancel();
+ }
+ } else {
+ this.sendCancel();
+ this.cancel();
+ }
+ } else {
+ this.sendCancel();
+ this.cancel();
+ }
+ }
+
+ private void sendInitRequest() {
+ JinglePacket packet = this.bootstrapPacket("session-initiate");
+ Content content = new Content(this.contentCreator, this.contentName);
+ if (message.getType() == Message.TYPE_IMAGE) {
+ content.setTransportId(this.transportId);
+ this.file = this.mXmppConnectionService.getFileBackend().getFile(
+ message, false);
+ if (message.getEncryption() == Message.ENCRYPTION_OTR) {
+ Conversation conversation = this.message.getConversation();
+ this.mXmppConnectionService.renewSymmetricKey(conversation);
+ content.setFileOffer(this.file, true);
+ this.file.setKey(conversation.getSymmetricKey());
+ } else {
+ content.setFileOffer(this.file, false);
+ }
+ this.transportId = this.mJingleConnectionManager.nextRandomId();
+ content.setTransportId(this.transportId);
+ content.socks5transport().setChildren(getCandidatesAsElements());
+ packet.setContent(content);
+ this.sendJinglePacket(packet);
+ this.mJingleStatus = JINGLE_STATUS_INITIATED;
+ }
+ }
+
+ private List<Element> getCandidatesAsElements() {
+ List<Element> elements = new ArrayList<Element>();
+ for (JingleCandidate c : this.candidates) {
+ elements.add(c.toElement());
+ }
+ return elements;
+ }
+
+ private void sendAccept() {
+ mJingleStatus = JINGLE_STATUS_ACCEPTED;
+ this.mStatus = Downloadable.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) && (!equalCandidateExists(candidate))) {
+ final JingleSocks5Transport socksConnection = new JingleSocks5Transport(
+ JingleConnection.this, candidate);
+ connections.put(candidate.getCid(), socksConnection);
+ socksConnection.connect(new OnTransportConnected() {
+
+ @Override
+ public void failed() {
+ Log.d(Config.LOGTAG,
+ "connection to our own primary candidate failed");
+ content.socks5transport().setChildren(
+ getCandidatesAsElements());
+ packet.setContent(content);
+ sendJinglePacket(packet);
+ connectNextCandidate();
+ }
+
+ @Override
+ public void established() {
+ Log.d(Config.LOGTAG,
+ "connected to primary candidate");
+ mergeCandidate(candidate);
+ content.socks5transport().setChildren(
+ getCandidatesAsElements());
+ packet.setContent(content);
+ sendJinglePacket(packet);
+ connectNextCandidate();
+ }
+ });
+ } else {
+ Log.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.getFullJid());
+ packet.setTo(this.message.getCounterpart());
+ packet.setSessionId(this.sessionId);
+ packet.setInitiator(this.initiator);
+ return packet;
+ }
+
+ private void sendJinglePacket(JinglePacket packet) {
+ // Log.d(Config.LOGTAG,packet.toString());
+ account.getXmppConnection().sendIqPacket(packet, responseListener);
+ }
+
+ private boolean receiveAccept(JinglePacket packet) {
+ Content content = packet.getJingleContent();
+ mergeCandidates(JingleCandidate.parse(content.socks5transport()
+ .getChildren()));
+ this.mJingleStatus = JINGLE_STATUS_ACCEPTED;
+ mXmppConnectionService.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");
+ Log.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 {
+ Log.d(Config.LOGTAG, "activated connection not found");
+ this.sendCancel();
+ this.cancel();
+ }
+ }
+ return true;
+ } else if (content.socks5transport().hasChild("proxy-error")) {
+ onProxyActivated.failed();
+ return true;
+ } else if (content.socks5transport().hasChild("candidate-error")) {
+ Log.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) {
+ Log.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 {
+ Log.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) {
+ Log.d(Config.LOGTAG, "could not find suitable candidate");
+ this.disconnect();
+ if (this.initiator.equals(account.getFullJid())) {
+ this.sendFallbackToIbb();
+ }
+ } else {
+ this.mJingleStatus = JINGLE_STATUS_TRANSMITTING;
+ if (connection.needsActivation()) {
+ if (connection.getCandidate().isOurs()) {
+ Log.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());
+ this.account.getXmppConnection().sendIqPacket(activation,
+ new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account,
+ IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE_ERROR) {
+ onProxyActivated.failed();
+ } else {
+ onProxyActivated.success();
+ sendProxyActivated(connection
+ .getCandidate().getCid());
+ }
+ }
+ });
+ } else {
+ Log.d(Config.LOGTAG,
+ "candidate "
+ + connection.getCandidate().getCid()
+ + " was a proxy. waiting for other party to activate");
+ }
+ } else {
+ if (initiator.equals(account.getFullJid())) {
+ Log.d(Config.LOGTAG, "we were initiating. sending file");
+ connection.send(file, onFileTransmissionSatusChanged);
+ } else {
+ Log.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();
+ // Log.d(Config.LOGTAG,"comparing candidate: "+currentConnection.getCandidate().toString());
+ if (currentConnection.isEstablished()
+ && (currentConnection.getCandidate().isUsedByCounterpart() || (!currentConnection
+ .getCandidate().isOurs()))) {
+ // Log.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()) {
+ // Log.d(Config.LOGTAG,"found two candidates with same priority");
+ if (initiator.equals(account.getFullJid())) {
+ 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.disconnect();
+ this.mJingleStatus = JINGLE_STATUS_FINISHED;
+ this.message.setStatus(Message.STATUS_RECEIVED);
+ this.message.setDownloadable(null);
+ this.mXmppConnectionService.updateMessage(message);
+ this.mJingleConnectionManager.finishConnection(this);
+ }
+
+ private void sendFallbackToIbb() {
+ Log.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) {
+ Log.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.account,
+ this.responder, 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",
+ Integer.toString(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.account,
+ this.responder, this.transportId, this.ibbBlockSize);
+ this.transport.connect(new OnTransportConnected() {
+
+ @Override
+ public void failed() {
+ Log.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;
+ this.mXmppConnectionService.markMessage(this.message,
+ Message.STATUS_SEND);
+ this.disconnect();
+ this.mJingleConnectionManager.finishConnection(this);
+ }
+
+ public void cancel() {
+ this.mJingleStatus = JINGLE_STATUS_CANCELED;
+ this.disconnect();
+ if (this.message != null) {
+ if (this.responder.equals(account.getFullJid())) {
+ this.mStatus = Downloadable.STATUS_FAILED;
+ this.mXmppConnectionService.updateConversationUi();
+ } else {
+ if (this.mJingleStatus == JINGLE_STATUS_INITIATED) {
+ this.mXmppConnectionService.markMessage(this.message,
+ Message.STATUS_SEND_REJECTED);
+ } else {
+ this.mXmppConnectionService.markMessage(this.message,
+ Message.STATUS_SEND_FAILED);
+ }
+ }
+ }
+ 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() {
+ Log.d(Config.LOGTAG,
+ "connection failed with " + candidate.getHost() + ":"
+ + candidate.getPort());
+ connectNextCandidate();
+ }
+
+ @Override
+ public void established() {
+ Log.d(Config.LOGTAG,
+ "established connection with " + candidate.getHost()
+ + ":" + candidate.getPort());
+ sendCandidateUsed(candidate.getCid());
+ }
+ });
+ }
+
+ private void disconnect() {
+ 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 String getInitiator() {
+ return this.initiator;
+ }
+
+ public String 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;
+ }
+
+ 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.STATUS_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;
+ }
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java
new file mode 100644
index 00000000..1e7c84d4
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java
@@ -0,0 +1,163 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import java.math.BigInteger;
+import java.security.SecureRandom;
+import java.util.HashMap;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import android.annotation.SuppressLint;
+import android.util.Log;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.services.AbstractConnectionManager;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.OnIqPacketReceived;
+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<JingleConnection>();
+
+ private HashMap<String, JingleCandidate> primaryCandidates = new HashMap<String, JingleCandidate>();
+
+ @SuppressLint("TrulyRandom")
+ private SecureRandom random = new SecureRandom();
+
+ public JingleConnectionManager(XmppConnectionService service) {
+ super(service);
+ }
+
+ 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;
+ }
+ }
+ account.getXmppConnection().sendIqPacket(
+ packet.generateRespone(IqPacket.TYPE_ERROR), null);
+ }
+ }
+
+ public JingleConnection createNewConnection(Message message) {
+ JingleConnection connection = new JingleConnection(this);
+ connection.init(message);
+ this.connections.add(connection);
+ return connection;
+ }
+
+ public JingleConnection createNewConnection(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 (!this.primaryCandidates.containsKey(account.getJid())) {
+ String xmlns = "http://jabber.org/protocol/bytestreams";
+ final String proxy = account.getXmppConnection()
+ .findDiscoItemByFeature(xmlns);
+ if (proxy != null) {
+ IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
+ iq.setTo(proxy);
+ iq.query(xmlns);
+ account.getXmppConnection().sendIqPacket(iq,
+ new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account,
+ IqPacket packet) {
+ Element streamhost = packet
+ .query()
+ .findChild("streamhost",
+ "http://jabber.org/protocol/bytestreams");
+ if (streamhost != null) {
+ JingleCandidate candidate = new JingleCandidate(
+ nextRandomId(), true);
+ candidate.setHost(streamhost
+ .getAttribute("host"));
+ candidate.setPort(Integer
+ .parseInt(streamhost
+ .getAttribute("port")));
+ candidate
+ .setType(JingleCandidate.TYPE_PROXY);
+ candidate.setJid(proxy);
+ candidate.setPriority(655360 + 65535);
+ primaryCandidates.put(account.getJid(),
+ candidate);
+ listener.onPrimaryCandidateFound(true,
+ candidate);
+ } else {
+ listener.onPrimaryCandidateFound(false,
+ null);
+ }
+ }
+ });
+ } else {
+ listener.onPrimaryCandidateFound(false, null);
+ }
+
+ } else {
+ listener.onPrimaryCandidateFound(true,
+ this.primaryCandidates.get(account.getJid()));
+ }
+ }
+
+ 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;
+ }
+ }
+ }
+ Log.d(Config.LOGTAG,
+ "couldnt deliver payload: " + payload.toString());
+ } else {
+ Log.d(Config.LOGTAG, "no sid found in incomming ibb packet");
+ }
+ }
+
+ public void cancelInTransmission() {
+ for (JingleConnection connection : this.connections) {
+ if (connection.getJingleStatus() == JingleConnection.JINGLE_STATUS_TRANSMITTING) {
+ connection.cancel();
+ }
+ }
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java
new file mode 100644
index 00000000..cc1e92f6
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java
@@ -0,0 +1,191 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+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 android.util.Base64;
+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.stanzas.IqPacket;
+
+public class JingleInbandTransport extends JingleTransport {
+
+ private Account account;
+ private String counterpart;
+ private int blockSize;
+ private int bufferSize;
+ private int seq = 0;
+ private String sessionId;
+
+ private boolean established = false;
+
+ private DownloadableFile file;
+
+ private InputStream fileInputStream = null;
+ private OutputStream fileOutputStream;
+ private long remainingSize;
+ private MessageDigest digest;
+
+ private OnFileTransmissionStatusChanged onFileTransmissionStatusChanged;
+
+ private OnIqPacketReceived onAckReceived = new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE_RESULT) {
+ sendNextBlock();
+ }
+ }
+ };
+
+ public JingleInbandTransport(Account account, String counterpart,
+ String sid, int blocksize) {
+ this.account = account;
+ this.counterpart = counterpart;
+ this.blockSize = blocksize;
+ this.bufferSize = blocksize / 4;
+ 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.account.getXmppConnection().sendIqPacket(iq,
+ new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account,
+ IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE_ERROR) {
+ 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 = file.createOutputStream();
+ if (this.fileOutputStream == null) {
+ callback.onFileTransferAborted();
+ return;
+ }
+ this.remainingSize = file.getExpectedSize();
+ } catch (NoSuchAlgorithmException e) {
+ callback.onFileTransferAborted();
+ } catch (IOException e) {
+ callback.onFileTransferAborted();
+ }
+ }
+
+ @Override
+ public void send(DownloadableFile file,
+ OnFileTransmissionStatusChanged callback) {
+ this.onFileTransmissionStatusChanged = callback;
+ this.file = file;
+ try {
+ this.digest = MessageDigest.getInstance("SHA-1");
+ this.digest.reset();
+ fileInputStream = this.file.createInputStream();
+ if (fileInputStream == null) {
+ callback.onFileTransferAborted();
+ return;
+ }
+ this.sendNextBlock();
+ } catch (NoSuchAlgorithmException e) {
+ callback.onFileTransferAborted();
+ }
+ }
+
+ private void sendNextBlock() {
+ byte[] buffer = new byte[this.bufferSize];
+ try {
+ int count = fileInputStream.read(buffer);
+ if (count == -1) {
+ file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest()));
+ fileInputStream.close();
+ this.onFileTransmissionStatusChanged.onFileTransmitted(file);
+ } else {
+ this.digest.update(buffer);
+ String base64 = Base64.encodeToString(buffer, 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.seq++;
+ }
+ } catch (IOException e) {
+ 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);
+ }
+ } catch (IOException e) {
+ this.onFileTransmissionStatusChanged.onFileTransferAborted();
+ }
+ }
+
+ public void deliverPayload(IqPacket packet, Element payload) {
+ if (payload.getName().equals("open")) {
+ if (!established) {
+ established = true;
+ this.account.getXmppConnection().sendIqPacket(
+ packet.generateRespone(IqPacket.TYPE_RESULT), null);
+ } else {
+ this.account.getXmppConnection().sendIqPacket(
+ packet.generateRespone(IqPacket.TYPE_ERROR), null);
+ }
+ } else if (payload.getName().equals("data")) {
+ this.receiveNextBlock(payload.getContent());
+ this.account.getXmppConnection().sendIqPacket(
+ packet.generateRespone(IqPacket.TYPE_RESULT), null);
+ } else {
+ // TODO some sort of exception
+ }
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java
new file mode 100644
index 00000000..1da2f0cd
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java
@@ -0,0 +1,212 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.utils.CryptoHelper;
+
+public class JingleSocks5Transport extends JingleTransport {
+ private JingleCandidate candidate;
+ 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;
+ try {
+ MessageDigest mDigest = MessageDigest.getInstance("SHA-1");
+ StringBuilder destBuilder = new StringBuilder();
+ destBuilder.append(jingleConnection.getSessionId());
+ if (candidate.isOurs()) {
+ destBuilder.append(jingleConnection.getAccount().getFullJid());
+ destBuilder.append(jingleConnection.getCounterPart());
+ } else {
+ destBuilder.append(jingleConnection.getCounterPart());
+ destBuilder.append(jingleConnection.getAccount().getFullJid());
+ }
+ 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(candidate.getHost(),
+ candidate.getPort());
+ inputStream = socket.getInputStream();
+ outputStream = socket.getOutputStream();
+ byte[] login = { 0x05, 0x01, 0x00 };
+ byte[] expectedReply = { 0x05, 0x00 };
+ byte[] reply = new byte[2];
+ outputStream.write(login);
+ inputStream.read(reply);
+ final String connect = Character.toString('\u0005')
+ + '\u0001' + '\u0000' + '\u0003' + '\u0028'
+ + destination + '\u0000' + '\u0000';
+ if (Arrays.equals(reply, expectedReply)) {
+ outputStream.write(connect.getBytes());
+ byte[] result = new byte[2];
+ inputStream.read(result);
+ int status = result[1];
+ if (status == 0) {
+ isEstablished = true;
+ callback.established();
+ } else {
+ callback.failed();
+ }
+ } else {
+ socket.close();
+ callback.failed();
+ }
+ } 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;
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-1");
+ digest.reset();
+ fileInputStream = file.createInputStream();
+ if (fileInputStream == null) {
+ callback.onFileTransferAborted();
+ return;
+ }
+ int count;
+ byte[] buffer = new byte[8192];
+ while ((count = fileInputStream.read(buffer)) > 0) {
+ outputStream.write(buffer, 0, count);
+ digest.update(buffer, 0, count);
+ }
+ outputStream.flush();
+ file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest()));
+ if (callback != null) {
+ callback.onFileTransmitted(file);
+ }
+ } catch (FileNotFoundException e) {
+ callback.onFileTransferAborted();
+ } catch (IOException e) {
+ callback.onFileTransferAborted();
+ } catch (NoSuchAlgorithmException e) {
+ callback.onFileTransferAborted();
+ } finally {
+ try {
+ if (fileInputStream != null) {
+ fileInputStream.close();
+ }
+ } catch (IOException e) {
+ callback.onFileTransferAborted();
+ }
+ }
+ }
+ }).start();
+
+ }
+
+ public void receive(final DownloadableFile file,
+ final OnFileTransmissionStatusChanged callback) {
+ new Thread(new Runnable() {
+
+ @Override
+ public void run() {
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-1");
+ digest.reset();
+ inputStream.skip(45);
+ socket.setSoTimeout(30000);
+ file.getParentFile().mkdirs();
+ file.createNewFile();
+ OutputStream fileOutputStream = file.createOutputStream();
+ if (fileOutputStream == null) {
+ callback.onFileTransferAborted();
+ return;
+ }
+ long remainingSize = file.getExpectedSize();
+ byte[] buffer = new byte[8192];
+ int count = buffer.length;
+ while (remainingSize > 0) {
+ count = inputStream.read(buffer);
+ if (count == -1) {
+ callback.onFileTransferAborted();
+ return;
+ } else {
+ fileOutputStream.write(buffer, 0, count);
+ digest.update(buffer, 0, count);
+ remainingSize -= count;
+ }
+ }
+ fileOutputStream.flush();
+ fileOutputStream.close();
+ file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest()));
+ callback.onFileTransmitted(file);
+ } catch (FileNotFoundException e) {
+ callback.onFileTransferAborted();
+ } catch (IOException e) {
+ callback.onFileTransferAborted();
+ } catch (NoSuchAlgorithmException e) {
+ callback.onFileTransferAborted();
+ }
+ }
+ }).start();
+ }
+
+ public boolean isProxy() {
+ return this.candidate.getType() == JingleCandidate.TYPE_PROXY;
+ }
+
+ public boolean needsActivation() {
+ return (this.isProxy() && !this.activated);
+ }
+
+ public void disconnect() {
+ if (this.socket != null) {
+ try {
+ this.socket.close();
+ } catch (IOException e) {
+
+ }
+ }
+ }
+
+ public boolean isEstablished() {
+ return this.isEstablished;
+ }
+
+ public JingleCandidate getCandidate() {
+ return this.candidate;
+ }
+
+ public void setActivated(boolean activated) {
+ this.activated = activated;
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java
new file mode 100644
index 00000000..1374e61c
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java
@@ -0,0 +1,13 @@
+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);
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnFileTransmissionStatusChanged.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnFileTransmissionStatusChanged.java
new file mode 100644
index 00000000..e45e7441
--- /dev/null
+++ b/conversations/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 {
+ public void onFileTransmitted(DownloadableFile file);
+
+ public void onFileTransferAborted();
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java
new file mode 100644
index 00000000..2aaf62a1
--- /dev/null
+++ b/conversations/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 {
+ public void onJinglePacketReceived(Account account, JinglePacket packet);
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java
new file mode 100644
index 00000000..03a437b2
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java
@@ -0,0 +1,6 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+public interface OnPrimaryCandidateFound {
+ public void onPrimaryCandidateFound(boolean success,
+ JingleCandidate canditate);
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnTransportConnected.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnTransportConnected.java
new file mode 100644
index 00000000..38f03c5d
--- /dev/null
+++ b/conversations/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/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java
new file mode 100644
index 00000000..bcadbe77
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java
@@ -0,0 +1,102 @@
+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 void 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.getSize()));
+ if (otr) {
+ file.addChild("name").setContent(actualFile.getName() + ".otr");
+ } else {
+ file.addChild("name").setContent(actualFile.getName());
+ }
+ }
+
+ 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/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java
new file mode 100644
index 00000000..77a73643
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java
@@ -0,0 +1,95 @@
+package eu.siacs.conversations.xmpp.jingle.stanzas;
+
+import eu.siacs.conversations.xml.Element;
+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(String initiator) {
+ this.jingle.setAttribute("initiator", initiator);
+ }
+
+ public boolean isAction(String action) {
+ return action.equalsIgnoreCase(this.getAction());
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java
new file mode 100644
index 00000000..610d5e76
--- /dev/null
+++ b/conversations/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/conversations/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java
new file mode 100644
index 00000000..154fadf6
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java
@@ -0,0 +1,71 @@
+package eu.siacs.conversations.xmpp.pep;
+
+import eu.siacs.conversations.xml.Element;
+import android.util.Base64;
+
+public class Avatar {
+ public String type;
+ public String sha1sum;
+ public String image;
+ public int height;
+ public int width;
+ public long size;
+ public String owner;
+
+ public byte[] getImageAsBytes() {
+ return Base64.decode(image, Base64.DEFAULT);
+ }
+
+ public String getFilename() {
+ if (type == null) {
+ return sha1sum;
+ } else if (type.equalsIgnoreCase("image/webp")) {
+ return sha1sum + ".webp";
+ } else if (type.equalsIgnoreCase("image/png")) {
+ return sha1sum + ".png";
+ } else {
+ 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");
+ avatar.sha1sum = child.getAttribute("id");
+ return avatar;
+ }
+ }
+ return null;
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java
new file mode 100644
index 00000000..eef41c79
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java
@@ -0,0 +1,34 @@
+package eu.siacs.conversations.xmpp.stanzas;
+
+import eu.siacs.conversations.xml.Element;
+
+public class AbstractStanza extends Element {
+
+ protected AbstractStanza(String name) {
+ super(name);
+ }
+
+ public String getTo() {
+ return getAttribute("to");
+ }
+
+ public String getFrom() {
+ return getAttribute("from");
+ }
+
+ public String getId() {
+ return this.getAttribute("id");
+ }
+
+ public void setTo(String to) {
+ setAttribute("to", to);
+ }
+
+ public void setFrom(String from) {
+ setAttribute("from", from);
+ }
+
+ public void setId(String id) {
+ setAttribute("id", id);
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java
new file mode 100644
index 00000000..9df05e67
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java
@@ -0,0 +1,76 @@
+package eu.siacs.conversations.xmpp.stanzas;
+
+import eu.siacs.conversations.xml.Element;
+
+public class IqPacket extends AbstractStanza {
+
+ public static final int TYPE_ERROR = -1;
+ public static final int TYPE_SET = 0;
+ public static final int TYPE_RESULT = 1;
+ public static final int TYPE_GET = 2;
+
+ private IqPacket(String name) {
+ super(name);
+ }
+
+ public IqPacket(int type) {
+ super("iq");
+ switch (type) {
+ case TYPE_SET:
+ this.setAttribute("type", "set");
+ break;
+ case TYPE_GET:
+ this.setAttribute("type", "get");
+ break;
+ case TYPE_RESULT:
+ this.setAttribute("type", "result");
+ break;
+ case TYPE_ERROR:
+ this.setAttribute("type", "error");
+ break;
+ default:
+ break;
+ }
+ }
+
+ public IqPacket() {
+ super("iq");
+ }
+
+ public Element query() {
+ Element query = findChild("query");
+ if (query == null) {
+ query = addChild("query");
+ }
+ return query;
+ }
+
+ public Element query(String xmlns) {
+ Element query = query();
+ query.setAttribute("xmlns", xmlns);
+ return query();
+ }
+
+ public int getType() {
+ String type = getAttribute("type");
+ if ("error".equals(type)) {
+ return TYPE_ERROR;
+ } else if ("result".equals(type)) {
+ return TYPE_RESULT;
+ } else if ("set".equals(type)) {
+ return TYPE_SET;
+ } else if ("get".equals(type)) {
+ return TYPE_GET;
+ } else {
+ return 1000;
+ }
+ }
+
+ public IqPacket generateRespone(int type) {
+ IqPacket packet = new IqPacket(type);
+ packet.setTo(this.getFrom());
+ packet.setId(this.getId());
+ return packet;
+ }
+
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java
new file mode 100644
index 00000000..4e7b532b
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java
@@ -0,0 +1,66 @@
+package eu.siacs.conversations.xmpp.stanzas;
+
+import eu.siacs.conversations.xml.Element;
+
+public class MessagePacket extends AbstractStanza {
+ 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() {
+ Element body = this.findChild("body");
+ if (body != null) {
+ return body.getContent();
+ } else {
+ return null;
+ }
+ }
+
+ public void setBody(String text) {
+ this.children.remove(findChild("body"));
+ Element body = new Element("body");
+ body.setContent(text);
+ this.children.add(body);
+ }
+
+ 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;
+ 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;
+ }
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/PresencePacket.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/PresencePacket.java
new file mode 100644
index 00000000..7ea32099
--- /dev/null
+++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/PresencePacket.java
@@ -0,0 +1,8 @@
+package eu.siacs.conversations.xmpp.stanzas;
+
+public class PresencePacket extends AbstractStanza {
+
+ public PresencePacket() {
+ super("presence");
+ }
+}
diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/ActivePacket.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/ActivePacket.java
new file mode 100644
index 00000000..78ab66d8
--- /dev/null
+++ b/conversations/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/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/InactivePacket.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/InactivePacket.java
new file mode 100644
index 00000000..f109280f
--- /dev/null
+++ b/conversations/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/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/AckPacket.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/AckPacket.java
new file mode 100644
index 00000000..f93b5d87
--- /dev/null
+++ b/conversations/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/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/EnablePacket.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/EnablePacket.java
new file mode 100644
index 00000000..78cd81ed
--- /dev/null
+++ b/conversations/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/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/RequestPacket.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/RequestPacket.java
new file mode 100644
index 00000000..98cfc748
--- /dev/null
+++ b/conversations/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/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/ResumePacket.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/ResumePacket.java
new file mode 100644
index 00000000..9cdcfa5e
--- /dev/null
+++ b/conversations/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));
+ }
+
+}