diff options
Diffstat (limited to 'src')
83 files changed, 4561 insertions, 2469 deletions
diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 30622887..937b6813 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -96,6 +96,10 @@ android:label="@string/mgmt_account_publish_avatar" android:windowSoftInputMode="stateHidden" /> <activity + android:name=".ui.VerifyOTRActivity" + android:label="@string/verify_otr" + android:windowSoftInputMode="stateHidden" /> + <activity android:name=".ui.ShareWithActivity" android:label="@string/title_activity_conversations" > <intent-filter> diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index 7dd5a799..7af29451 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -19,6 +19,9 @@ public final class Config { public static final int MESSAGE_MERGE_WINDOW = 20; public static final boolean PARSE_EMOTICONS = false; + public static final int PROGRESS_UI_UPDATE_INTERVAL = 750; + + public static final boolean NO_PROXY_LOOKUP = false; //useful to debug ibb private Config() { diff --git a/src/main/java/eu/siacs/conversations/crypto/OtrEngine.java b/src/main/java/eu/siacs/conversations/crypto/OtrEngine.java index e0bd0e79..8b2b704a 100644 --- a/src/main/java/eu/siacs/conversations/crypto/OtrEngine.java +++ b/src/main/java/eu/siacs/conversations/crypto/OtrEngine.java @@ -18,13 +18,18 @@ 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.services.XmppConnectionService; +import eu.siacs.conversations.xmpp.jid.InvalidJidException; +import eu.siacs.conversations.xmpp.jid.Jid; 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.crypto.OtrCryptoEngineImpl; +import net.java.otr4j.crypto.OtrCryptoException; import net.java.otr4j.session.InstanceTag; import net.java.otr4j.session.SessionID; @@ -85,18 +90,25 @@ public class OtrEngine implements OtrEngineHost { 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) { + } catch (final NoSuchAlgorithmException | InvalidKeySpecException e) { e.printStackTrace(); } - } + } @Override - public void askForSecret(SessionID arg0, InstanceTag arg1, String arg2) { - // TODO Auto-generated method stub - + public void askForSecret(SessionID id, InstanceTag instanceTag, String question) { + try { + final Jid jid = Jid.fromSessionID(id); + Conversation conversation = this.mXmppConnectionService.find(this.account,jid); + if (conversation!=null) { + conversation.smp().hint = question; + conversation.smp().status = Conversation.Smp.STATUS_CONTACT_REQUESTED; + mXmppConnectionService.updateConversationUi(); + } + } catch (InvalidJidException e) { + Log.d(Config.LOGTAG,account.getJid().toBareJid()+": smp in invalid session "+id.toString()); + } } @Override @@ -112,8 +124,11 @@ public class OtrEngine implements OtrEngineHost { @Override public byte[] getLocalFingerprintRaw(SessionID arg0) { - // TODO Auto-generated method stub - return null; + try { + return new OtrCryptoEngineImpl().getFingerprintRaw(getPublicKey()); + } catch (OtrCryptoException e) { + return null; + } } public PublicKey getPublicKey() { @@ -155,11 +170,11 @@ public class OtrEngine implements OtrEngineHost { public void injectMessage(SessionID session, String body) throws OtrException { MessagePacket packet = new MessagePacket(); - packet.setFrom(account.getFullJid()); + packet.setFrom(account.getJid()); if (session.getUserID().isEmpty()) { - packet.setTo(session.getAccountID()); + packet.setAttribute("to", session.getAccountID()); } else { - packet.setTo(session.getAccountID() + "/" + session.getUserID()); + packet.setAttribute("to", session.getAccountID() + "/" + session.getUserID()); } packet.setBody(body); packet.addChild("private", "urn:xmpp:carbons:2"); @@ -189,20 +204,31 @@ public class OtrEngine implements OtrEngineHost { @Override public void showError(SessionID arg0, String arg1) throws OtrException { - // TODO Auto-generated method stub - + Log.d(Config.LOGTAG,"show error"); } @Override - public void smpAborted(SessionID arg0) throws OtrException { - // TODO Auto-generated method stub + public void smpAborted(SessionID id) throws OtrException { + setSmpStatus(id, Conversation.Smp.STATUS_NONE); + } + private void setSmpStatus(SessionID id, int status) { + try { + final Jid jid = Jid.fromSessionID(id); + Conversation conversation = this.mXmppConnectionService.find(this.account,jid); + if (conversation!=null) { + conversation.smp().status = status; + mXmppConnectionService.updateConversationUi(); + } + } catch (final InvalidJidException ignored) { + + } } @Override - public void smpError(SessionID arg0, int arg1, boolean arg2) + public void smpError(SessionID id, int arg1, boolean arg2) throws OtrException { - throw new OtrException(new Exception("smp error")); + setSmpStatus(id, Conversation.Smp.STATUS_NONE); } @Override @@ -213,19 +239,29 @@ public class OtrEngine implements OtrEngineHost { @Override public void unreadableMessageReceived(SessionID arg0) throws OtrException { + Log.d(Config.LOGTAG,"unreadable message received"); throw new OtrException(new Exception("unreadable message received")); } @Override - public void unverify(SessionID arg0, String arg1) { - // TODO Auto-generated method stub - + public void unverify(SessionID id, String arg1) { + setSmpStatus(id, Conversation.Smp.STATUS_FAILED); } @Override - public void verify(SessionID arg0, String arg1, boolean arg2) { - // TODO Auto-generated method stub - + public void verify(SessionID id, String arg1, boolean arg2) { + try { + final Jid jid = Jid.fromSessionID(id); + Conversation conversation = this.mXmppConnectionService.find(this.account,jid); + if (conversation!=null) { + conversation.smp().hint = null; + conversation.smp().status = Conversation.Smp.STATUS_VERIFIED; + conversation.verifyOtrFingerprint(); + mXmppConnectionService.updateConversationUi(); + mXmppConnectionService.syncRosterToDisk(conversation.getAccount()); + } + } catch (final InvalidJidException ignored) { + } } } diff --git a/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java b/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java index 9a2b4a11..83d9b7b2 100644 --- a/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java +++ b/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java @@ -3,7 +3,6 @@ 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; @@ -24,7 +23,6 @@ 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.net.Uri; public class PgpEngine { @@ -41,7 +39,7 @@ public class PgpEngine { Intent params = new Intent(); params.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY); params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, message - .getConversation().getAccount().getJid()); + .getConversation().getAccount().getJid().toBareJid().toString()); if (message.getType() == Message.TYPE_TEXT) { InputStream is = new ByteArrayInputStream(message.getBody() .getBytes()); @@ -77,18 +75,16 @@ public class PgpEngine { return; case OpenPgpApi.RESULT_CODE_ERROR: callback.error(R.string.openpgp_error, message); - return; - default: - return; - } + } } }); - } else if (message.getType() == Message.TYPE_IMAGE) { + } else if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) { try { final DownloadableFile inputFile = this.mXmppConnectionService .getFileBackend().getFile(message, false); final DownloadableFile outputFile = this.mXmppConnectionService .getFileBackend().getFile(message, true); + outputFile.getParentFile().mkdirs(); outputFile.createNewFile(); InputStream is = new FileInputStream(inputFile); OutputStream os = new FileOutputStream(outputFile); @@ -100,24 +96,7 @@ public class PgpEngine { OpenPgpApi.RESULT_CODE_ERROR)) { case OpenPgpApi.RESULT_CODE_SUCCESS: URL url = message.getImageParams().url; - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - BitmapFactory.decodeFile( - outputFile.getAbsolutePath(), options); - int imageHeight = options.outHeight; - int imageWidth = options.outWidth; - if (url == null) { - message.setBody(Long.toString(outputFile - .getSize()) - + '|' - + imageWidth - + '|' - + imageHeight); - } else { - message.setBody(url.toString() + "|" - + Long.toString(outputFile.getSize()) - + '|' + imageWidth + '|' + imageHeight); - } + mXmppConnectionService.getFileBackend().updateFileParams(message,url); message.setEncryption(Message.ENCRYPTION_DECRYPTED); PgpEngine.this.mXmppConnectionService .updateMessage(message); @@ -135,15 +114,10 @@ public class PgpEngine { 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) { + } catch (final IOException e) { callback.error(R.string.error_decrypting_file, message); } @@ -164,7 +138,7 @@ public class PgpEngine { .getMucOptions().getPgpKeyIds()); } params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, message - .getConversation().getAccount().getJid()); + .getConversation().getAccount().getJid().toBareJid().toString()); if (message.getType() == Message.TYPE_TEXT) { params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); @@ -207,12 +181,13 @@ public class PgpEngine { } } }); - } else if (message.getType() == Message.TYPE_IMAGE) { + } else if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) { try { DownloadableFile inputFile = this.mXmppConnectionService .getFileBackend().getFile(message, true); DownloadableFile outputFile = this.mXmppConnectionService .getFileBackend().getFile(message, false); + outputFile.getParentFile().mkdirs(); outputFile.createNewFile(); InputStream is = new FileInputStream(inputFile); OutputStream os = new FileOutputStream(outputFile); @@ -237,12 +212,8 @@ public class PgpEngine { } } }); - } catch (FileNotFoundException e) { - callback.error(R.string.openpgp_error, message); - return; - } catch (IOException e) { + } catch (final IOException e) { callback.error(R.string.openpgp_error, message); - return; } } } @@ -254,7 +225,7 @@ public class PgpEngine { if (status == null) { status = ""; } - StringBuilder pgpSig = new StringBuilder(); + final StringBuilder pgpSig = new StringBuilder(); pgpSig.append("-----BEGIN PGP SIGNED MESSAGE-----"); pgpSig.append('\n'); pgpSig.append('\n'); @@ -269,7 +240,7 @@ public class PgpEngine { 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()); + params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, account.getJid().toBareJid().toString()); InputStream is = new ByteArrayInputStream(pgpSig.toString().getBytes()); ByteArrayOutputStream os = new ByteArrayOutputStream(); Intent result = api.executeApi(params, is, os); @@ -296,7 +267,7 @@ public class PgpEngine { 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()); + params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, account.getJid().toBareJid().toString()); InputStream is = new ByteArrayInputStream(status.getBytes()); final OutputStream os = new ByteArrayOutputStream(); api.executeApiAsync(params, is, os, new IOpenPgpCallback() { @@ -338,8 +309,7 @@ public class PgpEngine { return; case OpenPgpApi.RESULT_CODE_ERROR: callback.error(R.string.openpgp_error, account); - return; - } + } } }); } @@ -349,7 +319,7 @@ public class PgpEngine { params.setAction(OpenPgpApi.ACTION_GET_KEY); params.putExtra(OpenPgpApi.EXTRA_KEY_ID, contact.getPgpKeyId()); params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, contact.getAccount() - .getJid()); + .getJid().toBareJid().toString()); api.executeApiAsync(params, null, null, new IOpenPgpCallback() { @Override @@ -365,8 +335,7 @@ public class PgpEngine { return; case OpenPgpApi.RESULT_CODE_ERROR: callback.error(R.string.openpgp_error, contact); - return; - } + } } }); } @@ -376,7 +345,7 @@ public class PgpEngine { params.setAction(OpenPgpApi.ACTION_GET_KEY); params.putExtra(OpenPgpApi.EXTRA_KEY_ID, contact.getPgpKeyId()); params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, contact.getAccount() - .getJid()); + .getJid().toBareJid().toString()); Intent result = api.executeApi(params, null, null); return (PendingIntent) result .getParcelableExtra(OpenPgpApi.RESULT_INTENT); @@ -386,7 +355,7 @@ public class PgpEngine { 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()); + params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, account.getJid().toBareJid().toString()); Intent result = api.executeApi(params, null, null); return (PendingIntent) result .getParcelableExtra(OpenPgpApi.RESULT_INTENT); diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/DigestMd5.java b/src/main/java/eu/siacs/conversations/crypto/sasl/DigestMd5.java new file mode 100644 index 00000000..850cacc2 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/DigestMd5.java @@ -0,0 +1,87 @@ +package eu.siacs.conversations.crypto.sasl; + +import android.util.Base64; + +import java.math.BigInteger; +import java.nio.charset.Charset; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; + +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.utils.CryptoHelper; +import eu.siacs.conversations.xml.TagWriter; + +public class DigestMd5 extends SaslMechanism { + public DigestMd5(final TagWriter tagWriter, final Account account, final SecureRandom rng) { + super(tagWriter, account, rng); + } + + @Override + public int getPriority() { + return 10; + } + + @Override + public String getMechanism() { + return "DIGEST-MD5"; + } + + private State state = State.INITIAL; + + @Override + public String getResponse(final String challenge) throws AuthenticationException { + switch (state) { + case INITIAL: + state = State.RESPONSE_SENT; + final String encodedResponse; + try { + final Tokenizer tokenizer = new Tokenizer(Base64.decode(challenge, Base64.DEFAULT)); + String nonce = ""; + for (final String token : tokenizer) { + final String[] parts = token.split("="); + if (parts[0].equals("nonce")) { + nonce = parts[1].replace("\"", ""); + } else if (parts[0].equals("rspauth")) { + return ""; + } + } + final String digestUri = "xmpp/" + account.getServer(); + final String nonceCount = "00000001"; + final String x = account.getUsername() + ":" + account.getServer() + ":" + + account.getPassword(); + final MessageDigest md = MessageDigest.getInstance("MD5"); + final byte[] y = md.digest(x.getBytes(Charset.defaultCharset())); + final String cNonce = new BigInteger(100, rng).toString(32); + final byte[] a1 = CryptoHelper.concatenateByteArrays(y, + (":" + nonce + ":" + cNonce).getBytes(Charset.defaultCharset())); + final String a2 = "AUTHENTICATE:" + digestUri; + final String ha1 = CryptoHelper.bytesToHex(md.digest(a1)); + final String ha2 = CryptoHelper.bytesToHex(md.digest(a2.getBytes(Charset + .defaultCharset()))); + final String kd = ha1 + ":" + nonce + ":" + nonceCount + ":" + cNonce + + ":auth:" + ha2; + final String response = CryptoHelper.bytesToHex(md.digest(kd.getBytes(Charset + .defaultCharset()))); + final String saslString = "username=\"" + account.getUsername() + + "\",realm=\"" + account.getServer() + "\",nonce=\"" + + nonce + "\",cnonce=\"" + cNonce + "\",nc=" + nonceCount + + ",qop=auth,digest-uri=\"" + digestUri + "\",response=" + + response + ",charset=utf-8"; + encodedResponse = Base64.encodeToString( + saslString.getBytes(Charset.defaultCharset()), + Base64.NO_WRAP); + } catch (final NoSuchAlgorithmException e) { + throw new AuthenticationException(e); + } + + return encodedResponse; + case RESPONSE_SENT: + state = State.VALID_SERVER_RESPONSE; + break; + default: + throw new InvalidStateException(state); + } + return null; + } +} diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java b/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java new file mode 100644 index 00000000..c7dedc5e --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java @@ -0,0 +1,30 @@ +package eu.siacs.conversations.crypto.sasl; + +import android.util.Base64; + +import java.nio.charset.Charset; + +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.xml.TagWriter; + +public class Plain extends SaslMechanism { + public Plain(final TagWriter tagWriter, final Account account) { + super(tagWriter, account, null); + } + + @Override + public int getPriority() { + return 0; + } + + @Override + public String getMechanism() { + return "PLAIN"; + } + + @Override + public String getClientFirstMessage() { + final String sasl = '\u0000' + account.getUsername() + '\u0000' + account.getPassword(); + return Base64.encodeToString(sasl.getBytes(Charset.defaultCharset()), Base64.NO_WRAP); + } +} diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java new file mode 100644 index 00000000..14d8b944 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java @@ -0,0 +1,62 @@ +package eu.siacs.conversations.crypto.sasl; + +import java.security.SecureRandom; + +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.xml.TagWriter; + +public abstract class SaslMechanism { + + final protected TagWriter tagWriter; + final protected Account account; + final protected SecureRandom rng; + + protected static enum State { + INITIAL, + AUTH_TEXT_SENT, + RESPONSE_SENT, + VALID_SERVER_RESPONSE, + } + + public static class AuthenticationException extends Exception { + public AuthenticationException(final String message) { + super(message); + } + + public AuthenticationException(final Exception inner) { + super(inner); + } + } + + public static class InvalidStateException extends AuthenticationException { + public InvalidStateException(final String message) { + super(message); + } + + public InvalidStateException(final State state) { + this("Invalid state: " + state.toString()); + } + } + + public SaslMechanism(final TagWriter tagWriter, final Account account, final SecureRandom rng) { + this.tagWriter = tagWriter; + this.account = account; + this.rng = rng; + } + + /** + * The priority is used to pin the authentication mechanism. If authentication fails, it MAY be retried with another + * mechanism of the same priority, but MUST NOT be tried with a mechanism of lower priority (to prevent downgrade + * attacks). + * @return An arbitrary int representing the priority + */ + public abstract int getPriority(); + + public abstract String getMechanism(); + public String getClientFirstMessage() { + return ""; + } + public String getResponse(final String challenge) throws AuthenticationException { + return ""; + } +} diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java new file mode 100644 index 00000000..f5765cf1 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java @@ -0,0 +1,232 @@ +package eu.siacs.conversations.crypto.sasl; + +import android.util.Base64; +import android.util.LruCache; + +import org.bouncycastle.crypto.Digest; +import org.bouncycastle.crypto.digests.SHA1Digest; +import org.bouncycastle.crypto.macs.HMac; +import org.bouncycastle.crypto.params.KeyParameter; + +import java.math.BigInteger; +import java.nio.charset.Charset; +import java.security.InvalidKeyException; +import java.security.SecureRandom; + +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.utils.CryptoHelper; +import eu.siacs.conversations.xml.TagWriter; + +public class ScramSha1 extends SaslMechanism { + // TODO: When channel binding (SCRAM-SHA1-PLUS) is supported in future, generalize this to indicate support and/or usage. + final private static String GS2_HEADER = "n,,"; + private String clientFirstMessageBare; + private byte[] serverFirstMessage; + final private String clientNonce; + private byte[] serverSignature = null; + private static HMac HMAC; + private static Digest DIGEST; + private static final byte[] CLIENT_KEY_BYTES = "Client Key".getBytes(); + private static final byte[] SERVER_KEY_BYTES = "Server Key".getBytes(); + + public static class KeyPair { + final public byte[] clientKey; + final public byte[] serverKey; + + public KeyPair(final byte[] clientKey, final byte[] serverKey) { + this.clientKey = clientKey; + this.serverKey = serverKey; + } + } + + private static final LruCache<String, KeyPair> CACHE; + + static { + DIGEST = new SHA1Digest(); + HMAC = new HMac(new SHA1Digest()); + CACHE = new LruCache<String, KeyPair>(10) { + protected KeyPair create(final String k) { + // Map keys are "bytesToHex(JID),bytesToHex(password),bytesToHex(salt),iterations". + // Changing any of these values forces a cache miss. `CryptoHelper.bytesToHex()' + // is applied to prevent commas in the strings breaking things. + final String[] kparts = k.split(",", 4); + try { + final byte[] saltedPassword, serverKey, clientKey; + saltedPassword = hi(CryptoHelper.saslPrep(CryptoHelper.hexToString(kparts[1])).getBytes(), + Base64.decode(CryptoHelper.hexToString(kparts[2]), Base64.DEFAULT), Integer.valueOf(kparts[3])); + serverKey = hmac(saltedPassword, SERVER_KEY_BYTES); + clientKey = hmac(saltedPassword, CLIENT_KEY_BYTES); + + return new KeyPair(clientKey, serverKey); + } catch (final InvalidKeyException | NumberFormatException e) { + return null; + } + } + }; + } + + private State state = State.INITIAL; + + public ScramSha1(final TagWriter tagWriter, final Account account, final SecureRandom rng) { + super(tagWriter, account, rng); + + // This nonce should be different for each authentication attempt. + clientNonce = new BigInteger(100, this.rng).toString(32); + clientFirstMessageBare = ""; + } + + @Override + public int getPriority() { + return 20; + } + + @Override + public String getMechanism() { + return "SCRAM-SHA-1"; + } + + @Override + public String getClientFirstMessage() { + if (clientFirstMessageBare.isEmpty() && state == State.INITIAL) { + clientFirstMessageBare = "n=" + CryptoHelper.saslPrep(account.getUsername()) + + ",r=" + this.clientNonce; + state = State.AUTH_TEXT_SENT; + } + return Base64.encodeToString( + (GS2_HEADER + clientFirstMessageBare).getBytes(Charset.defaultCharset()), + Base64.NO_WRAP); + } + + @Override + public String getResponse(final String challenge) throws AuthenticationException { + switch (state) { + case AUTH_TEXT_SENT: + serverFirstMessage = Base64.decode(challenge, Base64.DEFAULT); + final Tokenizer tokenizer = new Tokenizer(serverFirstMessage); + String nonce = ""; + int iterationCount = -1; + String salt = ""; + for (final String token : tokenizer) { + if (token.charAt(1) == '=') { + switch (token.charAt(0)) { + case 'i': + try { + iterationCount = Integer.parseInt(token.substring(2)); + } catch (final NumberFormatException e) { + throw new AuthenticationException(e); + } + break; + case 's': + salt = token.substring(2); + break; + case 'r': + nonce = token.substring(2); + break; + case 'm': + /* + * RFC 5802: + * m: This attribute is reserved for future extensibility. In this + * version of SCRAM, its presence in a client or a server message + * MUST cause authentication failure when the attribute is parsed by + * the other end. + */ + throw new AuthenticationException("Server sent reserved token: `m'"); + } + } + } + + if (iterationCount < 0) { + throw new AuthenticationException("Server did not send iteration count"); + } + if (nonce.isEmpty() || !nonce.startsWith(clientNonce)) { + throw new AuthenticationException("Server nonce does not contain client nonce: " + nonce); + } + if (salt.isEmpty()) { + throw new AuthenticationException("Server sent empty salt"); + } + + final String clientFinalMessageWithoutProof = "c=" + Base64.encodeToString( + GS2_HEADER.getBytes(), Base64.NO_WRAP) + ",r=" + nonce; + final byte[] authMessage = (clientFirstMessageBare + ',' + new String(serverFirstMessage) + ',' + + clientFinalMessageWithoutProof).getBytes(); + + // Map keys are "bytesToHex(JID),bytesToHex(password),bytesToHex(salt),iterations". + final KeyPair keys = CACHE.get( + CryptoHelper.bytesToHex(account.getJid().toBareJid().toString().getBytes()) + "," + + CryptoHelper.bytesToHex(account.getPassword().getBytes()) + "," + + CryptoHelper.bytesToHex(salt.getBytes()) + "," + + String.valueOf(iterationCount) + ); + if (keys == null) { + throw new AuthenticationException("Invalid keys generated"); + } + final byte[] clientSignature; + try { + serverSignature = hmac(keys.serverKey, authMessage); + final byte[] storedKey = digest(keys.clientKey); + + clientSignature = hmac(storedKey, authMessage); + + } catch (final InvalidKeyException e) { + throw new AuthenticationException(e); + } + + final byte[] clientProof = new byte[keys.clientKey.length]; + + for (int i = 0; i < clientProof.length; i++) { + clientProof[i] = (byte) (keys.clientKey[i] ^ clientSignature[i]); + } + + + final String clientFinalMessage = clientFinalMessageWithoutProof + ",p=" + + Base64.encodeToString(clientProof, Base64.NO_WRAP); + state = State.RESPONSE_SENT; + return Base64.encodeToString(clientFinalMessage.getBytes(), Base64.NO_WRAP); + case RESPONSE_SENT: + final String clientCalculatedServerFinalMessage = "v=" + + Base64.encodeToString(serverSignature, Base64.NO_WRAP); + if (!clientCalculatedServerFinalMessage.equals(new String(Base64.decode(challenge, Base64.DEFAULT)))) { + throw new AuthenticationException("Server final message does not match calculated final message"); + } + state = State.VALID_SERVER_RESPONSE; + return ""; + default: + throw new InvalidStateException(state); + } + } + + public static synchronized byte[] hmac(final byte[] key, final byte[] input) + throws InvalidKeyException { + HMAC.init(new KeyParameter(key)); + HMAC.update(input, 0, input.length); + final byte[] out = new byte[HMAC.getMacSize()]; + HMAC.doFinal(out, 0); + return out; + } + + public static synchronized byte[] digest(byte[] bytes) { + DIGEST.reset(); + DIGEST.update(bytes, 0, bytes.length); + final byte[] out = new byte[DIGEST.getDigestSize()]; + DIGEST.doFinal(out, 0); + return out; + } + + /* + * Hi() is, essentially, PBKDF2 [RFC2898] with HMAC() as the + * pseudorandom function (PRF) and with dkLen == output length of + * HMAC() == output length of H(). + */ + private static synchronized byte[] hi(final byte[] key, final byte[] salt, final int iterations) + throws InvalidKeyException { + byte[] u = hmac(key, CryptoHelper.concatenateByteArrays(salt, CryptoHelper.ONE)); + byte[] out = u.clone(); + for (int i = 1; i < iterations; i++) { + u = hmac(key, u); + for (int j = 0; j < u.length; j++) { + out[j] ^= u[j]; + } + } + return out; + } +} diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/Tokenizer.java b/src/main/java/eu/siacs/conversations/crypto/sasl/Tokenizer.java new file mode 100644 index 00000000..4797e6e8 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/Tokenizer.java @@ -0,0 +1,76 @@ +package eu.siacs.conversations.crypto.sasl; + +import android.util.Base64; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +/** + * A tokenizer for GS2 header strings + */ +public final class Tokenizer implements Iterator<String>, Iterable<String> { + private final List<String> parts; + private int index; + + public Tokenizer(final byte[] challenge) { + final String challengeString = new String(challenge); + parts = new ArrayList<>(Arrays.asList(challengeString.split(","))); + index = 0; + } + + /** + * Returns true if there is at least one more element, false otherwise. + * + * @see #next + */ + @Override + public boolean hasNext() { + return parts.size() != index + 1; + } + + /** + * Returns the next object and advances the iterator. + * + * @return the next object. + * @throws java.util.NoSuchElementException if there are no more elements. + * @see #hasNext + */ + @Override + public String next() { + if (hasNext()) { + return parts.get(index++); + } else { + throw new NoSuchElementException("No such element. Size is: " + parts.size()); + } + } + + /** + * Removes the last object returned by {@code next} from the collection. + * This method can only be called once between each call to {@code next}. + * + * @throws UnsupportedOperationException if removing is not supported by the collection being + * iterated. + * @throws IllegalStateException if {@code next} has not been called, or {@code remove} has + * already been called after the last call to {@code next}. + */ + @Override + public void remove() { + if(index <= 0) { + throw new IllegalStateException("You can't delete an element before first next() method call"); + } + parts.remove(--index); + } + + /** + * Returns an {@link java.util.Iterator} for the elements in this object. + * + * @return An {@code Iterator} instance. + */ + @Override + public Iterator<String> iterator() { + return parts.iterator(); + } +} diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index 80a9d62f..1d0a025f 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -1,9 +1,8 @@ package eu.siacs.conversations.entities; -import java.security.interfaces.DSAPublicKey; -import java.util.List; -import java.util.Locale; -import java.util.concurrent.CopyOnWriteArrayList; +import android.content.ContentValues; +import android.database.Cursor; +import android.os.SystemClock; import net.java.otr4j.crypto.OtrCryptoEngineImpl; import net.java.otr4j.crypto.OtrCryptoException; @@ -11,14 +10,17 @@ import net.java.otr4j.crypto.OtrCryptoException; import org.json.JSONException; import org.json.JSONObject; +import java.security.interfaces.DSAPublicKey; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + 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; +import eu.siacs.conversations.xmpp.jid.InvalidJidException; +import eu.siacs.conversations.xmpp.jid.Jid; public class Account extends AbstractEntity { @@ -32,73 +34,138 @@ public class Account extends AbstractEntity { public static final String KEYS = "keys"; public static final String AVATAR = "avatar"; + public static final String PINNED_MECHANISM_KEY = "pinned_mechanism"; + public static final int OPTION_USETLS = 0; public static final int OPTION_DISABLED = 1; public static final int OPTION_REGISTER = 2; public static final int OPTION_USECOMPRESSION = 3; - public 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; + public static enum State { + DISABLED, + OFFLINE, + CONNECTING, + ONLINE, + NO_INTERNET, + UNAUTHORIZED(true), + SERVER_NOT_FOUND(true), + REGISTRATION_FAILED(true), + REGISTRATION_CONFLICT(true), + REGISTRATION_SUCCESSFUL, + REGISTRATION_NOT_SUPPORTED(true), + SECURITY_ERROR(true), + INCOMPATIBLE_SERVER(true); + + private boolean isError; + + public boolean isError() { + return this.isError; + } + + private State(final boolean isError) { + this.isError = isError; + } + + private State() { + this(false); + } + + public int getReadableId() { + switch (this) { + case DISABLED: + return R.string.account_status_disabled; + case ONLINE: + return R.string.account_status_online; + case CONNECTING: + return R.string.account_status_connecting; + case OFFLINE: + return R.string.account_status_offline; + case UNAUTHORIZED: + return R.string.account_status_unauthorized; + case SERVER_NOT_FOUND: + return R.string.account_status_not_found; + case NO_INTERNET: + return R.string.account_status_no_internet; + case REGISTRATION_FAILED: + return R.string.account_status_regis_fail; + case REGISTRATION_CONFLICT: + return R.string.account_status_regis_conflict; + case REGISTRATION_SUCCESSFUL: + return R.string.account_status_regis_success; + case REGISTRATION_NOT_SUPPORTED: + return R.string.account_status_regis_not_sup; + case SECURITY_ERROR: + return R.string.account_status_security_error; + case INCOMPATIBLE_SERVER: + return R.string.account_status_incompatible_server; + default: + return R.string.account_status_unknown; + } + } + } + + public List<Conversation> pendingConferenceJoins = new CopyOnWriteArrayList<>(); + public List<Conversation> pendingConferenceLeaves = new CopyOnWriteArrayList<>(); + protected Jid jid; protected String password; protected int options = 0; protected String rosterVersion; - protected String resource = "mobile"; - protected int status = -1; + protected State status = State.OFFLINE; 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>(); + private List<Bookmark> bookmarks = new CopyOnWriteArrayList<>(); public Account() { this.uuid = "0"; } - public Account(String username, String server, String password) { - this(java.util.UUID.randomUUID().toString(), username, server, + public Account(final Jid jid, final String password) { + this(java.util.UUID.randomUUID().toString(), jid, password, 0, null, "", null); } - public Account(String uuid, String username, String server, - String password, int options, String rosterVersion, String keys, - String avatar) { + public Account(final String uuid, final Jid jid, + final String password, final int options, final String rosterVersion, final String keys, + final String avatar) { this.uuid = uuid; - this.username = username; - this.server = server; + this.jid = jid; + if (jid.isBareJid()) { + this.setResource("mobile"); + } this.password = password; this.options = options; this.rosterVersion = rosterVersion; try { this.keys = new JSONObject(keys); - } catch (JSONException e) { + } catch (final JSONException ignored) { } this.avatar = avatar; } + public static Account fromCursor(Cursor cursor) { + Jid jid = null; + try { + jid = Jid.fromParts(cursor.getString(cursor.getColumnIndex(USERNAME)), + cursor.getString(cursor.getColumnIndex(SERVER)), "mobile"); + } catch (final InvalidJidException ignored) { + } + return new Account(cursor.getString(cursor.getColumnIndex(UUID)), + jid, + 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 boolean isOptionSet(int option) { return ((options & (1 << option)) != 0); } @@ -112,69 +179,62 @@ public class Account extends AbstractEntity { } public String getUsername() { - return username; + return jid.getLocalpart(); } - public void setUsername(String username) { - this.username = username; + public void setUsername(final String username) throws InvalidJidException { + jid = Jid.fromParts(username, jid.getDomainpart(), jid.getResourcepart()); } - public String getServer() { - return server; + public Jid getServer() { + return jid.toDomainJid(); } - public void setServer(String server) { - this.server = server; + public void setServer(final String server) throws InvalidJidException { + jid = Jid.fromParts(jid.getLocalpart(), server, jid.getResourcepart()); } public String getPassword() { return password; } - public void setPassword(String password) { + public void setPassword(final String password) { this.password = password; } - public void setStatus(int status) { - this.status = status; - } - - public int getStatus() { + public State getStatus() { if (isOptionSet(OPTION_DISABLED)) { - return STATUS_DISABLED; + return State.DISABLED; } else { return this.status; } } + public void setStatus(final State status) { + this.status = 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); + return getStatus().isError(); } public boolean hasErrorStatus() { - if (getXmppConnection() == null) { - return false; - } else { - return getStatus() > STATUS_NO_INTERNET - && (getXmppConnection().getAttempt() >= 2); - } + return getXmppConnection() != null && getStatus().isError() && getXmppConnection().getAttempt() >= 2; } - public void setResource(String resource) { - this.resource = resource; + public String getResource() { + return jid.getResourcepart(); } - public String getResource() { - return this.resource; + public void setResource(final String resource) { + try { + jid = Jid.fromParts(jid.getLocalpart(), jid.getDomainpart(), resource); + } catch (final InvalidJidException ignored) { + } } - public String getJid() { - return username.toLowerCase(Locale.getDefault()) + "@" - + server.toLowerCase(Locale.getDefault()); + public Jid getJid() { + return jid; } public JSONObject getKeys() { @@ -210,8 +270,8 @@ public class Account extends AbstractEntity { public ContentValues getContentValues() { ContentValues values = new ContentValues(); values.put(UUID, uuid); - values.put(USERNAME, username); - values.put(SERVER, server); + values.put(USERNAME, jid.getLocalpart()); + values.put(SERVER, jid.getDomainpart()); values.put(PASSWORD, password); values.put(OPTIONS, options); values.put(KEYS, this.keys.toString()); @@ -220,21 +280,11 @@ public class Account extends AbstractEntity { 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 void initOtrEngine(XmppConnectionService context) { + this.otrEngine = new OtrEngine(context, this); } - public OtrEngine getOtrEngine(XmppConnectionService context) { - if (otrEngine == null) { - otrEngine = new OtrEngine(context, this); - } + public OtrEngine getOtrEngine() { return this.otrEngine; } @@ -246,30 +296,24 @@ public class Account extends AbstractEntity { 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) { + if (this.otrEngine == 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) { - + DSAPublicKey publicKey = (DSAPublicKey) this.otrEngine.getPublicKey(); + if (publicKey == null) { + return null; + } + this.otrFingerprint = new OtrCryptoEngineImpl().getFingerprint(publicKey); + return this.otrFingerprint; + } catch (final OtrCryptoException ignored) { + return null; } + } else { + return this.otrFingerprint; } - return this.otrFingerprint; } public String getRosterVersion() { @@ -284,11 +328,6 @@ public class Account extends AbstractEntity { 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); } @@ -324,17 +363,17 @@ public class Account extends AbstractEntity { return this.roster; } - public void setBookmarks(List<Bookmark> bookmarks) { - this.bookmarks = bookmarks; - } - public List<Bookmark> getBookmarks() { return this.bookmarks; } - public boolean hasBookmarkFor(String conferenceJid) { + public void setBookmarks(List<Bookmark> bookmarks) { + this.bookmarks = bookmarks; + } + + public boolean hasBookmarkFor(final Jid conferenceJid) { for (Bookmark bmark : this.bookmarks) { - if (bmark.getJid().equals(conferenceJid)) { + if (bmark.getJid().equals(conferenceJid.toBareJid())) { return true; } } @@ -354,39 +393,9 @@ public class Account extends AbstractEntity { 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); + + (Config.CARBON_GRACE_PERIOD * 1000); } public void deactivateGracePeriod() { @@ -396,4 +405,13 @@ public class Account extends AbstractEntity { public boolean inGracePeriod() { return SystemClock.elapsedRealtime() < this.mEndGracePeriod; } + + public String getShareableUri() { + String fingerprint = this.getOtrFingerprint(); + if (fingerprint != null) { + return "xmpp:" + this.getJid().toBareJid().toString() + "?otr-fingerprint="+fingerprint; + } else { + return "xmpp:" + this.getJid().toBareJid().toString(); + } + } } diff --git a/src/main/java/eu/siacs/conversations/entities/Bookmark.java b/src/main/java/eu/siacs/conversations/entities/Bookmark.java index dd9e805c..54dcfea1 100644 --- a/src/main/java/eu/siacs/conversations/entities/Bookmark.java +++ b/src/main/java/eu/siacs/conversations/entities/Bookmark.java @@ -3,15 +3,17 @@ package eu.siacs.conversations.entities; import java.util.Locale; import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.jid.InvalidJidException; +import eu.siacs.conversations.xmpp.jid.Jid; public class Bookmark extends Element implements ListItem { private Account account; private Conversation mJoinedConversation; - public Bookmark(Account account, String jid) { + public Bookmark(final Account account, final Jid jid) { super("conference"); - this.setAttribute("jid", jid); + this.setAttribute("jid", jid.toString()); this.account = account; } @@ -55,10 +57,10 @@ public class Bookmark extends Element implements ListItem { } @Override - public int compareTo(ListItem another) { - return this.getDisplayName().compareToIgnoreCase( - another.getDisplayName()); - } + public int compareTo(final ListItem another) { + return this.getDisplayName().compareToIgnoreCase( + another.getDisplayName()); + } @Override public String getDisplayName() { @@ -68,16 +70,20 @@ public class Bookmark extends Element implements ListItem { } else if (getName() != null) { return getName(); } else { - return this.getJid().split("@")[0]; + return this.getJid().getLocalpart(); } } @Override - public String getJid() { - String jid = this.getAttribute("jid"); + public Jid getJid() { + final String jid = this.getAttribute("jid"); if (jid != null) { - return jid.toLowerCase(Locale.US); - } else { + try { + return Jid.fromString(jid); + } catch (final InvalidJidException e) { + return null; + } + } else { return null; } } @@ -108,7 +114,7 @@ public class Bookmark extends Element implements ListItem { public boolean match(String needle) { return needle == null - || getJid().contains(needle.toLowerCase(Locale.US)) + || getJid().toString().toLowerCase(Locale.US).contains(needle.toLowerCase(Locale.US)) || getDisplayName().toLowerCase(Locale.US).contains( needle.toLowerCase(Locale.US)); } diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java index af5172d3..32e4601d 100644 --- a/src/main/java/eu/siacs/conversations/entities/Contact.java +++ b/src/main/java/eu/siacs/conversations/entities/Contact.java @@ -1,16 +1,19 @@ package eu.siacs.conversations.entities; -import java.util.HashSet; -import java.util.Locale; -import java.util.Set; +import android.content.ContentValues; +import android.database.Cursor; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; + import eu.siacs.conversations.xml.Element; -import android.content.ContentValues; -import android.database.Cursor; +import eu.siacs.conversations.xmpp.jid.InvalidJidException; +import eu.siacs.conversations.xmpp.jid.Jid; public class Contact implements ListItem { public static final String TABLENAME = "contacts"; @@ -31,7 +34,7 @@ public class Contact implements ListItem { protected String systemName; protected String serverName; protected String presenceName; - protected String jid; + protected Jid jid; protected int subscription = 0; protected String systemAccount; protected String photoUri; @@ -41,12 +44,10 @@ public class Contact implements ListItem { protected Account account; - protected boolean inRoster = true; - public Lastseen lastseen = new Lastseen(); public Contact(final String account, final String systemName, final String serverName, - final String jid, final int subscription, final String photoUri, + final Jid jid, final int subscription, final String photoUri, final String systemAccount, final String keys, final String avatar, final Lastseen lastseen) { this(account, systemName, serverName, jid, subscription, photoUri, systemAccount, keys, @@ -55,7 +56,7 @@ public class Contact implements ListItem { } public Contact(final String account, final String systemName, final String serverName, - final String jid, final int subscription, final String photoUri, + final Jid jid, final int subscription, final String photoUri, final String systemAccount, final String keys, final String avatar) { this.accountUuid = account; this.systemName = systemName; @@ -72,33 +73,35 @@ public class Contact implements ListItem { this.avatar = avatar; } - public Contact(final String jid) { + public Contact(final Jid jid) { this.jid = jid; } public String getDisplayName() { if (this.systemName != null) { - return this.systemName; - } else if (this.serverName != null) { - return this.serverName; + return this.systemName; + } else if (this.serverName != null) { + return this.serverName; } else if (this.presenceName != null) { - return this.presenceName; + return this.presenceName; + } else if (jid.hasLocalpart()) { + return jid.getLocalpart(); } else { - return this.jid.split("@")[0]; - } + return jid.getDomainpart(); + } } public String getProfilePhoto() { return this.photoUri; } - public String getJid() { - return this.jid.toLowerCase(Locale.getDefault()); + public Jid getJid() { + return jid; } public boolean match(String needle) { return needle == null - || jid.contains(needle.toLowerCase()) + || jid.toString().contains(needle.toLowerCase()) || getDisplayName().toLowerCase() .contains(needle.toLowerCase()); } @@ -108,7 +111,7 @@ public class Contact implements ListItem { values.put(ACCOUNT, accountUuid); values.put(SYSTEMNAME, systemName); values.put(SERVERNAME, serverName); - values.put(JID, jid); + values.put(JID, jid.toString()); values.put(OPTIONS, subscription); values.put(SYSTEMACCOUNT, systemAccount); values.put(PHOTOURI, photoUri); @@ -123,10 +126,17 @@ public class Contact implements ListItem { final Lastseen lastseen = new Lastseen( cursor.getString(cursor.getColumnIndex(LAST_PRESENCE)), cursor.getLong(cursor.getColumnIndex(LAST_TIME))); - return new Contact(cursor.getString(cursor.getColumnIndex(ACCOUNT)), + final Jid jid; + try { + jid = Jid.fromString(cursor.getString(cursor.getColumnIndex(JID))); + } catch (final InvalidJidException e) { + // TODO: Borked DB... handle this somehow? + return null; + } + return new Contact(cursor.getString(cursor.getColumnIndex(ACCOUNT)), cursor.getString(cursor.getColumnIndex(SYSTEMNAME)), cursor.getString(cursor.getColumnIndex(SERVERNAME)), - cursor.getString(cursor.getColumnIndex(JID)), + jid, cursor.getInt(cursor.getColumnIndex(OPTIONS)), cursor.getString(cursor.getColumnIndex(PHOTOURI)), cursor.getString(cursor.getColumnIndex(SYSTEMACCOUNT)), @@ -197,24 +207,26 @@ public class Contact implements ListItem { return systemAccount; } - public Set<String> getOtrFingerprints() { - Set<String> set = new HashSet<String>(); + public ArrayList<String> getOtrFingerprints() { + ArrayList<String> fingerprints = new ArrayList<String>(); try { if (this.keys.has("otr_fingerprints")) { - JSONArray fingerprints = this.keys + JSONArray prints = this.keys .getJSONArray("otr_fingerprints"); - for (int i = 0; i < fingerprints.length(); ++i) { - set.add(fingerprints.getString(i)); + for (int i = 0; i < prints.length(); ++i) { + fingerprints.add(prints.getString(i)); } } - } catch (JSONException e) { - // TODO Auto-generated catch block - e.printStackTrace(); + } catch (final JSONException ignored) { + } - return set; + return fingerprints; } - public void addOtrFingerprint(String print) { + public boolean addOtrFingerprint(String print) { + if (getOtrFingerprints().contains(print)) { + return false; + } try { JSONArray fingerprints; if (!this.keys.has("otr_fingerprints")) { @@ -225,15 +237,16 @@ public class Contact implements ListItem { } fingerprints.put(print); this.keys.put("otr_fingerprints", fingerprints); - } catch (JSONException e) { - + return true; + } catch (final JSONException ignored) { + return false; } } public void setPgpKeyId(long keyId) { try { this.keys.put("pgp_keyid", keyId); - } catch (JSONException e) { + } catch (final JSONException ignored) { } } @@ -273,21 +286,26 @@ public class Contact implements ListItem { 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); - } + switch (subscription) { + case "to": + this.resetOption(Options.FROM); + this.setOption(Options.TO); + break; + case "from": + this.resetOption(Options.TO); + this.setOption(Options.FROM); + this.resetOption(Options.PREEMPTIVE_GRANT); + break; + case "both": + this.setOption(Options.TO); + this.setOption(Options.FROM); + this.resetOption(Options.PREEMPTIVE_GRANT); + break; + case "none": + this.resetOption(Options.FROM); + this.resetOption(Options.TO); + break; + } } // do NOT override asking if pending push request @@ -301,8 +319,8 @@ public class Contact implements ListItem { } public Element asElement() { - Element item = new Element("item"); - item.setAttribute("jid", this.jid); + final Element item = new Element("item"); + item.setAttribute("jid", this.jid.toString()); if (this.serverName != null) { item.setAttribute("name", this.serverName); } @@ -335,18 +353,13 @@ public class Contact implements ListItem { } @Override - public int compareTo(ListItem another) { + public int compareTo(final 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 Jid getServer() { + return getJid().toDomainJid(); } public boolean setAvatar(String filename) { @@ -387,4 +400,13 @@ public class Contact implements ListItem { public boolean trusted() { return getOption(Options.FROM) && getOption(Options.TO); } + + public String getShareableUri() { + if (getOtrFingerprints().size() >= 1) { + String otr = getOtrFingerprints().get(0); + return "xmpp:"+getJid().toBareJid().toString()+"?otr-fingerprint="+otr.replace(" ",""); + } else { + return "xmpp:"+getJid().toBareJid().toString(); + } + } } diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index 9d4c36db..b1df7ec7 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -1,13 +1,8 @@ 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 android.content.ContentValues; +import android.database.Cursor; +import android.os.SystemClock; import net.java.otr4j.OtrException; import net.java.otr4j.crypto.OtrCryptoEngineImpl; @@ -15,9 +10,17 @@ 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; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.security.interfaces.DSAPublicKey; +import java.util.ArrayList; +import java.util.List; + +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xmpp.jid.InvalidJidException; +import eu.siacs.conversations.xmpp.jid.Jid; public class Conversation extends AbstractEntity { public static final String TABLENAME = "conversations"; @@ -45,43 +48,42 @@ public class Conversation extends AbstractEntity { private String name; private String contactUuid; private String accountUuid; - private String contactJid; + private Jid contactJid; private int status; private long created; private int mode; private JSONObject attributes = new JSONObject(); - private String nextPresence; + private Jid nextCounterpart; - protected ArrayList<Message> messages = new ArrayList<Message>(); + protected ArrayList<Message> messages = new ArrayList<>(); protected Account account = null; private transient SessionImpl otrSession; private transient String otrFingerprint = null; + private Smp mSmp = new Smp(); private String nextMessage; private transient MucOptions mucOptions = null; - // private transient String latestMarkableMessageId; - private byte[] symmetricKey; private Bookmark bookmark; - public Conversation(String name, Account account, String contactJid, - int mode) { + public Conversation(final String name, final Account account, final Jid contactJid, + final int mode) { this(java.util.UUID.randomUUID().toString(), name, null, account .getUuid(), contactJid, System.currentTimeMillis(), STATUS_AVAILABLE, mode, ""); this.account = account; } - public Conversation(String uuid, String name, String contactUuid, - String accountUuid, String contactJid, long created, int status, - int mode, String attributes) { + public Conversation(final String uuid, final String name, final String contactUuid, + final String accountUuid, final Jid contactJid, final long created, final int status, + final int mode, final String attributes) { this.uuid = uuid; this.name = name; this.contactUuid = contactUuid; @@ -91,10 +93,7 @@ public class Conversation extends AbstractEntity { this.status = status; this.mode = mode; try { - if (attributes == null) { - attributes = new String(); - } - this.attributes = new JSONObject(attributes); + this.attributes = new JSONObject(attributes == null ? "" : attributes); } catch (JSONException e) { this.attributes = new JSONObject(); } @@ -105,10 +104,8 @@ public class Conversation extends AbstractEntity { } public boolean isRead() { - if ((this.messages == null) || (this.messages.size() == 0)) - return true; - return this.messages.get(this.messages.size() - 1).isRead(); - } + return (this.messages == null) || (this.messages.size() == 0) || this.messages.get(this.messages.size() - 1).isRead(); + } public void markRead() { if (this.messages == null) { @@ -186,7 +183,7 @@ public class Conversation extends AbstractEntity { this.account = account; } - public String getContactJid() { + public Jid getContactJid() { return this.contactJid; } @@ -204,7 +201,7 @@ public class Conversation extends AbstractEntity { values.put(NAME, name); values.put(CONTACT, contactUuid); values.put(ACCOUNT, accountUuid); - values.put(CONTACTJID, contactJid); + values.put(CONTACTJID, contactJid.toString()); values.put(CREATED, created); values.put(STATUS, status); values.put(MODE, mode); @@ -213,11 +210,18 @@ public class Conversation extends AbstractEntity { } public static Conversation fromCursor(Cursor cursor) { - return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)), + Jid jid; + try { + jid = Jid.fromString(cursor.getString(cursor.getColumnIndex(CONTACTJID))); + } catch (final InvalidJidException e) { + // Borked DB.. + jid = null; + } + return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)), cursor.getString(cursor.getColumnIndex(NAME)), cursor.getString(cursor.getColumnIndex(CONTACT)), cursor.getString(cursor.getColumnIndex(ACCOUNT)), - cursor.getString(cursor.getColumnIndex(CONTACTJID)), + jid, cursor.getLong(cursor.getColumnIndex(CREATED)), cursor.getInt(cursor.getColumnIndex(STATUS)), cursor.getInt(cursor.getColumnIndex(MODE)), @@ -236,15 +240,14 @@ public class Conversation extends AbstractEntity { this.mode = mode; } - public SessionImpl startOtrSession(XmppConnectionService service, - String presence, boolean sendStart) { + public SessionImpl startOtrSession(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)); + final SessionID sessionId = new SessionID(this.getContactJid().toBareJid().toString(), + presence, + "xmpp"); + this.otrSession = new SessionImpl(sessionId, getAccount().getOtrEngine()); try { if (sendStart) { this.otrSession.startSession(); @@ -265,6 +268,13 @@ public class Conversation extends AbstractEntity { public void resetOtrSession() { this.otrFingerprint = null; this.otrSession = null; + this.mSmp.hint = null; + this.mSmp.secret = null; + this.mSmp.status = Smp.STATUS_NONE; + } + + public Smp smp() { + return mSmp; } public void startOtrIfNeeded() { @@ -317,13 +327,21 @@ public class Conversation extends AbstractEntity { builder.insert(26, " "); builder.insert(35, " "); this.otrFingerprint = builder.toString(); - } catch (OtrCryptoException e) { + } catch (final OtrCryptoException ignored) { } } return this.otrFingerprint; } + public void verifyOtrFingerprint() { + getContact().addOtrFingerprint(getOtrFingerprint()); + } + + public boolean isOtrFingerprintVerified() { + return getContact().getOtrFingerprints().contains(getOtrFingerprint()); + } + public synchronized MucOptions getMucOptions() { if (this.mucOptions == null) { this.mucOptions = new MucOptions(this); @@ -335,16 +353,16 @@ public class Conversation extends AbstractEntity { this.mucOptions = null; } - public void setContactJid(String jid) { + public void setContactJid(final Jid jid) { this.contactJid = jid; } - public void setNextPresence(String presence) { - this.nextPresence = presence; + public void setNextCounterpart(Jid jid) { + this.nextCounterpart = jid; } - public String getNextPresence() { - return this.nextPresence; + public Jid getNextCounterpart() { + return this.nextCounterpart; } public int getLatestEncryption() { @@ -397,6 +415,10 @@ public class Conversation extends AbstractEntity { } } + public boolean smpRequested() { + return smp().status == Smp.STATUS_CONTACT_REQUESTED; + } + public void setNextMessage(String message) { this.nextMessage = message; } @@ -497,4 +519,16 @@ public class Conversation extends AbstractEntity { this.messages.addAll(index, messages); } } + + public class Smp { + public static final int STATUS_NONE = 0; + public static final int STATUS_CONTACT_REQUESTED = 1; + public static final int STATUS_WE_REQUESTED = 2; + public static final int STATUS_FAILED = 3; + public static final int STATUS_VERIFIED = 4; + + public String secret = null; + public String hint = null; + public int status = 0; + } } diff --git a/src/main/java/eu/siacs/conversations/entities/Downloadable.java b/src/main/java/eu/siacs/conversations/entities/Downloadable.java index e4c85336..d25bf93a 100644 --- a/src/main/java/eu/siacs/conversations/entities/Downloadable.java +++ b/src/main/java/eu/siacs/conversations/entities/Downloadable.java @@ -2,7 +2,7 @@ package eu.siacs.conversations.entities; public interface Downloadable { - public final String[] VALID_EXTENSIONS = {"webp", "jpeg", "jpg", "png", "jpe"}; + public final String[] VALID_IMAGE_EXTENSIONS = {"webp", "jpeg", "jpg", "png", "jpe"}; public final String[] VALID_CRYPTO_EXTENSIONS = {"pgp", "gpg", "otr"}; public static final int STATUS_UNKNOWN = 0x200; @@ -12,10 +12,17 @@ public interface Downloadable { public static final int STATUS_DOWNLOADING = 0x204; public static final int STATUS_DELETED = 0x205; public static final int STATUS_OFFER_CHECK_FILESIZE = 0x206; + public static final int STATUS_UPLOADING = 0x207; public boolean start(); public int getStatus(); public long getFileSize(); + + public int getProgress(); + + public String getMimeType(); + + public void cancel(); } diff --git a/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java b/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java index 1605c75b..25f33907 100644 --- a/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java +++ b/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java @@ -6,6 +6,7 @@ import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.InputStream; import java.io.OutputStream; +import java.net.URLConnection; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.Key; @@ -28,6 +29,7 @@ public class DownloadableFile extends File { private long expectedSize = 0; private String sha1sum; private Key aeskey; + private String mime; private byte[] iv = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0xf }; @@ -52,6 +54,18 @@ public class DownloadableFile extends File { } } + public String getMimeType() { + String path = this.getAbsolutePath(); + String mime = URLConnection.guessContentTypeFromName(path); + if (mime != null) { + return mime; + } else if (mime == null && path.endsWith(".webp")) { + return "image/webp"; + } else { + return ""; + } + } + public void setExpectedSize(long size) { this.expectedSize = size; } diff --git a/src/main/java/eu/siacs/conversations/entities/DownloadablePlaceholder.java b/src/main/java/eu/siacs/conversations/entities/DownloadablePlaceholder.java new file mode 100644 index 00000000..03fceceb --- /dev/null +++ b/src/main/java/eu/siacs/conversations/entities/DownloadablePlaceholder.java @@ -0,0 +1,39 @@ +package eu.siacs.conversations.entities; + +public class DownloadablePlaceholder implements Downloadable { + + private int status; + + public DownloadablePlaceholder(int status) { + this.status = status; + } + @Override + public boolean start() { + return false; + } + + @Override + public int getStatus() { + return status; + } + + @Override + public long getFileSize() { + return 0; + } + + @Override + public int getProgress() { + return 0; + } + + @Override + public String getMimeType() { + return ""; + } + + @Override + public void cancel() { + + } +} diff --git a/src/main/java/eu/siacs/conversations/entities/ListItem.java b/src/main/java/eu/siacs/conversations/entities/ListItem.java index a1872d2f..fa650f1c 100644 --- a/src/main/java/eu/siacs/conversations/entities/ListItem.java +++ b/src/main/java/eu/siacs/conversations/entities/ListItem.java @@ -1,7 +1,9 @@ package eu.siacs.conversations.entities; +import eu.siacs.conversations.xmpp.jid.Jid; + public interface ListItem extends Comparable<ListItem> { public String getDisplayName(); - public String getJid(); + public Jid getJid(); } diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index b33d5f37..33f3443b 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -1,12 +1,15 @@ package eu.siacs.conversations.entities; +import android.content.ContentValues; +import android.database.Cursor; + import java.net.MalformedURLException; import java.net.URL; import java.util.Arrays; import eu.siacs.conversations.Config; -import android.content.ContentValues; -import android.database.Cursor; +import eu.siacs.conversations.xmpp.jid.InvalidJidException; +import eu.siacs.conversations.xmpp.jid.Jid; public class Message extends AbstractEntity { @@ -29,7 +32,7 @@ public class Message extends AbstractEntity { 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_FILE = 2; public static final int TYPE_STATUS = 3; public static final int TYPE_PRIVATE = 4; @@ -42,9 +45,10 @@ public class Message extends AbstractEntity { public static String STATUS = "status"; public static String TYPE = "type"; public static String REMOTE_MSG_ID = "remoteMsgId"; - + public static String RELATIVE_FILE_PATH = "relativeFilePath"; + public boolean markable = false; protected String conversationUuid; - protected String counterpart; + protected Jid counterpart; protected String trueCounterpart; protected String body; protected String encryptedBody; @@ -52,13 +56,11 @@ public class Message extends AbstractEntity { protected int encryption; protected int status; protected int type; + protected String relativeFilePath; 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; @@ -67,24 +69,20 @@ public class Message extends AbstractEntity { } 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; + this(conversation,body,encryption,STATUS_UNSEND); } - public Message(Conversation conversation, String counterpart, String body, - int encryption, int status) { + public Message(Conversation conversation, String body, int encryption, int status) { this(java.util.UUID.randomUUID().toString(), conversation.getUuid(), - counterpart, null, body, System.currentTimeMillis(), - encryption, status, TYPE_TEXT, null); + conversation.getContactJid().toBareJid(), null, body, System + .currentTimeMillis(), encryption, + status, TYPE_TEXT, null,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) { + public Message(final String uuid, final String conversationUUid, final Jid counterpart, + final String trueCounterpart, final String body, final long timeSent, + final int encryption, final int status, final int type, final String remoteMsgId, final String relativeFilePath) { this.uuid = uuid; this.conversationUuid = conversationUUid; this.counterpart = counterpart; @@ -95,6 +93,39 @@ public class Message extends AbstractEntity { this.status = status; this.type = type; this.remoteMsgId = remoteMsgId; + this.relativeFilePath = relativeFilePath; + } + + public static Message fromCursor(Cursor cursor) { + Jid jid; + try { + String value = cursor.getString(cursor.getColumnIndex(COUNTERPART)); + if (value!=null) { + jid = Jid.fromString(value); + } else { + jid = null; + } + } catch (InvalidJidException e) { + jid = null; + } + return new Message(cursor.getString(cursor.getColumnIndex(UUID)), + cursor.getString(cursor.getColumnIndex(CONVERSATION)), + jid, + 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)), + cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH))); + } + + public static Message createStatusMessage(Conversation conversation) { + Message message = new Message(); + message.setType(Message.TYPE_STATUS); + message.setConversation(conversation); + return message; } @Override @@ -102,7 +133,11 @@ public class Message extends AbstractEntity { ContentValues values = new ContentValues(); values.put(UUID, uuid); values.put(CONVERSATION, conversationUuid); - values.put(COUNTERPART, counterpart); + if (counterpart == null) { + values.putNull(COUNTERPART); + } else { + values.put(COUNTERPART, counterpart.toString()); + } values.put(TRUE_COUNTERPART, trueCounterpart); values.put(BODY, body); values.put(TIME_SENT, timeSent); @@ -110,6 +145,7 @@ public class Message extends AbstractEntity { values.put(STATUS, status); values.put(TYPE, type); values.put(REMOTE_MSG_ID, remoteMsgId); + values.put(RELATIVE_FILE_PATH, relativeFilePath); return values; } @@ -121,10 +157,18 @@ public class Message extends AbstractEntity { return this.conversation; } - public String getCounterpart() { + public void setConversation(Conversation conv) { + this.conversation = conv; + } + + public Jid getCounterpart() { return counterpart; } + public void setCounterpart(final Jid counterpart) { + this.counterpart = counterpart; + } + public Contact getContact() { if (this.conversation.getMode() == Conversation.MODE_SINGLE) { return this.conversation.getContact(); @@ -142,6 +186,10 @@ public class Message extends AbstractEntity { return body; } + public void setBody(String body) { + this.body = body; + } + public long getTimeSent() { return timeSent; } @@ -150,37 +198,32 @@ public class Message extends AbstractEntity { return encryption; } + public void setEncryption(int encryption) { + this.encryption = encryption; + } + public int getStatus() { return status; } - public String getRemoteMsgId() { - return this.remoteMsgId; + public void setStatus(int status) { + this.status = status; } - public void setRemoteMsgId(String id) { - this.remoteMsgId = id; + public void setRelativeFilePath(String path) { + this.relativeFilePath = path; } - 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 String getRelativeFilePath() { + return this.relativeFilePath; } - public void setConversation(Conversation conv) { - this.conversation = conv; + public String getRemoteMsgId() { + return this.remoteMsgId; } - public void setStatus(int status) { - this.status = status; + public void setRemoteMsgId(String id) { + this.remoteMsgId = id; } public boolean isRead() { @@ -199,14 +242,6 @@ public class Message extends AbstractEntity { 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; } @@ -215,57 +250,24 @@ public class Message extends AbstractEntity { 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 setType(int type) { + this.type = type; } 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 void setDownloadable(Downloadable downloadable) { + this.downloadable = downloadable; } public boolean equals(Message message) { @@ -320,13 +322,14 @@ public class Message extends AbstractEntity { && message.getEncryption() != Message.ENCRYPTION_PGP && this.getType() == message.getType() && this.getEncryption() == message.getEncryption() + && this.getCounterpart() != null && 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)))) + .getStatus() == Message.STATUS_SEND_DISPLAYED)))) && !message.bodyContainsDownloadable() && !this.bodyContainsDownloadable()); } @@ -359,13 +362,9 @@ public class Message extends AbstractEntity { public boolean wasMergedIntoPrevious() { Message prev = this.prev(); - if (prev == null) { - return false; - } else { - return prev.mergeable(this); - } + return prev != null && prev.mergeable(this); } - + public boolean trusted() { Contact contact = this.getContact(); return (status > STATUS_RECEIVED || (contact != null && contact.trusted())); @@ -390,15 +389,15 @@ public class Message extends AbstractEntity { } String[] extensionParts = filename.split("\\."); if (extensionParts.length == 2 - && Arrays.asList(Downloadable.VALID_EXTENSIONS).contains( - extensionParts[extensionParts.length - 1])) { + && Arrays.asList(Downloadable.VALID_IMAGE_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])) { + .asList(Downloadable.VALID_CRYPTO_EXTENSIONS) + .contains(extensionParts[extensionParts.length - 1]) + && Arrays.asList(Downloadable.VALID_IMAGE_EXTENSIONS).contains( + extensionParts[extensionParts.length - 2])) { return true; } else { return false; @@ -410,7 +409,7 @@ public class Message extends AbstractEntity { public ImageParams getImageParams() { ImageParams params = getLegacyImageParams(); - if (params!=null) { + if (params != null) { return params; } params = new ImageParams(); @@ -473,7 +472,7 @@ public class Message extends AbstractEntity { } return params; } - + public ImageParams getLegacyImageParams() { ImageParams params = new ImageParams(); if (body == null) { diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java index 01190374..6eb1d43c 100644 --- a/src/main/java/eu/siacs/conversations/entities/MucOptions.java +++ b/src/main/java/eu/siacs/conversations/entities/MucOptions.java @@ -6,6 +6,8 @@ import java.util.concurrent.CopyOnWriteArrayList; import eu.siacs.conversations.crypto.PgpEngine; import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.jid.InvalidJidException; +import eu.siacs.conversations.xmpp.jid.Jid; import eu.siacs.conversations.xmpp.stanzas.PresencePacket; import android.annotation.SuppressLint; @@ -66,15 +68,20 @@ public class MucOptions { 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; - } + switch (role) { + case "moderator": + this.role = ROLE_MODERATOR; + break; + case "participant": + this.role = ROLE_PARTICIPANT; + break; + case "visitor": + this.role = ROLE_VISITOR; + break; + default: + this.role = ROLE_NONE; + break; + } } public int getAffiliation() { @@ -109,7 +116,7 @@ public class MucOptions { } private Account account; - private List<User> users = new CopyOnWriteArrayList<User>(); + private List<User> users = new CopyOnWriteArrayList<>(); private Conversation conversation; private boolean isOnline = false; private int error = ERROR_ROOM_NOT_FOUND; @@ -145,9 +152,9 @@ public class MucOptions { } public void processPacket(PresencePacket packet, PgpEngine pgp) { - String[] fromParts = packet.getFrom().split("/", 2); - if (fromParts.length >= 2) { - String name = fromParts[1]; + final Jid from = packet.getFrom(); + if (!from.isBareJid()) { + final String name = from.getResourcepart(); String type = packet.getAttribute("type"); if (type == null) { User user = new User(); @@ -236,13 +243,12 @@ public class MucOptions { } 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]; + if (!conversation.getContactJid().isBareJid()) { + return conversation.getContactJid().getResourcepart(); } else { return account.getUsername(); } @@ -300,7 +306,7 @@ public class MucOptions { } public long[] getPgpKeyIds() { - List<Long> ids = new ArrayList<Long>(); + List<Long> ids = new ArrayList<>(); for (User user : getUsers()) { if (user.getPgpKeyId() != 0) { ids.add(user.getPgpKeyId()); @@ -331,10 +337,14 @@ public class MucOptions { return true; } - public String getJoinJid() { - return this.conversation.getContactJid().split("/", 2)[0] + "/" - + this.joinnick; - } + public Jid getJoinJid() { + try { + return Jid.fromString(this.conversation.getContactJid().toBareJid().toString() + "/" ++ this.joinnick); + } catch (final InvalidJidException e) { + return null; + } + } public String getTrueCounterpart(String counterpart) { for (User user : this.getUsers()) { diff --git a/src/main/java/eu/siacs/conversations/entities/Roster.java b/src/main/java/eu/siacs/conversations/entities/Roster.java index 3267b15a..27d4deb0 100644 --- a/src/main/java/eu/siacs/conversations/entities/Roster.java +++ b/src/main/java/eu/siacs/conversations/entities/Roster.java @@ -2,12 +2,13 @@ package eu.siacs.conversations.entities; import java.util.ArrayList; import java.util.List; -import java.util.Locale; import java.util.concurrent.ConcurrentHashMap; +import eu.siacs.conversations.xmpp.jid.Jid; + public class Roster { Account account; - ConcurrentHashMap<String, Contact> contacts = new ConcurrentHashMap<String, Contact>(); + final ConcurrentHashMap<String, Contact> contacts = new ConcurrentHashMap<>(); private String version = null; public Roster(Account account) { @@ -27,14 +28,14 @@ public class Roster { } } - public Contact getContact(String jid) { - String cleanJid = jid.split("/", 2)[0].toLowerCase(Locale.getDefault()); - if (contacts.containsKey(cleanJid)) { - return contacts.get(cleanJid); + public Contact getContact(final Jid jid) { + final Jid bareJid = jid.toBareJid(); + if (contacts.containsKey(bareJid.toString())) { + return contacts.get(bareJid.toString()); } else { - Contact contact = new Contact(cleanJid); + Contact contact = new Contact(bareJid); contact.setAccount(account); - contacts.put(cleanJid, contact); + contacts.put(bareJid.toString(), contact); return contact; } } @@ -60,13 +61,13 @@ public class Roster { } public List<Contact> getContacts() { - return new ArrayList<Contact>(this.contacts.values()); + return new ArrayList<>(this.contacts.values()); } - public void initContact(Contact contact) { + public void initContact(final Contact contact) { contact.setAccount(account); contact.setOption(Contact.Options.IN_ROSTER); - contacts.put(contact.getJid(), contact); + contacts.put(contact.getJid().toBareJid().toString(), contact); } public void setVersion(String version) { diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index d44bf0ca..5d674748 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -6,6 +6,7 @@ import java.util.List; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.jid.Jid; import eu.siacs.conversations.xmpp.pep.Avatar; import eu.siacs.conversations.xmpp.stanzas.IqPacket; @@ -18,7 +19,7 @@ public class IqGenerator extends AbstractGenerator { public IqPacket discoResponse(IqPacket request) { IqPacket packet = new IqPacket(IqPacket.TYPE_RESULT); packet.setId(request.getId()); - packet.setTo(request.getFrom()); + packet.setTo(request.getFrom()); Element query = packet.addChild("query", "http://jabber.org/protocol/disco#info"); query.setAttribute("node", request.query().getAttribute("node")); @@ -86,8 +87,8 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public IqPacket retrieveAvatarMetaData(String to) { - IqPacket packet = retrieve("urn:xmpp:avatar:metadata", null); + public IqPacket retrieveAvatarMetaData(final Jid to) { + final IqPacket packet = retrieve("urn:xmpp:avatar:metadata", null); if (to != null) { packet.setTo(to); } diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index dd833e56..65f776ff 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -12,6 +12,7 @@ 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.jid.Jid; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; public class MessageGenerator extends AbstractGenerator { @@ -34,10 +35,10 @@ public class MessageGenerator extends AbstractGenerator { packet.setTo(message.getCounterpart()); packet.setType(MessagePacket.TYPE_CHAT); } else { - packet.setTo(message.getCounterpart().split("/", 2)[0]); + packet.setTo(message.getCounterpart().toBareJid()); packet.setType(MessagePacket.TYPE_GROUPCHAT); } - packet.setFrom(account.getFullJid()); + packet.setFrom(account.getJid()); packet.setId(message.getUuid()); if (addDelay) { addDelay(packet, message.getTimeSent()); @@ -113,17 +114,17 @@ public class MessageGenerator extends AbstractGenerator { private MessagePacket generateError(MessagePacket origin) { MessagePacket packet = new MessagePacket(); packet.setId(origin.getId()); - packet.setTo(origin.getFrom()); + packet.setTo(origin.getFrom()); packet.setBody(origin.getBody()); packet.setType(MessagePacket.TYPE_ERROR); return packet; } - public MessagePacket confirm(Account account, String to, String id) { + public MessagePacket confirm(final Account account, final Jid to, final String id) { MessagePacket packet = new MessagePacket(); packet.setType(MessagePacket.TYPE_NORMAL); packet.setTo(to); - packet.setFrom(account.getFullJid()); + packet.setFrom(account.getJid()); Element received = packet.addChild("displayed", "urn:xmpp:chat-markers:0"); received.setAttribute("id", id); @@ -134,28 +135,28 @@ public class MessageGenerator extends AbstractGenerator { String subject) { MessagePacket packet = new MessagePacket(); packet.setType(MessagePacket.TYPE_GROUPCHAT); - packet.setTo(conversation.getContactJid().split("/", 2)[0]); + packet.setTo(conversation.getContactJid().toBareJid()); Element subjectChild = new Element("subject"); subjectChild.setContent(subject); packet.addChild(subjectChild); - packet.setFrom(conversation.getAccount().getJid()); + packet.setFrom(conversation.getAccount().getJid().toBareJid()); return packet; } - public MessagePacket directInvite(Conversation conversation, String contact) { + public MessagePacket directInvite(final Conversation conversation, final Jid contact) { MessagePacket packet = new MessagePacket(); packet.setType(MessagePacket.TYPE_NORMAL); packet.setTo(contact); - packet.setFrom(conversation.getAccount().getFullJid()); + packet.setFrom(conversation.getAccount().getJid()); Element x = packet.addChild("x", "jabber:x:conference"); - x.setAttribute("jid", conversation.getContactJid().split("/", 2)[0]); + x.setAttribute("jid", conversation.getContactJid().toBareJid().toString()); 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()); + packet.setTo(conversation.getContactJid().toBareJid()); + packet.setFrom(conversation.getAccount().getJid()); Element x = new Element("x"); x.setAttribute("xmlns", "http://jabber.org/protocol/muc#user"); Element invite = new Element("invite"); @@ -169,8 +170,8 @@ public class MessageGenerator extends AbstractGenerator { MessagePacket originalMessage, String namespace) { MessagePacket receivedPacket = new MessagePacket(); receivedPacket.setType(MessagePacket.TYPE_NORMAL); - receivedPacket.setTo(originalMessage.getFrom()); - receivedPacket.setFrom(account.getFullJid()); + receivedPacket.setTo(originalMessage.getFrom()); + receivedPacket.setFrom(account.getJid()); Element received = receivedPacket.addChild("received", namespace); received.setAttribute("id", originalMessage.getId()); return receivedPacket; diff --git a/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java b/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java index d896dd00..e3642f6b 100644 --- a/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java @@ -15,8 +15,8 @@ public class PresenceGenerator extends AbstractGenerator { 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()); + packet.setTo(contact.getJid()); + packet.setFrom(contact.getAccount().getJid().toBareJid()); return packet; } @@ -38,7 +38,7 @@ public class PresenceGenerator extends AbstractGenerator { public PresencePacket sendPresence(Account account) { PresencePacket packet = new PresencePacket(); - packet.setAttribute("from", account.getFullJid()); + packet.setFrom(account.getJid()); String sig = account.getPgpSignature(); if (sig != null) { packet.addChild("status").setContent("online"); diff --git a/src/main/java/eu/siacs/conversations/http/HttpConnection.java b/src/main/java/eu/siacs/conversations/http/HttpConnection.java index 147ac42f..68c26c47 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpConnection.java +++ b/src/main/java/eu/siacs/conversations/http/HttpConnection.java @@ -3,6 +3,7 @@ package eu.siacs.conversations.http; import android.content.Intent; import android.graphics.BitmapFactory; import android.net.Uri; +import android.os.SystemClock; import org.apache.http.conn.ssl.StrictHostnameVerifier; @@ -21,6 +22,7 @@ import javax.net.ssl.SSLContext; import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.X509TrustManager; +import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Downloadable; import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message; @@ -37,6 +39,8 @@ public class HttpConnection implements Downloadable { private DownloadableFile file; private int mStatus = Downloadable.STATUS_UNKNOWN; private boolean acceptedAutomatically = false; + private int mProgress = 0; + private long mLastGuiRefresh = 0; public HttpConnection(HttpConnectionManager manager) { this.mHttpConnectionManager = manager; @@ -235,10 +239,14 @@ public class HttpConnection implements Downloadable { if (os == null) { throw new IOException(); } + long transmitted = 0; + long expected = file.getExpectedSize(); int count = -1; byte[] buffer = new byte[1024]; while ((count = is.read(buffer)) != -1) { + transmitted += count; os.write(buffer, 0, count); + updateProgress((int) ((((double) transmitted) / expected) * 100)); } os.flush(); os.close(); @@ -246,19 +254,21 @@ public class HttpConnection implements Downloadable { } 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.getFileBackend().updateFileParams(message,mUrl); mXmppConnectionService.updateMessage(message); } } + public void updateProgress(int i) { + this.mProgress = i; + if (SystemClock.elapsedRealtime() - this.mLastGuiRefresh > Config.PROGRESS_UI_UPDATE_INTERVAL) { + this.mLastGuiRefresh = SystemClock.elapsedRealtime(); + mXmppConnectionService.updateConversationUi(); + } + } + @Override public int getStatus() { return this.mStatus; @@ -272,4 +282,14 @@ public class HttpConnection implements Downloadable { return 0; } } + + @Override + public int getProgress() { + return this.mProgress; + } + + @Override + public String getMimeType() { + return ""; + } }
\ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java index 5541c1c6..eedfca16 100644 --- a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java +++ b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java @@ -11,6 +11,8 @@ import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.jid.InvalidJidException; +import eu.siacs.conversations.xmpp.jid.Jid; public abstract class AbstractParser { @@ -22,7 +24,7 @@ public abstract class AbstractParser { protected long getTimestamp(Element packet) { long now = System.currentTimeMillis(); - ArrayList<String> stamps = new ArrayList<String>(); + ArrayList<String> stamps = new ArrayList<>(); for (Element child : packet.getChildren()) { if (child.getName().equals("delay")) { stamps.add(child.getAttribute("stamp").replace("Z", "+0000")); @@ -58,21 +60,21 @@ public abstract class AbstractParser { } } - 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 = ""; - } + protected void updateLastseen(final Element packet, final Account account, + final boolean presenceOverwrite) { + Jid from; + try { + from = Jid.fromString(packet.getAttribute("from")).toBareJid(); + } catch (final InvalidJidException e) { + // TODO: Handle this? + from = null; + } + String presence = from == null || from.isBareJid() ? "" : from.getResourcepart(); Contact contact = account.getRoster().getContact(from); long timestamp = getTimestamp(packet); if (timestamp >= contact.lastseen.time) { contact.lastseen.time = timestamp; - if ((presence != null) && (presenceOverwrite)) { + if (!presence.isEmpty() && presenceOverwrite) { contact.lastseen.presence = presence; } } diff --git a/src/main/java/eu/siacs/conversations/parser/IqParser.java b/src/main/java/eu/siacs/conversations/parser/IqParser.java index df6754f2..d5d1f3a0 100644 --- a/src/main/java/eu/siacs/conversations/parser/IqParser.java +++ b/src/main/java/eu/siacs/conversations/parser/IqParser.java @@ -5,6 +5,8 @@ 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.jid.InvalidJidException; +import eu.siacs.conversations.xmpp.jid.Jid; import eu.siacs.conversations.xmpp.stanzas.IqPacket; public class IqParser extends AbstractParser implements OnIqPacketReceived { @@ -20,8 +22,14 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { } for (Element item : query.getChildren()) { if (item.getName().equals("item")) { - String jid = item.getAttribute("jid"); - String name = item.getAttribute("name"); + Jid jid; + try { + jid = Jid.fromString(item.getAttribute("jid")); + } catch (final InvalidJidException e) { + // TODO: Handle this? + jid = null; + } + String name = item.getAttribute("name"); String subscription = item.getAttribute("subscription"); Contact contact = account.getRoster().getContact(jid); if (!contact.getOption(Contact.Options.DIRTY_PUSH)) { @@ -59,8 +67,8 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { @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()))) { + final Jid from = packet.getFrom(); + if ((from == null) || (from.equals(account.getJid().toBareJid()))) { Element query = packet.findChild("query"); this.rosterItems(account, query); } diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 7fab1b1b..b7aef931 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -1,8 +1,11 @@ package eu.siacs.conversations.parser; +import android.util.Log; + import net.java.otr4j.session.Session; import net.java.otr4j.session.SessionStatus; +import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; @@ -11,6 +14,8 @@ 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.jid.InvalidJidException; +import eu.siacs.conversations.xmpp.jid.Jid; import eu.siacs.conversations.xmpp.pep.Avatar; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; @@ -21,48 +26,48 @@ public class MessageParser extends AbstractParser implements } private Message parseChat(MessagePacket packet, Account account) { - String[] fromParts = packet.getFrom().split("/", 2); + final Jid jid = packet.getFrom(); Conversation conversation = mXmppConnectionService - .findOrCreateConversation(account, fromParts[0], false); + .findOrCreateConversation(account, jid.toBareJid(), false); updateLastseen(packet, account, true); String pgpBody = getPgpBody(packet); Message finishedMessage; if (pgpBody != null) { - finishedMessage = new Message(conversation, packet.getFrom(), + finishedMessage = new Message(conversation, pgpBody, Message.ENCRYPTION_PGP, Message.STATUS_RECEIVED); } else { - finishedMessage = new Message(conversation, packet.getFrom(), + finishedMessage = new Message(conversation, 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) { + && !jid.isBareJid()) { finishedMessage.setType(Message.TYPE_PRIVATE); - finishedMessage.setPresence(fromParts[1]); finishedMessage.setTrueCounterpart(conversation.getMucOptions() - .getTrueCounterpart(fromParts[1])); + .getTrueCounterpart(jid.getResourcepart())); if (conversation.hasDuplicateMessage(finishedMessage)) { return null; } } + finishedMessage.setCounterpart(jid); finishedMessage.setTime(getTimestamp(packet)); return finishedMessage; } private Message parseOtrChat(MessagePacket packet, Account account) { - boolean properlyAddressed = (packet.getTo().split("/", 2).length == 2) + boolean properlyAddressed = (!packet.getTo().isBareJid()) || (account.countPresences() == 1); - String[] fromParts = packet.getFrom().split("/", 2); + final Jid from = packet.getFrom(); Conversation conversation = mXmppConnectionService - .findOrCreateConversation(account, fromParts[0], false); + .findOrCreateConversation(account, from.toBareJid(), false); String presence; - if (fromParts.length >= 2) { - presence = fromParts[1]; + if (from.isBareJid()) { + presence = ""; } else { - presence = ""; + presence = from.getResourcepart(); } updateLastseen(packet, account, true); String body = packet.getBody(); @@ -71,8 +76,7 @@ public class MessageParser extends AbstractParser implements } if (!conversation.hasValidOtrSession()) { if (properlyAddressed) { - conversation.startOtrSession(mXmppConnectionService, presence, - false); + conversation.startOtrSession(presence,false); } else { return null; } @@ -82,8 +86,7 @@ public class MessageParser extends AbstractParser implements if (!foreignPresence.equals(presence)) { conversation.endOtrIfNeeded(); if (properlyAddressed) { - conversation.startOtrSession(mXmppConnectionService, - presence, false); + conversation.startOtrSession(presence, false); } else { return null; } @@ -108,12 +111,12 @@ public class MessageParser extends AbstractParser implements conversation.setSymmetricKey(CryptoHelper.hexToBytes(key)); return null; } - Message finishedMessage = new Message(conversation, - packet.getFrom(), body, Message.ENCRYPTION_OTR, + Message finishedMessage = new Message(conversation, body, Message.ENCRYPTION_OTR, Message.STATUS_RECEIVED); finishedMessage.setTime(getTimestamp(packet)); finishedMessage.setRemoteMsgId(packet.getId()); finishedMessage.markable = isMarkable(packet); + finishedMessage.setCounterpart(from); return finishedMessage; } catch (Exception e) { String receivedId = packet.getId(); @@ -127,24 +130,26 @@ public class MessageParser extends AbstractParser implements private Message parseGroupchat(MessagePacket packet, Account account) { int status; - String[] fromParts = packet.getFrom().split("/", 2); + final Jid from = packet.getFrom(); + if (from == null) { + return null; + } if (mXmppConnectionService.find(account.pendingConferenceLeaves, - account, fromParts[0]) != null) { + account, from.toBareJid()) != null) { return null; } Conversation conversation = mXmppConnectionService - .findOrCreateConversation(account, fromParts[0], true); + .findOrCreateConversation(account, from.toBareJid(), true); if (packet.hasChild("subject")) { conversation.getMucOptions().setSubject( packet.findChild("subject").getContent()); mXmppConnectionService.updateConversationUi(); return null; } - if ((fromParts.length == 1)) { + if (from.isBareJid()) { return null; } - String counterPart = fromParts[1]; - if (counterPart.equals(conversation.getMucOptions().getActualNick())) { + if (from.getResourcepart().equals(conversation.getMucOptions().getActualNick())) { if (mXmppConnectionService.markMessage(conversation, packet.getId(), Message.STATUS_SEND)) { return null; @@ -157,17 +162,18 @@ public class MessageParser extends AbstractParser implements String pgpBody = getPgpBody(packet); Message finishedMessage; if (pgpBody == null) { - finishedMessage = new Message(conversation, counterPart, + finishedMessage = new Message(conversation, packet.getBody(), Message.ENCRYPTION_NONE, status); } else { - finishedMessage = new Message(conversation, counterPart, pgpBody, + finishedMessage = new Message(conversation, pgpBody, Message.ENCRYPTION_PGP, status); } finishedMessage.setRemoteMsgId(packet.getId()); finishedMessage.markable = isMarkable(packet); + finishedMessage.setCounterpart(from); if (status == Message.STATUS_RECEIVED) { finishedMessage.setTrueCounterpart(conversation.getMucOptions() - .getTrueCounterpart(counterPart)); + .getTrueCounterpart(from.getResourcepart())); } if (packet.hasChild("delay") && conversation.hasDuplicateMessage(finishedMessage)) { @@ -177,9 +183,9 @@ public class MessageParser extends AbstractParser implements return finishedMessage; } - private Message parseCarbonMessage(MessagePacket packet, Account account) { + private Message parseCarbonMessage(final MessagePacket packet, final Account account) { int status; - String fullJid; + final Jid fullJid; Element forwarded; if (packet.hasChild("received", "urn:xmpp:carbons:2")) { forwarded = packet.findChild("received", "urn:xmpp:carbons:2") @@ -205,11 +211,11 @@ public class MessageParser extends AbstractParser implements parseNonMessage(message, account); } else if (status == Message.STATUS_SEND && message.hasChild("displayed", "urn:xmpp:chat-markers:0")) { - String to = message.getAttribute("to"); + final Jid to = message.getAttributeAsJid("to"); if (to != null) { - Conversation conversation = mXmppConnectionService.find( + final Conversation conversation = mXmppConnectionService.find( mXmppConnectionService.getConversations(), account, - to.split("/")[0]); + to.toBareJid()); if (conversation != null) { mXmppConnectionService.markRead(conversation, false); } @@ -218,40 +224,39 @@ public class MessageParser extends AbstractParser implements return null; } if (status == Message.STATUS_RECEIVED) { - fullJid = message.getAttribute("from"); + fullJid = message.getAttributeAsJid("from"); if (fullJid == null) { return null; } else { updateLastseen(message, account, true); } } else { - fullJid = message.getAttribute("to"); + fullJid = message.getAttributeAsJid("to"); if (fullJid == null) { return null; } } - String[] parts = fullJid.split("/", 2); Conversation conversation = mXmppConnectionService - .findOrCreateConversation(account, parts[0], false); + .findOrCreateConversation(account, fullJid.toBareJid(), false); String pgpBody = getPgpBody(message); Message finishedMessage; if (pgpBody != null) { - finishedMessage = new Message(conversation, fullJid, pgpBody, + finishedMessage = new Message(conversation, pgpBody, Message.ENCRYPTION_PGP, status); } else { String body = message.findChild("body").getContent(); - finishedMessage = new Message(conversation, fullJid, body, + finishedMessage = new Message(conversation, body, Message.ENCRYPTION_NONE, status); } finishedMessage.setTime(getTimestamp(message)); finishedMessage.setRemoteMsgId(message.getAttribute("id")); finishedMessage.markable = isMarkable(message); + finishedMessage.setCounterpart(fullJid); if (conversation.getMode() == Conversation.MODE_MULTI - && parts.length >= 2) { + && !fullJid.isBareJid()) { finishedMessage.setType(Message.TYPE_PRIVATE); - finishedMessage.setPresence(parts[1]); finishedMessage.setTrueCounterpart(conversation.getMucOptions() - .getTrueCounterpart(parts[1])); + .getTrueCounterpart(fullJid.getResourcepart())); if (conversation.hasDuplicateMessage(finishedMessage)) { return null; } @@ -259,39 +264,39 @@ public class MessageParser extends AbstractParser implements return finishedMessage; } - private void parseError(MessagePacket packet, Account account) { - String[] fromParts = packet.getFrom().split("/", 2); - mXmppConnectionService.markMessage(account, fromParts[0], + private void parseError(final MessagePacket packet, final Account account) { + final Jid from = packet.getFrom(); + mXmppConnectionService.markMessage(account, from.toBareJid(), packet.getId(), Message.STATUS_SEND_FAILED); } private void parseNonMessage(Element packet, Account account) { - String from = packet.getAttribute("from"); + final Jid from = packet.getAttributeAsJid("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); + parseEvent(event, 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], + mXmppConnectionService.markMessage(account, from.toBareJid(), 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], + mXmppConnectionService.markMessage(account, from.toBareJid(), 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], + mXmppConnectionService.markMessage(account, from.toBareJid(), id, Message.STATUS_SEND_RECEIVED); } else if (packet.hasChild("x", "http://jabber.org/protocol/muc#user")) { Element x = packet.findChild("x", @@ -299,7 +304,7 @@ public class MessageParser extends AbstractParser implements if (x.hasChild("invite")) { Conversation conversation = mXmppConnectionService .findOrCreateConversation(account, - packet.getAttribute("from"), true); + packet.getAttributeAsJid("from"), true); if (!conversation.getMucOptions().online()) { if (x.hasChild("password")) { Element password = x.findChild("password"); @@ -314,8 +319,13 @@ public class MessageParser extends AbstractParser implements } } 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"); + Jid jid; + try { + jid = Jid.fromString(x.getAttribute("jid")); + } catch (InvalidJidException e) { + jid = null; + } + String password = x.getAttribute("password"); if (jid != null) { Conversation conversation = mXmppConnectionService .findOrCreateConversation(account, jid, true); @@ -332,7 +342,7 @@ public class MessageParser extends AbstractParser implements } } - private void parseEvent(Element event, String from, Account account) { + private void parseEvent(final Element event, final Jid from, final Account account) { Element items = event.findChild("items"); String node = items.getAttribute("node"); if (node != null) { @@ -342,7 +352,7 @@ public class MessageParser extends AbstractParser implements avatar.owner = from; if (mXmppConnectionService.getFileBackend().isAvatarCached( avatar)) { - if (account.getJid().equals(from)) { + if (account.getJid().toBareJid().equals(from)) { if (account.setAvatar(avatar.getFilename())) { mXmppConnectionService.databaseBackend .updateAccount(account); @@ -473,7 +483,8 @@ public class MessageParser extends AbstractParser implements if (message.getStatus() == Message.STATUS_RECEIVED && conversation.getOtrSession() != null && !conversation.getOtrSession().getSessionID().getUserID() - .equals(message.getPresence())) { + .equals(message.getCounterpart().getResourcepart())) { + Log.d(Config.LOGTAG, "ending because of reasons"); conversation.endOtrIfNeeded(); } diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java index 4e90cda8..635f2932 100644 --- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java +++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java @@ -9,6 +9,7 @@ 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.jid.Jid; import eu.siacs.conversations.xmpp.stanzas.PresencePacket; public class PresenceParser extends AbstractParser implements @@ -21,8 +22,9 @@ public class PresenceParser extends AbstractParser implements 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]); + final Conversation muc = packet.getFrom() == null ? null : mXmppConnectionService.find( + account, + packet.getFrom().toBareJid()); if (muc != null) { boolean before = muc.getMucOptions().online(); muc.getMucOptions().processPacket(packet, mPgpEngine); @@ -32,8 +34,8 @@ public class PresenceParser extends AbstractParser implements 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]); + final Conversation muc = mXmppConnectionService.find(account, + packet.getFrom().toBareJid()); if (muc != null) { boolean before = muc.getMucOptions().online(); muc.getMucOptions().processPacket(packet, mPgpEngine); @@ -51,15 +53,15 @@ public class PresenceParser extends AbstractParser implements if (packet.getFrom() == null) { return; } - String[] fromParts = packet.getFrom().split("/", 2); + final Jid from = packet.getFrom(); String type = packet.getAttribute("type"); - if (fromParts[0].equals(account.getJid())) { - if (fromParts.length == 2) { + if (from.toBareJid().equals(account.getJid().toBareJid())) { + if (!from.isBareJid()) { if (type == null) { - account.updatePresence(fromParts[1], + account.updatePresence(from.getResourcepart(), Presences.parseShow(packet.findChild("show"))); } else if (type.equals("unavailable")) { - account.removePresence(fromParts[1]); + account.removePresence(from.getResourcepart()); account.deactivateGracePeriod(); } } @@ -67,8 +69,8 @@ public class PresenceParser extends AbstractParser implements Contact contact = account.getRoster().getContact(packet.getFrom()); if (type == null) { String presence; - if (fromParts.length >= 2) { - presence = fromParts[1]; + if (!from.isBareJid()) { + presence = from.getResourcepart(); } else { presence = ""; } @@ -95,10 +97,10 @@ public class PresenceParser extends AbstractParser implements mXmppConnectionService.onContactStatusChanged .onContactStatusChanged(contact, online); } else if (type.equals("unavailable")) { - if (fromParts.length != 2) { + if (from.isBareJid()) { contact.clearPresences(); } else { - contact.removePresence(fromParts[1]); + contact.removePresence(from.getResourcepart()); } mXmppConnectionService.onContactStatusChanged .onContactStatusChanged(contact, false); diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index e5ea8f90..6879a068 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -9,6 +9,8 @@ import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Roster; +import eu.siacs.conversations.xmpp.jid.Jid; + import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteCantOpenDatabaseException; @@ -20,7 +22,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { private static DatabaseBackend instance = null; private static final String DATABASE_NAME = "history"; - private static final int DATABASE_VERSION = 9; + private static final int DATABASE_VERSION = 10; private static String CREATE_CONTATCS_STATEMENT = "create table " + Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, " @@ -62,6 +64,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { + " TEXT, " + Message.TRUE_COUNTERPART + " TEXT," + Message.BODY + " TEXT, " + Message.ENCRYPTION + " NUMBER, " + Message.STATUS + " NUMBER," + Message.TYPE + " NUMBER, " + + Message.RELATIVE_FILE_PATH + " TEXT, " + Message.REMOTE_MSG_ID + " TEXT, FOREIGN KEY(" + Message.CONVERSATION + ") REFERENCES " + Conversation.TABLENAME + "(" + Conversation.UUID @@ -108,6 +111,10 @@ public class DatabaseBackend extends SQLiteOpenHelper { db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " + Contact.LAST_PRESENCE + " TEXT"); } + if (oldVersion < 10 && newVersion >= 10) { + db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + + Message.RELATIVE_FILE_PATH + " TEXT"); + } } public static synchronized DatabaseBackend getInstance(Context context) { @@ -149,7 +156,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { } public CopyOnWriteArrayList<Conversation> getConversations(int status) { - CopyOnWriteArrayList<Conversation> list = new CopyOnWriteArrayList<Conversation>(); + CopyOnWriteArrayList<Conversation> list = new CopyOnWriteArrayList<>(); SQLiteDatabase db = this.getReadableDatabase(); String[] selectionArgs = { Integer.toString(status) }; Cursor cursor = db.rawQuery("select * from " + Conversation.TABLENAME @@ -168,7 +175,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { public ArrayList<Message> getMessages(Conversation conversation, int limit, long timestamp) { - ArrayList<Message> list = new ArrayList<Message>(); + ArrayList<Message> list = new ArrayList<>(); SQLiteDatabase db = this.getReadableDatabase(); Cursor cursor; if (timestamp == -1) { @@ -196,9 +203,9 @@ public class DatabaseBackend extends SQLiteOpenHelper { return list; } - public Conversation findConversation(Account account, String contactJid) { + public Conversation findConversation(final Account account, final Jid contactJid) { SQLiteDatabase db = this.getReadableDatabase(); - String[] selectionArgs = { account.getUuid(), contactJid + "%" }; + String[] selectionArgs = { account.getUuid(), contactJid.toBareJid().toString() + "%" }; Cursor cursor = db.query(Conversation.TABLENAME, null, Conversation.ACCOUNT + "=? AND " + Conversation.CONTACTJID + " like ?", selectionArgs, null, null, null); @@ -218,7 +225,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { } public List<Account> getAccounts() { - List<Account> list = new ArrayList<Account>(); + List<Account> list = new ArrayList<>(); SQLiteDatabase db = this.getReadableDatabase(); Cursor cursor = db.query(Account.TABLENAME, null, null, null, null, null, null); @@ -282,15 +289,15 @@ public class DatabaseBackend extends SQLiteOpenHelper { cursor.close(); } - public void writeRoster(Roster roster) { - Account account = roster.getAccount(); - SQLiteDatabase db = this.getWritableDatabase(); + public void writeRoster(final Roster roster) { + final Account account = roster.getAccount(); + final 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() }; + String[] whereArgs = { account.getUuid(), contact.getJid().toString() }; db.delete(Contact.TABLENAME, where, whereArgs); } } @@ -353,7 +360,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { } public List<Message> getImageMessages(Conversation conversation) { - ArrayList<Message> list = new ArrayList<Message>(); + ArrayList<Message> list = new ArrayList<>(); SQLiteDatabase db = this.getReadableDatabase(); Cursor cursor; String[] selectionArgs = { conversation.getUuid(), String.valueOf(Message.TYPE_IMAGE) }; diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index 0241b77e..9683d38d 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -7,6 +7,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.net.URL; import java.security.DigestOutputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -26,6 +27,8 @@ import android.provider.MediaStore; import android.util.Base64; import android.util.Base64OutputStream; import android.util.Log; +import android.webkit.MimeTypeMap; + import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.DownloadableFile; @@ -53,25 +56,40 @@ public class FileBackend { } 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"); + String path = message.getRelativeFilePath(); + if (!decrypted && (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED)) { + String extension; + if (path != null && !path.isEmpty()) { + String[] parts = path.split("\\."); + extension = "."+parts[parts.length - 1]; + } else if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_TEXT) { + extension = ".webp"; } else { - filename.append(".webp.pgp"); + extension = ""; } + return new DownloadableFile(getConversationsFileDirectory()+message.getUuid()+extension+".pgp"); + } else if (path != null && !path.isEmpty()) { + if (path.startsWith("/")) { + return new DownloadableFile(path); + } else { + return new DownloadableFile(getConversationsFileDirectory()+path); + } + } else { + StringBuilder filename = new StringBuilder(); + filename.append(getConversationsImageDirectory()); + filename.append(message.getUuid()+".webp"); + return new DownloadableFile(filename.toString()); } - return new DownloadableFile(filename.toString()); } - public static String getConversationsDirectory() { + public static String getConversationsFileDirectory() { + return Environment.getExternalStorageDirectory().getAbsolutePath()+"/Conversations/"; + } + + public static String getConversationsImageDirectory() { return Environment.getExternalStoragePublicDirectory( - Environment.DIRECTORY_PICTURES).getAbsolutePath() - + "/Conversations/"; + Environment.DIRECTORY_PICTURES).getAbsolutePath() + + "/Conversations/"; } public Bitmap resize(Bitmap originalBitmap, int size) { @@ -103,13 +121,60 @@ public class FileBackend { return Bitmap.createBitmap(bitmap, 0, 0, w, h, mtx, true); } + public String getOriginalPath(Uri uri) { + String path = null; + if (uri.getScheme().equals("file")) { + return uri.getPath(); + } else if (uri.toString().startsWith("content://media/")) { + String[] projection = {MediaStore.MediaColumns.DATA}; + Cursor metaCursor = mXmppConnectionService.getContentResolver().query(uri, + projection, null, null, null); + if (metaCursor != null) { + try { + if (metaCursor.moveToFirst()) { + path = metaCursor.getString(0); + } + } finally { + metaCursor.close(); + } + } + } + return path; + } + + public DownloadableFile copyFileToPrivateStorage(Message message, Uri uri) throws FileCopyException { + try { + Log.d(Config.LOGTAG, "copy " + uri.toString() + " to private storage"); + String mime = mXmppConnectionService.getContentResolver().getType(uri); + String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mime); + message.setRelativeFilePath(message.getUuid() + "." + extension); + DownloadableFile file = mXmppConnectionService.getFileBackend().getFile(message); + OutputStream os = new FileOutputStream(file); + InputStream is = mXmppConnectionService.getContentResolver().openInputStream(uri); + byte[] buffer = new byte[1024]; + int length; + while ((length = is.read(buffer)) > 0) { + os.write(buffer, 0, length); + } + os.flush(); + os.close(); + is.close(); + Log.d(Config.LOGTAG, "output file name " + mXmppConnectionService.getFileBackend().getFile(message)); + return file; + } catch (FileNotFoundException e) { + throw new FileCopyException(R.string.error_file_not_found); + } catch (IOException e) { + throw new FileCopyException(R.string.error_io_exception); + } + } + public DownloadableFile copyImageToPrivateStorage(Message message, Uri image) - throws ImageCopyException { + throws FileCopyException { return this.copyImageToPrivateStorage(message, image, 0); } private DownloadableFile copyImageToPrivateStorage(Message message, - Uri image, int sampleSize) throws ImageCopyException { + Uri image, int sampleSize) throws FileCopyException { try { InputStream is = mXmppConnectionService.getContentResolver() .openInputStream(image); @@ -125,7 +190,7 @@ public class FileBackend { originalBitmap = BitmapFactory.decodeStream(is, null, options); is.close(); if (originalBitmap == null) { - throw new ImageCopyException(R.string.error_not_an_image_file); + throw new FileCopyException(R.string.error_not_an_image_file); } Bitmap scalledBitmap = resize(originalBitmap, IMAGE_SIZE); originalBitmap = null; @@ -137,7 +202,7 @@ public class FileBackend { boolean success = scalledBitmap.compress( Bitmap.CompressFormat.WEBP, 75, os); if (!success) { - throw new ImageCopyException(R.string.error_compressing_image); + throw new FileCopyException(R.string.error_compressing_image); } os.flush(); os.close(); @@ -147,18 +212,18 @@ public class FileBackend { message.setBody(Long.toString(size) + ',' + width + ',' + height); return file; } catch (FileNotFoundException e) { - throw new ImageCopyException(R.string.error_file_not_found); + throw new FileCopyException(R.string.error_file_not_found); } catch (IOException e) { - throw new ImageCopyException(R.string.error_io_exception); + throw new FileCopyException(R.string.error_io_exception); } catch (SecurityException e) { - throw new ImageCopyException( + throw new FileCopyException( 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); + throw new FileCopyException(R.string.error_out_of_memory); } } } @@ -400,11 +465,34 @@ public class FileBackend { return Uri.parse("file://" + file.getAbsolutePath()); } - public class ImageCopyException extends Exception { + public void updateFileParams(Message message) { + updateFileParams(message,null); + } + + public void updateFileParams(Message message, URL url) { + DownloadableFile file = getFile(message); + if (message.getType() == Message.TYPE_IMAGE || file.getMimeType().startsWith("image/")) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(file.getAbsolutePath(), options); + int imageHeight = options.outHeight; + int imageWidth = options.outWidth; + if (url == null) { + message.setBody(Long.toString(file.getSize()) + '|' + imageWidth + '|' + imageHeight); + } else { + message.setBody(url.toString()+"|"+Long.toString(file.getSize()) + '|' + imageWidth + '|' + imageHeight); + } + } else { + message.setBody(Long.toString(file.getSize())); + } + + } + + public class FileCopyException extends Exception { private static final long serialVersionUID = -1010013599132881427L; private int resId; - public ImageCopyException(int resId) { + public FileCopyException(int resId) { this.resId = resId; } diff --git a/src/main/java/eu/siacs/conversations/services/AvatarService.java b/src/main/java/eu/siacs/conversations/services/AvatarService.java index bdf18f54..83bdacf8 100644 --- a/src/main/java/eu/siacs/conversations/services/AvatarService.java +++ b/src/main/java/eu/siacs/conversations/services/AvatarService.java @@ -29,7 +29,7 @@ public class AvatarService { private static final String PREFIX_ACCOUNT = "account"; private static final String PREFIX_GENERIC = "generic"; - private ArrayList<Integer> sizes = new ArrayList<Integer>(); + final private ArrayList<Integer> sizes = new ArrayList<>(); protected XmppConnectionService mXmppConnectionService = null; @@ -37,7 +37,7 @@ public class AvatarService { this.mXmppConnectionService = service; } - public Bitmap get(Contact contact, int size) { + public Bitmap get(final Contact contact, final int size) { final String KEY = key(contact, size); Bitmap avatar = this.mXmppConnectionService.getBitmapCache().get(KEY); if (avatar != null) { @@ -50,7 +50,7 @@ public class AvatarService { avatar = mXmppConnectionService.getFileBackend().getAvatar(contact.getAvatar(), size); } if (avatar == null) { - avatar = get(contact.getDisplayName(), size); + avatar = get(contact.getDisplayName(), size); } this.mXmppConnectionService.getBitmapCache().put(KEY, avatar); return avatar; @@ -69,7 +69,7 @@ public class AvatarService { this.sizes.add(size); } } - return PREFIX_CONTACT + "_" + contact.getAccount().getJid() + "_" + return PREFIX_CONTACT + "_" + contact.getAccount().getJid().toBareJid() + "_" + contact.getJid() + "_" + String.valueOf(size); } @@ -174,7 +174,7 @@ public class AvatarService { avatar = mXmppConnectionService.getFileBackend().getAvatar( account.getAvatar(), size); if (avatar == null) { - avatar = get(account.getJid(), size); + avatar = get(account.getJid().toBareJid().toString(), size); } mXmppConnectionService.getBitmapCache().put(KEY, avatar); return avatar; @@ -197,7 +197,7 @@ public class AvatarService { + String.valueOf(size); } - public Bitmap get(String name, int size) { + public Bitmap get(final String name, final int size) { final String KEY = key(name, size); Bitmap bitmap = mXmppConnectionService.getBitmapCache().get(KEY); if (bitmap != null) { diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index 4cb28145..c23a95b9 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -28,6 +28,7 @@ import eu.siacs.conversations.R; 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.ui.ConversationActivity; @@ -38,6 +39,7 @@ public class NotificationService { private LinkedHashMap<String, ArrayList<Message>> notifications = new LinkedHashMap<String, ArrayList<Message>>(); public static int NOTIFICATION_ID = 0x2342; + public static int FOREGROUND_NOTIFICATION_ID = 0x8899; private Conversation mOpenConversation; private boolean mIsInForeground; private long mLastNotification; @@ -266,14 +268,21 @@ public class NotificationService { if (message.getDownloadable() != null && (message.getDownloadable().getStatus() == Downloadable.STATUS_OFFER || message .getDownloadable().getStatus() == Downloadable.STATUS_OFFER_CHECK_FILESIZE)) { - return mXmppConnectionService.getText( - R.string.image_offered_for_download).toString(); + if (message.getType() == Message.TYPE_FILE) { + return mXmppConnectionService.getString(R.string.file_offered_for_download); + } else { + return mXmppConnectionService.getText( + R.string.image_offered_for_download).toString(); + } } else if (message.getEncryption() == Message.ENCRYPTION_PGP) { return mXmppConnectionService.getText( R.string.encrypted_message_received).toString(); } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) { return mXmppConnectionService.getText(R.string.decryption_failed) .toString(); + } else if (message.getType() == Message.TYPE_FILE) { + DownloadableFile file = mXmppConnectionService.getFileBackend().getFile(message); + return mXmppConnectionService.getString(R.string.file,file.getMimeType()); } else if (message.getType() == Message.TYPE_IMAGE) { return mXmppConnectionService.getText(R.string.image_file) .toString(); @@ -290,9 +299,11 @@ public class NotificationService { Intent viewConversationIntent = new Intent(mXmppConnectionService, ConversationActivity.class); viewConversationIntent.setAction(Intent.ACTION_VIEW); - viewConversationIntent.putExtra(ConversationActivity.CONVERSATION, - conversationUuid); - viewConversationIntent.setType(ConversationActivity.VIEW_CONVERSATION); + if (conversationUuid!=null) { + viewConversationIntent.putExtra(ConversationActivity.CONVERSATION, + conversationUuid); + viewConversationIntent.setType(ConversationActivity.VIEW_CONVERSATION); + } stackBuilder.addNextIntent(viewConversationIntent); @@ -304,7 +315,14 @@ public class NotificationService { private PendingIntent createDeleteIntent() { Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class); - intent.setAction("clear_notification"); + intent.setAction(XmppConnectionService.ACTION_CLEAR_NOTIFICATION); + return PendingIntent.getService(mXmppConnectionService, 0, intent, 0); + } + + private PendingIntent createDisableForeground() { + Intent intent = new Intent(mXmppConnectionService, + XmppConnectionService.class); + intent.setAction(XmppConnectionService.ACTION_DISABLE_FOREGROUND); return PendingIntent.getService(mXmppConnectionService, 0, intent, 0); } @@ -347,8 +365,19 @@ public class NotificationService { } private boolean inMiniGracePeriod(Account account) { - int miniGrace = account.getStatus() == Account.STATUS_ONLINE ? Config.MINI_GRACE_PERIOD + int miniGrace = account.getStatus() == Account.State.ONLINE ? Config.MINI_GRACE_PERIOD : Config.MINI_GRACE_PERIOD * 2; return SystemClock.elapsedRealtime() < (this.mLastNotification + miniGrace); } + + public Notification createForegroundNotification() { + NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService); + mBuilder.setSmallIcon(R.drawable.ic_stat_communication_import_export); + mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.conversations_foreground_service)); + mBuilder.setContentText(mXmppConnectionService.getString(R.string.touch_to_disable)); + mBuilder.setContentIntent(createDisableForeground()); + mBuilder.setWhen(0); + mBuilder.setPriority(NotificationCompat.PRIORITY_MIN); + return mBuilder.build(); + } } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index ea6f509f..509030c6 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -1,5 +1,40 @@ package eu.siacs.conversations.services; +import android.annotation.SuppressLint; +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.content.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; + +import net.java.otr4j.OtrException; +import net.java.otr4j.session.Session; +import net.java.otr4j.session.SessionID; +import net.java.otr4j.session.SessionStatus; + +import org.openintents.openpgp.util.OpenPgpApi; +import org.openintents.openpgp.util.OpenPgpServiceConnection; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.OutputStream; import java.security.SecureRandom; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -12,14 +47,7 @@ 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; @@ -28,6 +56,8 @@ 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.DownloadableFile; +import eu.siacs.conversations.entities.DownloadablePlaceholder; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.MucOptions.OnRenameListener; @@ -55,6 +85,8 @@ 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.jid.InvalidJidException; +import eu.siacs.conversations.xmpp.jid.Jid; import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived; import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; @@ -62,51 +94,50 @@ 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 static String ACTION_MERGE_PHONE_CONTACTS = "merge_phone_contacts"; + public static String ACTION_DISABLE_FOREGROUND = "disable_foreground"; + 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 final IBinder mBinder = new XmppConnectionBinder(); + public DatabaseBackend databaseBackend; + public OnContactStatusChanged onContactStatusChanged = new OnContactStatusChanged() { + @Override + public void onContactStatusChanged(Contact contact, boolean online) { + Conversation conversation = find(getConversations(), contact); + if (conversation != null) { + if (online && contact.getPresences().size() > 1) { + conversation.endOtrIfNeeded(); + } else { + conversation.resetOtrSession(); + } + if (online && (contact.getPresences().size() == 1)) { + sendUnsendMessages(conversation); + } + } + } + }; + private FileBackend fileBackend = new FileBackend(this); 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( @@ -114,56 +145,9 @@ public class XmppConnectionService extends Service { 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) { - if (online && contact.getPresences().size() > 1) { - conversation.endOtrIfNeeded(); - } else { - conversation.resetOtrSession(); - } - 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 @@ -171,9 +155,8 @@ public class XmppConnectionService extends Service { XmppConnection connection = account.getXmppConnection(); if (mOnAccountUpdate != null) { mOnAccountUpdate.onAccountUpdate(); - ; } - if (account.getStatus() == Account.STATUS_ONLINE) { + if (account.getStatus() == Account.State.ONLINE) { for (Conversation conversation : account.pendingConferenceLeaves) { leaveMuc(conversation); } @@ -182,50 +165,63 @@ public class XmppConnectionService extends Service { } 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)); + for (Conversation conversation : conversations) { + if (conversation.getAccount() == account) { + conversation.startOtrIfNeeded(); + sendUnsendMessages(conversation); } } if (connection != null && connection.getFeatures().csi()) { if (checkListeners()) { - Log.d(Config.LOGTAG, account.getJid() + Log.d(Config.LOGTAG, account.getJid().toBareJid() + " sending csi//inactive"); connection.sendInactive(); } else { - Log.d(Config.LOGTAG, account.getJid() + Log.d(Config.LOGTAG, account.getJid().toBareJid() + " sending csi//active"); connection.sendActive(); } } syncDirtyContacts(account); scheduleWakeupCall(Config.PING_MAX_INTERVAL, true); - } else if (account.getStatus() == Account.STATUS_OFFLINE) { + } else if (account.getStatus() == Account.State.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) { + } else if (account.getStatus() == Account.State.REGISTRATION_SUCCESSFUL) { databaseBackend.updateAccount(account); reconnectAccount(account, true); - } else if ((account.getStatus() != Account.STATUS_CONNECTING) - && (account.getStatus() != Account.STATUS_NO_INTERNET)) { + } else if ((account.getStatus() != Account.State.CONNECTING) + && (account.getStatus() != Account.State.NO_INTERNET)) { if (connection != null) { int next = connection.getTimeToNextAttempt(); - Log.d(Config.LOGTAG, account.getJid() + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": 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 Integer accountChangedListenerCount = 0; + private OnRosterUpdate mOnRosterUpdate = null; + private Integer rosterChangedListenerCount = 0; + private SecureRandom mRandom; + private FileObserver fileObserver = new FileObserver( + FileBackend.getConversationsImageDirectory()) { + @Override + public void onEvent(int event, String path) { + if (event == FileObserver.DELETE) { + markFileDeleted(path.split("\\.")[0]); + } + } + }; private OnJinglePacketReceived jingleListener = new OnJinglePacketReceived() { @Override @@ -265,24 +261,26 @@ public class XmppConnectionService extends Service { if (conversation.getAccount() == account) { for (Message message : conversation.getMessages()) { if ((message.getStatus() == Message.STATUS_UNSEND || message - .getStatus() == Message.STATUS_WAITING) + .getStatus() == Message.STATUS_WAITING) && message.getUuid().equals(uuid)) { markMessage(message, Message.STATUS_SEND); return; - } + } } } } } }; private LruCache<String, Bitmap> mBitmapCache; + private OnRenameListener renameListener = null; + private IqGenerator mIqGenerator = new IqGenerator(this); public PgpEngine getPgpEngine() { if (pgpServiceConnection.isBound()) { if (this.mPgpEngine == null) { this.mPgpEngine = new PgpEngine(new OpenPgpApi( - getApplicationContext(), - pgpServiceConnection.getService()), this); + getApplicationContext(), + pgpServiceConnection.getService()), this); } return mPgpEngine; } else { @@ -299,7 +297,49 @@ public class XmppConnectionService extends Service { return this.mAvatarService; } - public Message attachImageToConversation(final Conversation conversation, + public void attachFileToConversation(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.setCounterpart(conversation.getNextCounterpart()); + message.setType(Message.TYPE_FILE); + message.setStatus(Message.STATUS_OFFERED); + String path = getFileBackend().getOriginalPath(uri); + if (path!=null) { + message.setRelativeFilePath(path); + getFileBackend().updateFileParams(message); + if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { + getPgpEngine().encrypt(message, callback); + } else { + callback.success(message); + } + } else { + new Thread(new Runnable() { + @Override + public void run() { + try { + getFileBackend().copyFileToPrivateStorage(message, uri); + getFileBackend().updateFileParams(message); + if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { + getPgpEngine().encrypt(message, callback); + } else { + callback.success(message); + } + } catch (FileBackend.FileCopyException e) { + callback.error(e.getResId(),message); + } + } + }).start(); + + } + } + + public void attachImageToConversation(final Conversation conversation, final Uri uri, final UiCallback<Message> callback) { final Message message; if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) { @@ -309,7 +349,7 @@ public class XmppConnectionService extends Service { message = new Message(conversation, "", conversation.getNextEncryption(forceEncryption())); } - message.setPresence(conversation.getNextPresence()); + message.setCounterpart(conversation.getNextCounterpart()); message.setType(Message.TYPE_IMAGE); message.setStatus(Message.STATUS_OFFERED); new Thread(new Runnable() { @@ -317,34 +357,27 @@ public class XmppConnectionService extends Service { @Override public void run() { try { - getFileBackend().copyImageToPrivateStorage(message, uri); + DownloadableFile file = getFileBackend().copyImageToPrivateStorage(message, uri); if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) { getPgpEngine().encrypt(message, callback); } else { callback.success(message); } - } catch (FileBackend.ImageCopyException e) { + } catch (FileBackend.FileCopyException 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) { + public Conversation find(final Account account, final Jid 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) { @@ -356,6 +389,9 @@ public class XmppConnectionService extends Service { return START_NOT_STICKY; } else if (intent.getAction().equals(ACTION_CLEAR_NOTIFICATION)) { mNotificationService.clear(); + } else if (intent.getAction().equals(ACTION_DISABLE_FOREGROUND)) { + getPreferences().edit().putBoolean("keep_foreground_service",false).commit(); + toggleForegroundService(); } } this.wakeLock.acquire(); @@ -363,22 +399,22 @@ public class XmppConnectionService extends Service { for (Account account : accounts) { if (!account.isOptionSet(Account.OPTION_DISABLED)) { if (!hasInternetConnection()) { - account.setStatus(Account.STATUS_NO_INTERNET); + account.setStatus(Account.State.NO_INTERNET); if (statusListener != null) { statusListener.onStatusChanged(account); } } else { - if (account.getStatus() == Account.STATUS_NO_INTERNET) { - account.setStatus(Account.STATUS_OFFLINE); + if (account.getStatus() == Account.State.NO_INTERNET) { + account.setStatus(Account.State.OFFLINE); if (statusListener != null) { statusListener.onStatusChanged(account); } } - if (account.getStatus() == Account.STATUS_ONLINE) { + if (account.getStatus() == Account.State.ONLINE) { long lastReceived = account.getXmppConnection() - .getLastPacketReceived(); + .getLastPacketReceived(); long lastSent = account.getXmppConnection() - .getLastPingSent(); + .getLastPingSent(); if (lastSent - lastReceived >= Config.PING_TIMEOUT * 1000) { Log.d(Config.LOGTAG, account.getJid() + ": ping timeout"); @@ -387,13 +423,13 @@ public class XmppConnectionService extends Service { account.getXmppConnection().sendPing(); this.scheduleWakeupCall(2, false); } - } else if (account.getStatus() == Account.STATUS_OFFLINE) { + } else if (account.getStatus() == Account.State.OFFLINE) { if (account.getXmppConnection() == null) { account.setXmppConnection(this .createConnection(account)); } new Thread(account.getXmppConnection()).start(); - } else if ((account.getStatus() == Account.STATUS_CONNECTING) + } else if ((account.getStatus() == Account.State.CONNECTING) && ((SystemClock.elapsedRealtime() - account .getXmppConnection().getLastConnect()) / 1000 >= Config.CONNECT_TIMEOUT)) { Log.d(Config.LOGTAG, account.getJid() @@ -412,10 +448,14 @@ public class XmppConnectionService extends Service { } } } + /*PowerManager pm = (PowerManager) this.getSystemService(Context.POWER_SERVICE); + if (!pm.isScreenOn()) { + removeStaleListeners(); + }*/ if (wakeLock.isHeld()) { try { wakeLock.release(); - } catch (RuntimeException re) { + } catch (final RuntimeException ignored) { } } return START_STICKY; @@ -423,7 +463,7 @@ public class XmppConnectionService extends Service { public boolean hasInternetConnection() { ConnectivityManager cm = (ConnectivityManager) getApplicationContext() - .getSystemService(Context.CONNECTIVITY_SERVICE); + .getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); return activeNetwork != null && activeNetwork.isConnected(); } @@ -447,10 +487,11 @@ public class XmppConnectionService extends Service { }; this.databaseBackend = DatabaseBackend - .getInstance(getApplicationContext()); + .getInstance(getApplicationContext()); this.accounts = databaseBackend.getAccounts(); for (Account account : this.accounts) { + account.initOtrEngine(this); this.databaseBackend.readRoster(account.getRoster()); } this.mergePhoneContactsWithRoster(); @@ -466,18 +507,23 @@ public class XmppConnectionService extends Service { this.pm = (PowerManager) getSystemService(Context.POWER_SERVICE); this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "XmppConnectionService"); + toggleForegroundService(); } - @Override - public void onDestroy() { - super.onDestroy(); - this.logoutAndSave(); + public void toggleForegroundService() { + if (getPreferences().getBoolean("keep_foreground_service",false)) { + startForeground(NotificationService.FOREGROUND_NOTIFICATION_ID, this.mNotificationService.createForegroundNotification()); + } else { + stopForeground(true); + } } @Override public void onTaskRemoved(Intent rootIntent) { super.onTaskRemoved(rootIntent); - this.logoutAndSave(); + if (!getPreferences().getBoolean("keep_foreground_service",false)) { + this.logoutAndSave(); + } } private void logoutAndSave() { @@ -489,7 +535,7 @@ public class XmppConnectionService extends Service { } Context context = getApplicationContext(); AlarmManager alarmManager = (AlarmManager) context - .getSystemService(Context.ALARM_SERVICE); + .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"); @@ -500,7 +546,7 @@ public class XmppConnectionService extends Service { long timeToWake = SystemClock.elapsedRealtime() + seconds * 1000; Context context = getApplicationContext(); AlarmManager alarmManager = (AlarmManager) context - .getSystemService(Context.ALARM_SERVICE); + .getSystemService(Context.ALARM_SERVICE); if (ping) { if (this.pingIntent == null) { @@ -521,7 +567,7 @@ public class XmppConnectionService extends Service { context, 0, this.pingIntent, 0); alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, pendingPingIntent); - } + } } } else { Intent intent = new Intent(context, EventReceiver.class); @@ -546,7 +592,7 @@ public class XmppConnectionService extends Service { connection.setOnJinglePacketReceivedListener(this.jingleListener); connection.setOnBindListener(this.mOnBindListener); connection - .setOnMessageAcknowledgeListener(this.mOnMessageAcknowledgedListener); + .setOnMessageAcknowledgeListener(this.mOnMessageAcknowledgedListener); return connection; } @@ -557,21 +603,19 @@ public class XmppConnectionService extends Service { MessagePacket packet = null; boolean saveInDb = true; boolean send = false; - if (account.getStatus() == Account.STATUS_ONLINE + if (account.getStatus() == Account.State.ONLINE && account.getXmppConnection() != null) { - if (message.getType() == Message.TYPE_IMAGE) { - if (message.getPresence() != null) { + if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) { + if (message.getCounterpart() != null) { if (message.getEncryption() == Message.ENCRYPTION_OTR) { - if (!conv.hasValidOtrSession() - && (message.getPresence() != null)) { - conv.startOtrSession(this, message.getPresence(), - true); + if (!conv.hasValidOtrSession()) { + conv.startOtrSession(message.getCounterpart().getResourcepart(),true); message.setStatus(Message.STATUS_WAITING); } else if (conv.hasValidOtrSession() && conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) { mJingleConnectionManager - .createNewConnection(message); - } + .createNewConnection(message); + } } else { mJingleConnectionManager.createNewConnection(message); } @@ -583,19 +627,18 @@ public class XmppConnectionService extends Service { } } else { if (message.getEncryption() == Message.ENCRYPTION_OTR) { - if (!conv.hasValidOtrSession() - && (message.getPresence() != null)) { - conv.startOtrSession(this, message.getPresence(), true); + if (!conv.hasValidOtrSession() && (message.getCounterpart() != null)) { + conv.startOtrSession(message.getCounterpart().getResourcepart(), 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) { - conv.startOtrIfNeeded(); + } else if (conv.hasValidOtrSession()) { + if (conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) { + packet = mMessageGenerator.generateOtrChat(message); + send = true; + } else { + message.setStatus(Message.STATUS_WAITING); + conv.startOtrIfNeeded(); + } + } else { message.setStatus(Message.STATUS_WAITING); } } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { @@ -613,7 +656,7 @@ public class XmppConnectionService extends Service { 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) { @@ -627,13 +670,10 @@ public class XmppConnectionService extends Service { 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); - } + if (!conv.hasValidOtrSession() + && message.getCounterpart() != null) { + conv.startOtrSession(message.getCounterpart().getResourcepart(), false); + } } } @@ -643,7 +683,7 @@ public class XmppConnectionService extends Service { if (message.getEncryption() == Message.ENCRYPTION_NONE || saveEncryptedMessages()) { databaseBackend.createMessage(message); - } + } } if ((send) && (packet != null)) { sendMessagePacket(account, packet); @@ -665,29 +705,32 @@ public class XmppConnectionService extends Service { MessagePacket packet = null; if (message.getEncryption() == Message.ENCRYPTION_OTR) { Presences presences = message.getConversation().getContact() - .getPresences(); + .getPresences(); if (!message.getConversation().hasValidOtrSession()) { - if ((message.getPresence() != null) - && (presences.has(message.getPresence()))) { - message.getConversation().startOtrSession(this, - message.getPresence(), true); + if ((message.getCounterpart() != null) + && (presences.has(message.getCounterpart().getResourcepart()))) { + message.getConversation().startOtrSession(message.getCounterpart().getResourcepart(), true); } else { if (presences.size() == 1) { String presence = presences.asStringArray()[0]; - message.getConversation().startOtrSession(this, - presence, true); + message.getConversation().startOtrSession(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); + try { + message.setCounterpart(Jid.fromSessionID(message.getConversation().getOtrSession().getSessionID())); + if (message.getType() == Message.TYPE_TEXT) { + packet = mMessageGenerator.generateOtrChat(message, + true); + } else if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) { + mJingleConnectionManager.createNewConnection(message); + } + } catch (InvalidJidException e) { + } - } + } } } else if (message.getType() == Message.TYPE_TEXT) { if (message.getEncryption() == Message.ENCRYPTION_NONE) { @@ -695,18 +738,22 @@ public class XmppConnectionService extends Service { } 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()))) { + } + } else if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) { + Contact contact = message.getConversation().getContact(); + Presences presences = contact.getPresences(); + if ((message.getCounterpart() != null) + && (presences.has(message.getCounterpart().getResourcepart()))) { markMessage(message, Message.STATUS_OFFERED); mJingleConnectionManager.createNewConnection(message); } else { if (presences.size() == 1) { String presence = presences.asStringArray()[0]; - message.setPresence(presence); + try { + message.setCounterpart(Jid.fromParts(contact.getJid().getLocalpart(), contact.getJid().getDomainpart(), presence)); + } catch (InvalidJidException e) { + return; + } markMessage(message, Message.STATUS_OFFERED); mJingleConnectionManager.createNewConnection(message); } @@ -726,10 +773,10 @@ public class XmppConnectionService extends Service { public void fetchRosterFromServer(Account account) { IqPacket iqPacket = new IqPacket(IqPacket.TYPE_GET); if (!"".equals(account.getRosterVersion())) { - Log.d(Config.LOGTAG, account.getJid() + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": fetching roster version " + account.getRosterVersion()); } else { - Log.d(Config.LOGTAG, account.getJid() + ": fetching roster"); + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": fetching roster"); } iqPacket.query("jabber:iq:roster").setAttribute("ver", account.getRosterVersion()); @@ -757,7 +804,7 @@ public class XmppConnectionService extends Service { @Override public void onIqPacketReceived(Account account, IqPacket packet) { Element query = packet.query(); - List<Bookmark> bookmarks = new CopyOnWriteArrayList<Bookmark>(); + List<Bookmark> bookmarks = new CopyOnWriteArrayList<>(); Element storage = query.findChild("storage", "storage:bookmarks"); if (storage != null) { @@ -806,13 +853,19 @@ public class XmppConnectionService extends Service { } for (Bundle phoneContact : phoneContacts) { for (Account account : accounts) { - String jid = phoneContact.getString("jid"); - Contact contact = account.getRoster() - .getContact(jid); + Jid jid; + try { + jid = Jid.fromString(phoneContact.getString("jid")); + } catch (final InvalidJidException e) { + // TODO: Warn if contact import fails here? + break; + } + final Contact contact = account.getRoster() + .getContact(jid); String systemAccount = phoneContact - .getInt("phoneid") - + "#" - + phoneContact.getString("lookup"); + .getInt("phoneid") + + "#" + + phoneContact.getString("lookup"); contact.setSystemAccount(systemAccount); contact.setPhotoUri(phoneContact .getString("photouri")); @@ -827,12 +880,12 @@ public class XmppConnectionService extends Service { public List<Conversation> getConversations() { if (this.conversations == null) { - Hashtable<String, Account> accountLookupTable = new Hashtable<String, Account>(); + Hashtable<String, Account> accountLookupTable = new Hashtable<>(); for (Account account : this.accounts) { accountLookupTable.put(account.getUuid(), account); } this.conversations = databaseBackend - .getConversations(Conversation.STATUS_AVAILABLE); + .getConversations(Conversation.STATUS_AVAILABLE); for (Conversation conv : this.conversations) { Account account = accountLookupTable.get(conv.getAccountUuid()); conv.setAccount(account); @@ -845,12 +898,12 @@ public class XmppConnectionService extends Service { private void checkDeletedFiles(Conversation conversation) { for (Message message : conversation.getMessages()) { - if (message.getType() == Message.TYPE_IMAGE + if ((message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) && message.getEncryption() != Message.ENCRYPTION_PGP) { if (!getFileBackend().isFileAvailable(message)) { - message.setDownloadable(new DeletedDownloadable()); + message.setDownloadable(new DownloadablePlaceholder(Downloadable.STATUS_DELETED)); } - } + } } } @@ -861,11 +914,11 @@ public class XmppConnectionService extends Service { && message.getEncryption() != Message.ENCRYPTION_PGP && message.getUuid().equals(uuid)) { if (!getFileBackend().isFileAvailable(message)) { - message.setDownloadable(new DeletedDownloadable()); + message.setDownloadable(new DownloadablePlaceholder(Downloadable.STATUS_DELETED)); updateConversationUi(); } return; - } + } } } } @@ -925,20 +978,20 @@ public class XmppConnectionService extends Service { return null; } - public Conversation find(List<Conversation> haystack, Account account, - String jid) { + public Conversation find(final List<Conversation> haystack, + final Account account, + final Jid jid) { for (Conversation conversation : haystack) { if ((account == null || conversation.getAccount() == account) - && (conversation.getContactJid().split("/", 2)[0] - .equalsIgnoreCase(jid))) { + && (conversation.getContactJid().toBareJid().equals(jid.toBareJid()))) { return conversation; - } + } } return null; } - public Conversation findOrCreateConversation(Account account, String jid, - boolean muc) { + public Conversation findOrCreateConversation(final Account account, final Jid jid, + final boolean muc) { Conversation conversation = find(account, jid); if (conversation != null) { return conversation; @@ -953,7 +1006,7 @@ public class XmppConnectionService extends Service { conversation.setMode(Conversation.MODE_SINGLE); } conversation.setMessages(databaseBackend.getMessages(conversation, - 50)); + 50)); this.databaseBackend.updateConversation(conversation); } else { String conversationName; @@ -961,7 +1014,7 @@ public class XmppConnectionService extends Service { if (contact != null) { conversationName = contact.getDisplayName(); } else { - conversationName = jid.split("@")[0]; + conversationName = jid.getLocalpart(); } if (muc) { conversation = new Conversation(conversationName, account, jid, @@ -979,7 +1032,7 @@ public class XmppConnectionService extends Service { public void archiveConversation(Conversation conversation) { if (conversation.getMode() == Conversation.MODE_MULTI) { - if (conversation.getAccount().getStatus() == Account.STATUS_ONLINE) { + if (conversation.getAccount().getStatus() == Account.State.ONLINE) { Bookmark bookmark = conversation.getBookmark(); if (bookmark != null && bookmark.autojoin()) { bookmark.setAutojoin(false); @@ -1006,6 +1059,7 @@ public class XmppConnectionService extends Service { } public void createAccount(Account account) { + account.initOtrEngine(this); databaseBackend.createAccount(account); this.accounts.add(account); this.reconnectAccount(account, false); @@ -1040,13 +1094,53 @@ public class XmppConnectionService extends Service { UIHelper.showErrorNotification(getApplicationContext(), getAccounts()); } + private void removeStaleListeners() { + boolean removedListener = false; + synchronized (this.convChangedListenerCount) { + if (this.mOnConversationUpdate != null) { + this.mOnConversationUpdate = null; + this.convChangedListenerCount = 0; + this.mNotificationService.setIsInForeground(false); + removedListener = true; + } + } + synchronized (this.accountChangedListenerCount) { + if (this.mOnAccountUpdate != null) { + this.mOnAccountUpdate = null; + this.accountChangedListenerCount = 0; + removedListener = true; + } + } + synchronized (this.rosterChangedListenerCount) { + if (this.mOnRosterUpdate != null) { + this.mOnRosterUpdate = null; + this.rosterChangedListenerCount = 0; + removedListener = true; + } + } + if (removedListener) { + final String msg = "removed stale listeners"; + Log.d(Config.LOGTAG, msg); + checkListeners(); + try { + OutputStream os = openFileOutput("stacktrace.txt", MODE_PRIVATE); + os.write(msg.getBytes()); + os.flush(); + os.close(); + } catch (final FileNotFoundException ignored) { + + } catch (final IOException ignored) { + } + } + } + public void setOnConversationListChangedListener( OnConversationUpdate listener) { - if (!isScreenOn()) { + /*if (!isScreenOn()) { Log.d(Config.LOGTAG, - "ignoring setOnConversationListChangedListener"); + "ignoring setOnConversationListChangedListener"); return; - } + }*/ synchronized (this.convChangedListenerCount) { if (checkListeners()) { switchToForeground(); @@ -1055,7 +1149,7 @@ public class XmppConnectionService extends Service { this.mNotificationService.setIsInForeground(true); this.convChangedListenerCount++; } - } + } public void removeOnConversationListChangedListener() { synchronized (this.convChangedListenerCount) { @@ -1072,10 +1166,10 @@ public class XmppConnectionService extends Service { } public void setOnAccountListChangedListener(OnAccountUpdate listener) { - if (!isScreenOn()) { + /*if (!isScreenOn()) { Log.d(Config.LOGTAG, "ignoring setOnAccountListChangedListener"); return; - } + }*/ synchronized (this.accountChangedListenerCount) { if (checkListeners()) { switchToForeground(); @@ -1099,10 +1193,10 @@ public class XmppConnectionService extends Service { } public void setOnRosterUpdateListener(OnRosterUpdate listener) { - if (!isScreenOn()) { + /*if (!isScreenOn()) { Log.d(Config.LOGTAG, "ignoring setOnRosterUpdateListener"); return; - } + }*/ synchronized (this.rosterChangedListenerCount) { if (checkListeners()) { switchToForeground(); @@ -1132,7 +1226,7 @@ public class XmppConnectionService extends Service { private void switchToForeground() { for (Account account : getAccounts()) { - if (account.getStatus() == Account.STATUS_ONLINE) { + if (account.getStatus() == Account.State.ONLINE) { XmppConnection connection = account.getXmppConnection(); if (connection != null && connection.getFeatures().csi()) { connection.sendActive(); @@ -1144,7 +1238,7 @@ public class XmppConnectionService extends Service { private void switchToBackground() { for (Account account : getAccounts()) { - if (account.getStatus() == Account.STATUS_ONLINE) { + if (account.getStatus() == Account.State.ONLINE) { XmppConnection connection = account.getXmppConnection(); if (connection != null && connection.getFeatures().csi()) { connection.sendInactive(); @@ -1157,18 +1251,17 @@ public class XmppConnectionService extends Service { private boolean isScreenOn() { PowerManager pm = (PowerManager) this - .getSystemService(Context.POWER_SERVICE); + .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); + for (Conversation conversation : conversations) { if ((conversation.getMode() == Conversation.MODE_MULTI) && (conversation.getAccount() == account)) { joinMuc(conversation); - } + } } } @@ -1176,14 +1269,14 @@ public class XmppConnectionService extends Service { Account account = conversation.getAccount(); account.pendingConferenceJoins.remove(conversation); account.pendingConferenceLeaves.remove(conversation); - if (account.getStatus() == Account.STATUS_ONLINE) { + if (account.getStatus() == Account.State.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()); + final Jid joinJid = conversation.getMucOptions().getJoinJid(); + packet.setTo(conversation.getMucOptions().getJoinJid()); Element x = new Element("x"); x.setAttribute("xmlns", "http://jabber.org/protocol/muc"); if (conversation.getMucOptions().getPassword() != null) { @@ -1215,9 +1308,6 @@ public class XmppConnectionService extends Service { } } - private OnRenameListener renameListener = null; - private IqGenerator mIqGenerator = new IqGenerator(this); - public void setOnRenameListener(OnRenameListener listener) { this.renameListener = listener; } @@ -1260,8 +1350,8 @@ public class XmppConnectionService extends Service { }); options.flagAboutToRename(); PresencePacket packet = new PresencePacket(); - packet.setAttribute("to", options.getJoinJid()); - packet.setAttribute("from", conversation.getAccount().getFullJid()); + packet.setTo(options.getJoinJid()); + packet.setFrom(conversation.getAccount().getJid()); String sig = account.getPgpSignature(); if (sig != null) { @@ -1272,7 +1362,7 @@ public class XmppConnectionService extends Service { } else { conversation.setContactJid(options.getJoinJid()); databaseBackend.updateConversation(conversation); - if (conversation.getAccount().getStatus() == Account.STATUS_ONLINE) { + if (conversation.getAccount().getStatus() == Account.State.ONLINE) { Bookmark bookmark = conversation.getBookmark(); if (bookmark != null) { bookmark.setNick(nick); @@ -1287,15 +1377,15 @@ public class XmppConnectionService extends Service { Account account = conversation.getAccount(); account.pendingConferenceJoins.remove(conversation); account.pendingConferenceLeaves.remove(conversation); - if (account.getStatus() == Account.STATUS_ONLINE) { + if (account.getStatus() == Account.State.ONLINE) { PresencePacket packet = new PresencePacket(); - packet.setAttribute("to", conversation.getMucOptions().getJoinJid()); - packet.setAttribute("from", conversation.getAccount().getFullJid()); + packet.setTo(conversation.getMucOptions().getJoinJid()); + packet.setFrom(conversation.getAccount().getJid()); packet.setAttribute("type", "unavailable"); sendPresencePacket(conversation.getAccount(), packet); conversation.getMucOptions().setOffline(); conversation.deregisterWithBookmark(); - Log.d(Config.LOGTAG, conversation.getAccount().getJid() + Log.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid() + ": leaving muc " + conversation.getContactJid()); } else { account.pendingConferenceLeaves.add(conversation); @@ -1303,18 +1393,17 @@ public class XmppConnectionService extends Service { } public void disconnect(Account account, boolean force) { - if ((account.getStatus() == Account.STATUS_ONLINE) - || (account.getStatus() == Account.STATUS_DISABLED)) { + if ((account.getStatus() == Account.State.ONLINE) + || (account.getStatus() == Account.State.DISABLED)) { if (!force) { List<Conversation> conversations = getConversations(); - for (int i = 0; i < conversations.size(); i++) { - Conversation conversation = conversations.get(i); + for (Conversation conversation : conversations) { if (conversation.getAccount() == account) { if (conversation.getMode() == Conversation.MODE_MULTI) { leaveMuc(conversation); } else { if (conversation.endOtrIfNeeded()) { - Log.d(Config.LOGTAG, account.getJid() + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": ended otr session with " + conversation.getContactJid()); } @@ -1323,7 +1412,7 @@ public class XmppConnectionService extends Service { } } account.getXmppConnection().disconnect(force); - } + } } @Override @@ -1362,26 +1451,30 @@ public class XmppConnectionService extends Service { 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); + account.getJid().toBareJid() + " otr session established with " + + conversation.getContactJid() + "/" + + otrSession.getSessionID().getUserID()); + for (Message msg : messages) { if ((msg.getStatus() == Message.STATUS_UNSEND || msg.getStatus() == Message.STATUS_WAITING) && (msg.getEncryption() == Message.ENCRYPTION_OTR)) { - msg.setPresence(otrSession.getSessionID().getUserID()); + SessionID id = otrSession.getSessionID(); + try { + msg.setCounterpart(Jid.fromString(id.getAccountID() + "/" + id.getUserID())); + } catch (InvalidJidException e) { + break; + } if (msg.getType() == Message.TYPE_TEXT) { MessagePacket outPacket = mMessageGenerator - .generateOtrChat(msg, true); + .generateOtrChat(msg, true); if (outPacket != null) { msg.setStatus(Message.STATUS_SEND); databaseBackend.updateMessage(msg); sendMessagePacket(account, outPacket); } - } else if (msg.getType() == Message.TYPE_IMAGE) { + } else if (msg.getType() == Message.TYPE_IMAGE || msg.getType() == Message.TYPE_FILE) { mJingleConnectionManager.createNewConnection(msg); } - } + } } updateConversationUi(); } @@ -1394,15 +1487,15 @@ public class XmppConnectionService extends Service { if (otrSession != null) { MessagePacket packet = new MessagePacket(); packet.setType(MessagePacket.TYPE_CHAT); - packet.setFrom(account.getFullJid()); + packet.setFrom(account.getJid()); packet.addChild("private", "urn:xmpp:carbons:2"); packet.addChild("no-copy", "urn:xmpp:hints"); - packet.setTo(otrSession.getSessionID().getAccountID() + "/" + packet.setAttribute("to", otrSession.getSessionID().getAccountID() + "/" + otrSession.getSessionID().getUserID()); try { packet.setBody(otrSession .transformSending(CryptoHelper.FILETRANSFER - + CryptoHelper.bytesToHex(symmetricKey))); + + CryptoHelper.bytesToHex(symmetricKey))); sendMessagePacket(account, packet); conversation.setSymmetricKey(symmetricKey); return true; @@ -1417,11 +1510,11 @@ public class XmppConnectionService extends Service { contact.resetOption(Contact.Options.DIRTY_DELETE); contact.setOption(Contact.Options.DIRTY_PUSH); Account account = contact.getAccount(); - if (account.getStatus() == Account.STATUS_ONLINE) { + if (account.getStatus() == Account.State.ONLINE) { boolean ask = contact.getOption(Contact.Options.ASKING); boolean sendUpdates = contact - .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST) - && contact.getOption(Contact.Options.PREEMPTIVE_GRANT); + .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); @@ -1441,7 +1534,7 @@ public class XmppConnectionService extends Service { final Bitmap.CompressFormat format = Config.AVATAR_FORMAT; final int size = Config.AVATAR_SIZE; final Avatar avatar = getFileBackend() - .getPepAvatar(image, size, format); + .getPepAvatar(image, size, format); if (avatar != null) { avatar.height = size; avatar.width = size; @@ -1463,7 +1556,7 @@ public class XmppConnectionService extends Service { public void onIqPacketReceived(Account account, IqPacket result) { if (result.getType() == IqPacket.TYPE_RESULT) { IqPacket packet = XmppConnectionService.this.mIqGenerator - .publishAvatarMetadata(avatar); + .publishAvatarMetadata(avatar); sendIqPacket(account, packet, new OnIqPacketReceived() { @Override @@ -1504,13 +1597,13 @@ public class XmppConnectionService extends Service { @Override public void onIqPacketReceived(Account account, IqPacket result) { - final String ERROR = account.getJid() - + ": fetching avatar for " + avatar.owner + " failed "; + final String ERROR = account.getJid().toBareJid() + + ": 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.getJid().toBareJid().equals(avatar.owner)) { if (account.setAvatar(avatar.getFilename())) { databaseBackend.updateAccount(account); } @@ -1519,7 +1612,7 @@ public class XmppConnectionService extends Service { updateAccountUi(); } else { Contact contact = account.getRoster() - .getContact(avatar.owner); + .getContact(avatar.owner); contact.setAvatar(avatar.getFilename()); getAvatarService().clear(contact); updateConversationUi(); @@ -1528,7 +1621,7 @@ public class XmppConnectionService extends Service { if (callback != null) { callback.success(avatar); } - Log.d(Config.LOGTAG, account.getJid() + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": succesfully fetched avatar for " + avatar.owner); return; @@ -1568,7 +1661,7 @@ public class XmppConnectionService extends Service { if (items != null) { Avatar avatar = Avatar.parseMetadata(items); if (avatar != null) { - avatar.owner = account.getJid(); + avatar.owner = account.getJid().toBareJid(); if (fileBackend.isAvatarCached(avatar)) { if (account.setAvatar(avatar.getFilename())) { databaseBackend.updateAccount(account); @@ -1593,10 +1686,10 @@ public class XmppConnectionService extends Service { contact.resetOption(Contact.Options.DIRTY_PUSH); contact.setOption(Contact.Options.DIRTY_DELETE); Account account = contact.getAccount(); - if (account.getStatus() == Account.STATUS_ONLINE) { + if (account.getStatus() == Account.State.ONLINE) { IqPacket iq = new IqPacket(IqPacket.TYPE_SET); Element item = iq.query("jabber:iq:roster").addChild("item"); - item.setAttribute("jid", contact.getJid()); + item.setAttribute("jid", contact.getJid().toString()); item.setAttribute("subscription", "remove"); account.getXmppConnection().sendIqPacket(iq, null); } @@ -1642,14 +1735,14 @@ public class XmppConnectionService extends Service { 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) { + public boolean markMessage(final Account account, final Jid recipient, final String uuid, + final int status) { if (uuid == null) { return false; } else { @@ -1657,7 +1750,7 @@ public class XmppConnectionService extends Service { if (conversation.getContactJid().equals(recipient) && conversation.getAccount().equals(account)) { return markMessage(conversation, uuid, status); - } + } } return false; } @@ -1671,10 +1764,10 @@ public class XmppConnectionService extends Service { for (Message message : conversation.getMessages()) { if (uuid.equals(message.getUuid()) || (message.getStatus() >= Message.STATUS_SEND && uuid - .equals(message.getRemoteMsgId()))) { + .equals(message.getRemoteMsgId()))) { markMessage(message, status); return true; - } + } } return false; } @@ -1683,9 +1776,9 @@ public class XmppConnectionService extends Service { 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)) { + .getStatus() == Message.STATUS_SEND_DISPLAYED)) { return; - } + } message.setStatus(status); databaseBackend.updateMessage(message); updateConversationUi(); @@ -1693,7 +1786,7 @@ public class XmppConnectionService extends Service { public SharedPreferences getPreferences() { return PreferenceManager - .getDefaultSharedPreferences(getApplicationContext()); + .getDefaultSharedPreferences(getApplicationContext()); } public boolean forceEncryption() { @@ -1730,9 +1823,9 @@ public class XmppConnectionService extends Service { } } - public Account findAccountByJid(String accountJid) { + public Account findAccountByJid(final Jid accountJid) { for (Account account : this.accounts) { - if (account.getJid().equals(accountJid)) { + if (account.getJid().toBareJid().equals(accountJid)) { return account; } } @@ -1753,10 +1846,10 @@ public class XmppConnectionService extends Service { String id = conversation.getLatestMarkableMessageId(); conversation.markRead(); if (confirmMessages() && id != null && calledByUi) { - Log.d(Config.LOGTAG, conversation.getAccount().getJid() + Log.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid() + ": sending read marker for " + conversation.getName()); Account account = conversation.getAccount(); - String to = conversation.getContactJid(); + final Jid to = conversation.getContactJid(); this.sendMessagePacket(conversation.getAccount(), mMessageGenerator.confirm(account, to, id)); } @@ -1770,7 +1863,7 @@ public class XmppConnectionService extends Service { if (message.getEncryption() == Message.ENCRYPTION_OTR && message.getStatus() == Message.STATUS_WAITING) { markMessage(message, Message.STATUS_SEND_FAILED); - } + } } } @@ -1791,9 +1884,9 @@ public class XmppConnectionService extends Service { } public void replyWithNotAcceptable(Account account, MessagePacket packet) { - if (account.getStatus() == Account.STATUS_ONLINE) { + if (account.getStatus() == Account.State.ONLINE) { MessagePacket error = this.mMessageGenerator - .generateNotAcceptable(packet); + .generateNotAcceptable(packet); sendMessagePacket(account, error); } } @@ -1810,14 +1903,14 @@ public class XmppConnectionService extends Service { } public List<String> getKnownHosts() { - List<String> hosts = new ArrayList<String>(); + List<String> hosts = new ArrayList<>(); for (Account account : getAccounts()) { - if (!hosts.contains(account.getServer())) { - hosts.add(account.getServer()); + if (!hosts.contains(account.getServer().toString())) { + hosts.add(account.getServer().toString()); } for (Contact contact : account.getRoster().getContacts()) { if (contact.showInRoster()) { - String server = contact.getServer(); + final String server = contact.getServer().toString(); if (server != null && !hosts.contains(server)) { hosts.add(server); } @@ -1828,7 +1921,7 @@ public class XmppConnectionService extends Service { } public List<String> getKnownConferenceHosts() { - ArrayList<String> mucServers = new ArrayList<String>(); + ArrayList<String> mucServers = new ArrayList<>(); for (Account account : accounts) { if (account.getXmppConnection() != null) { String server = account.getXmppConnection().getMucServer(); @@ -1878,20 +1971,8 @@ public class XmppConnectionService extends Service { 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>(); + ArrayList<Contact> contacts = new ArrayList<>(); for (Account account : getAccounts()) { if (!account.isOptionSet(Account.OPTION_DISABLED)) { Contact contact = account.getRoster().getContactFromRoster(jid); @@ -1911,29 +1992,10 @@ public class XmppConnectionService extends Service { 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; - } - - } - public void resendFailedMessages(Message message) { - List<Message> messages = new ArrayList<Message>(); + List<Message> messages = new ArrayList<>(); Message current = message; - while(current.getStatus() == Message.STATUS_SEND_FAILED) { + while (current.getStatus() == Message.STATUS_SEND_FAILED) { messages.add(current); if (current.mergeable(current.next())) { current = current.next(); @@ -1941,9 +2003,27 @@ public class XmppConnectionService extends Service { break; } } - for(Message msg: messages) { + for (Message msg : messages) { markMessage(msg, Message.STATUS_WAITING); this.resendMessage(msg); } } + + public interface OnConversationUpdate { + public void onConversationUpdate(); + } + + public interface OnAccountUpdate { + public void onAccountUpdate(); + } + + public interface OnRosterUpdate { + public void onRosterUpdate(); + } + + public class XmppConnectionBinder extends Binder { + public XmppConnectionService getService() { + return XmppConnectionService.this; + } + } } diff --git a/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java b/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java index f14da352..e7254933 100644 --- a/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java @@ -25,7 +25,7 @@ import eu.siacs.conversations.ui.adapter.ListItemAdapter; public class ChooseContactActivity extends XmppActivity { private ListView mListView; - private ArrayList<ListItem> contacts = new ArrayList<ListItem>(); + private ArrayList<ListItem> contacts = new ArrayList<>(); private ArrayAdapter<ListItem> mContactsAdapter; private EditText mSearchEditText; @@ -96,10 +96,10 @@ public class ChooseContactActivity extends XmppActivity { Intent request = getIntent(); Intent data = new Intent(); ListItem mListItem = contacts.get(position); - data.putExtra("contact", mListItem.getJid()); + data.putExtra("contact", mListItem.getJid().toString()); String account = request.getStringExtra("account"); if (account == null && mListItem instanceof Contact) { - account = ((Contact) mListItem).getAccount().getJid(); + account = ((Contact) mListItem).getAccount().getJid().toBareJid().toString(); } data.putExtra("account", account); data.putExtra("conversation", @@ -130,7 +130,7 @@ public class ChooseContactActivity extends XmppActivity { protected void filterContacts(String needle) { this.contacts.clear(); for (Account account : xmppConnectionService.getAccounts()) { - if (account.getStatus() != Account.STATUS_DISABLED) { + if (account.getStatus() != Account.State.DISABLED) { for (Contact contact : account.getRoster().getContacts()) { if (contact.showInRoster() && contact.match(needle)) { this.contacts.add(contact); diff --git a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java index cc9fca26..4eb081ce 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java @@ -31,7 +31,7 @@ import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; -public class ConferenceDetailsActivity extends XmppActivity { +public class ConferenceDetailsActivity extends XmppActivity implements OnConversationUpdate, OnRenameListener { public static final String ACTION_VIEW_MUC = "view_muc"; private Conversation conversation; private TextView mYourNick; @@ -53,8 +53,28 @@ public class ConferenceDetailsActivity extends XmppActivity { } }; - private List<User> users = new ArrayList<MucOptions.User>(); - private OnConversationUpdate onConvChanged = new OnConversationUpdate() { + @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 List<User> users = new ArrayList<>(); @Override public void onConversationUpdate() { @@ -66,7 +86,6 @@ public class ConferenceDetailsActivity extends XmppActivity { } }); } - }; @Override protected void onCreate(Bundle savedInstanceState) { @@ -142,7 +161,7 @@ public class ConferenceDetailsActivity extends XmppActivity { @Override protected String getShareableUri() { if (conversation!=null) { - return "xmpp:"+conversation.getContactJid().split("/")[0]+"?join"; + return "xmpp:"+conversation.getContactJid().toBareJid().toString()+"?join"; } else { return ""; } @@ -156,7 +175,6 @@ public class ConferenceDetailsActivity extends XmppActivity { @Override void onBackendConnected() { - registerListener(); if (getIntent().getAction().equals(ACTION_VIEW_MUC)) { this.uuid = getIntent().getExtras().getString("uuid"); } @@ -169,49 +187,13 @@ public class ConferenceDetailsActivity extends XmppActivity { } } - @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())); + .getAccount().getJid().toBareJid())); mYourPhoto.setImageBitmap(avatarService().get( conversation.getAccount(), getPixel(48))); setTitle(conversation.getName()); - mFullJid.setText(conversation.getContactJid().split("/", 2)[0]); + mFullJid.setText(conversation.getContactJid().toBareJid().toString()); mYourNick.setText(conversation.getMucOptions().getActualNick()); mRoleAffiliaton = (TextView) findViewById(R.id.muc_role); if (conversation.getMucOptions().online()) { diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index 7ac30e39..788244e3 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -1,9 +1,5 @@ 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; @@ -21,12 +17,17 @@ 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.CompoundButton.OnCheckedChangeListener; import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.QuickContactBadge; import android.widget.TextView; + +import org.openintents.openpgp.util.OpenPgpUtils; + +import java.util.Iterator; + import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.PgpEngine; import eu.siacs.conversations.entities.Account; @@ -35,23 +36,13 @@ 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; +import eu.siacs.conversations.xmpp.jid.InvalidJidException; +import eu.siacs.conversations.xmpp.jid.Jid; -public class ContactDetailsActivity extends XmppActivity { +public class ContactDetailsActivity extends XmppActivity implements OnAccountUpdate, OnRosterUpdate { 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 @@ -61,61 +52,16 @@ public class ContactDetailsActivity extends XmppActivity { 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) { + boolean isChecked) { if (isChecked) { if (contact .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { xmppConnectionService.sendPresencePacket(contact - .getAccount(), + .getAccount(), xmppConnectionService.getPresenceGenerator() .sendPresenceUpdatesTo(contact)); } else { @@ -129,12 +75,11 @@ public class ContactDetailsActivity extends XmppActivity { } } }; - private OnCheckedChangeListener mOnReceiveCheckedChange = new OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, - boolean isChecked) { + boolean isChecked) { if (isChecked) { xmppConnectionService.sendPresencePacket(contact.getAccount(), xmppConnectionService.getPresenceGenerator() @@ -146,25 +91,70 @@ public class ContactDetailsActivity extends XmppActivity { } } }; - - private OnAccountUpdate accountUpdate = new OnAccountUpdate() { + private Jid accountJid; + private Jid 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 addToPhonebook = new DialogInterface.OnClickListener() { @Override - public void onAccountUpdate() { - runOnUiThread(new Runnable() { + public void onClick(DialogInterface dialog, int which) { + Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT); + intent.setType(Contacts.CONTENT_ITEM_TYPE); + intent.putExtra(Intents.Insert.IM_HANDLE, contact.getJid().toString()); + intent.putExtra(Intents.Insert.IM_PROTOCOL, + CommonDataKinds.Im.PROTOCOL_JABBER); + intent.putExtra("finishActivityOnSaveCompleted", true); + ContactDetailsActivity.this.startActivityForResult(intent, 0); + } + }; + private OnClickListener onBadgeClick = new OnClickListener() { - @Override - public void run() { - populateView(); - } - }); + @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; + + @Override + public void onRosterUpdate() { + runOnUiThread(new Runnable() { + + @Override + public void run() { + populateView(); + } + }); + } + + @Override + public void onAccountUpdate() { + runOnUiThread(new Runnable() { + + @Override + public void run() { + populateView(); + } + }); + } @Override protected String getShareableUri() { - if (contact!=null) { - return "xmpp:"+contact.getJid(); + if (contact != null) { + return contact.getShareableUri(); } else { return ""; } @@ -174,8 +164,14 @@ public class ContactDetailsActivity extends XmppActivity { 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"); + try { + this.accountJid = Jid.fromString(getIntent().getExtras().getString("account")); + } catch (final InvalidJidException ignored) { + } + try { + this.contactJid = Jid.fromString(getIntent().getExtras().getString("contact")); + } catch (final InvalidJidException ignored) { + } } setContentView(R.layout.activity_contact_details); @@ -197,39 +193,39 @@ public class ContactDetailsActivity extends XmppActivity { 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; + 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); } @@ -270,7 +266,7 @@ public class ContactDetailsActivity extends XmppActivity { receive.setChecked(false); } } - if (contact.getAccount().getStatus() == Account.STATUS_ONLINE) { + if (contact.getAccount().getStatus() == Account.State.ONLINE) { receive.setEnabled(true); send.setEnabled(true); } else { @@ -285,43 +281,43 @@ public class ContactDetailsActivity extends XmppActivity { 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; + 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()); + contactJidTv.setText(contact.getJid().toString()); } accountJidTv.setText(getString(R.string.using_account, contact - .getAccount().getJid())); + .getAccount().getJid().toBareJid())); prepareContactBadge(badge, contact); if (contact.getSystemAccount() == null) { badge.setOnClickListener(onBadgeClick); @@ -330,10 +326,8 @@ public class ContactDetailsActivity extends XmppActivity { keys.removeAllViews(); boolean hasKeys = false; LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); - for (Iterator<String> iterator = contact.getOtrFingerprints() - .iterator(); iterator.hasNext();) { + for(final String otrFingerprint : contact.getOtrFingerprints()) { hasKeys = true; - final String otrFingerprint = iterator.next(); 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); @@ -419,9 +413,6 @@ public class ContactDetailsActivity extends XmppActivity { @Override public void onBackendConnected() { - xmppConnectionService.setOnRosterUpdateListener(this.rosterUpdate); - xmppConnectionService - .setOnAccountListChangedListener(this.accountUpdate); if ((accountJid != null) && (contactJid != null)) { Account account = xmppConnectionService .findAccountByJid(accountJid); @@ -432,12 +423,4 @@ public class ContactDetailsActivity extends XmppActivity { populateView(); } } - - @Override - protected void onStop() { - super.onStop(); - xmppConnectionService.removeOnRosterUpdateListener(); - xmppConnectionService.removeOnAccountListChangedListener(); - } - } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java index d02a7c7b..5dccde18 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java @@ -52,13 +52,14 @@ public class ConversationActivity extends XmppActivity implements public static final int REQUEST_SEND_MESSAGE = 0x0201; public static final int REQUEST_DECRYPT_PGP = 0x0202; public static final int REQUEST_ENCRYPT_MESSAGE = 0x0207; - private static final int REQUEST_ATTACH_FILE_DIALOG = 0x0203; + private static final int REQUEST_ATTACH_IMAGE_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; + private static final int REQUEST_ATTACH_FILE_DIALOG = 0x0208; 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 int ATTACHMENT_CHOICE_CHOOSE_FILE = 0x0303; private static final String STATE_OPEN_CONVERSATION = "state_open_conversation"; private static final String STATE_PANEL_OPEN = "state_panel_open"; private static final String STATE_PENDING_URI = "state_pending_uri"; @@ -66,17 +67,18 @@ public class ConversationActivity extends XmppActivity implements private String mOpenConverstaion = null; private boolean mPanelOpen = true; private Uri mPendingImageUri = null; + private Uri mPendingFileUri = null; private View mContentView; - private List<Conversation> conversationList = new ArrayList<Conversation>(); + private List<Conversation> conversationList = new ArrayList<>(); private Conversation selectedConversation = null; private ListView listView; private ConversationFragment mConversationFragment; private ArrayAdapter<Conversation> listAdapter; - private Toast prepareImageToast; + private Toast prepareFileToast; public List<Conversation> getConversationList() { @@ -106,7 +108,7 @@ public class ConversationActivity extends XmppActivity implements protected String getShareableUri() { Conversation conversation = getSelectedConversation(); if (conversation != null) { - return "xmpp:" + conversation.getAccount().getJid(); + return conversation.getAccount().getShareableUri(); } else { return ""; } @@ -160,8 +162,10 @@ public class ConversationActivity extends XmppActivity implements this.listAdapter = new ConversationAdapter(this, conversationList); listView.setAdapter(this.listAdapter); - getActionBar().setDisplayHomeAsUpEnabled(false); - getActionBar().setHomeButtonEnabled(false); + if (getActionBar() != null) { + getActionBar().setDisplayHomeAsUpEnabled(false); + getActionBar().setHomeButtonEnabled(false); + } listView.setOnItemClickListener(new OnItemClickListener() { @@ -228,8 +232,7 @@ public class ConversationActivity extends XmppActivity implements .useSubjectToIdentifyConference()) { ab.setTitle(getSelectedConversation().getName()); } else { - ab.setTitle(getSelectedConversation().getContactJid() - .split("/")[0]); + ab.setTitle(getSelectedConversation().getContactJid().toBareJid().toString()); } } invalidateOptionsMenu(); @@ -307,11 +310,16 @@ public class ConversationActivity extends XmppActivity implements attachFileIntent.setAction(Intent.ACTION_GET_CONTENT); Intent chooser = Intent.createChooser(attachFileIntent, getString(R.string.attach_file)); + startActivityForResult(chooser, REQUEST_ATTACH_IMAGE_DIALOG); + } else if (attachmentChoice == ATTACHMENT_CHOICE_CHOOSE_FILE) { + Intent attachFileIntent = new Intent(); + //attachFileIntent.setType("file/*"); + attachFileIntent.setType("*/*"); + attachFileIntent.addCategory(Intent.CATEGORY_OPENABLE); + 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); } } }); @@ -482,7 +490,7 @@ public class ConversationActivity extends XmppActivity implements attachFile(ATTACHMENT_CHOICE_TAKE_PHOTO); break; case R.id.attach_record_voice: - attachFile(ATTACHMENT_CHOICE_RECORD_VOICE); + attachFile(ATTACHMENT_CHOICE_CHOOSE_FILE); break; } return false; @@ -600,7 +608,7 @@ public class ConversationActivity extends XmppActivity implements } @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { + public boolean onKeyDown(final int keyCode, final KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK) { if (!isConversationsOverviewVisable()) { showConversationsOverview(); @@ -611,7 +619,7 @@ public class ConversationActivity extends XmppActivity implements } @Override - protected void onNewIntent(Intent intent) { + protected void onNewIntent(final Intent intent) { if (xmppConnectionServiceBound) { if (intent != null && VIEW_CONVERSATION.equals(intent.getType())) { handleViewConversationIntent(intent); @@ -633,19 +641,7 @@ public class ConversationActivity extends XmppActivity implements } @Override - protected void onStop() { - if (xmppConnectionServiceBound) { - xmppConnectionService.removeOnConversationListChangedListener(); - xmppConnectionService.removeOnAccountListChangedListener(); - xmppConnectionService.removeOnRosterUpdateListener(); - xmppConnectionService.getNotificationService().setOpenConversation( - null); - } - super.onStop(); - } - - @Override - public void onSaveInstanceState(Bundle savedInstanceState) { + public void onSaveInstanceState(final Bundle savedInstanceState) { Conversation conversation = getSelectedConversation(); if (conversation != null) { savedInstanceState.putString(STATE_OPEN_CONVERSATION, @@ -661,9 +657,7 @@ public class ConversationActivity extends XmppActivity implements @Override void onBackendConnected() { - this.registerListener(); updateConversationList(); - if (xmppConnectionService.getAccounts().size() == 0) { startActivity(new Intent(this, EditAccountActivity.class)); } else if (conversationList.size() <= 0) { @@ -688,14 +682,17 @@ public class ConversationActivity extends XmppActivity implements } else { showConversationsOverview(); mPendingImageUri = null; + mPendingFileUri = null; setSelectedConversation(conversationList.get(0)); this.mConversationFragment.reInit(getSelectedConversation()); } if (mPendingImageUri != null) { - attachImageToConversation(getSelectedConversation(), - mPendingImageUri); + attachImageToConversation(getSelectedConversation(),mPendingImageUri); mPendingImageUri = null; + } else if (mPendingFileUri != null) { + attachFileToConversation(getSelectedConversation(),mPendingFileUri); + mPendingFileUri = null; } ExceptionHelper.checkForCrash(this, this.xmppConnectionService); setIntent(new Intent()); @@ -714,17 +711,17 @@ public class ConversationActivity extends XmppActivity implements } private void selectConversationByUuid(String uuid) { - for (int i = 0; i < conversationList.size(); ++i) { - if (conversationList.get(i).getUuid().equals(uuid)) { - setSelectedConversation(conversationList.get(i)); - } - } + for (Conversation aConversationList : conversationList) { + if (aConversationList.getUuid().equals(uuid)) { + setSelectedConversation(aConversationList); + } + } } - public void registerListener() { - xmppConnectionService.setOnConversationListChangedListener(this); - xmppConnectionService.setOnAccountListChangedListener(this); - xmppConnectionService.setOnRosterUpdateListener(this); + @Override + protected void unregisterListeners() { + super.unregisterListeners(); + xmppConnectionService.getNotificationService().setOpenConversation(null); } @Override @@ -739,13 +736,20 @@ public class ConversationActivity extends XmppActivity implements selectedFragment.hideSnackbar(); selectedFragment.updateMessages(); } - } else if (requestCode == REQUEST_ATTACH_FILE_DIALOG) { + } else if (requestCode == REQUEST_ATTACH_IMAGE_DIALOG) { mPendingImageUri = data.getData(); if (xmppConnectionServiceBound) { attachImageToConversation(getSelectedConversation(), mPendingImageUri); mPendingImageUri = null; } + } else if (requestCode == REQUEST_ATTACH_FILE_DIALOG) { + mPendingFileUri = data.getData(); + if (xmppConnectionServiceBound) { + attachFileToConversation(getSelectedConversation(), + mPendingFileUri); + mPendingFileUri = null; + } } else if (requestCode == REQUEST_SEND_PGP_IMAGE) { } else if (requestCode == ATTACHMENT_CHOICE_CHOOSE_IMAGE) { @@ -767,9 +771,6 @@ public class ConversationActivity extends XmppActivity implements Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); intent.setData(mPendingImageUri); sendBroadcast(intent); - } else if (requestCode == REQUEST_RECORD_AUDIO) { - attachAudioToConversation(getSelectedConversation(), - data.getData()); } } else { if (requestCode == REQUEST_IMAGE_CAPTURE) { @@ -778,21 +779,40 @@ public class ConversationActivity extends XmppActivity implements } } - private void attachAudioToConversation(Conversation conversation, Uri uri) { + private void attachFileToConversation(Conversation conversation, Uri uri) { + prepareFileToast = Toast.makeText(getApplicationContext(), + getText(R.string.preparing_file), Toast.LENGTH_LONG); + prepareFileToast.show(); + xmppConnectionService.attachFileToConversation(conversation,uri, new UiCallback<Message>() { + @Override + public void success(Message message) { + hidePrepareFileToast(); + xmppConnectionService.sendMessage(message); + } + + @Override + public void error(int errorCode, Message message) { + displayErrorDialog(errorCode); + } + + @Override + public void userInputRequried(PendingIntent pi, Message message) { + } + }); } private void attachImageToConversation(Conversation conversation, Uri uri) { - prepareImageToast = Toast.makeText(getApplicationContext(), + prepareFileToast = Toast.makeText(getApplicationContext(), getText(R.string.preparing_image), Toast.LENGTH_LONG); - prepareImageToast.show(); + prepareFileToast.show(); xmppConnectionService.attachImageToConversation(conversation, uri, new UiCallback<Message>() { @Override public void userInputRequried(PendingIntent pi, Message object) { - hidePrepareImageToast(); + hidePrepareFileToast(); ConversationActivity.this.runIntent(pi, ConversationActivity.REQUEST_SEND_PGP_IMAGE); } @@ -804,19 +824,19 @@ public class ConversationActivity extends XmppActivity implements @Override public void error(int error, Message message) { - hidePrepareImageToast(); + hidePrepareFileToast(); displayErrorDialog(error); } }); } - private void hidePrepareImageToast() { - if (prepareImageToast != null) { + private void hidePrepareFileToast() { + if (prepareFileToast != null) { runOnUiThread(new Runnable() { @Override public void run() { - prepareImageToast.cancel(); + prepareFileToast.cancel(); } }); } @@ -832,7 +852,7 @@ public class ConversationActivity extends XmppActivity implements try { this.startIntentSenderForResult(pi.getIntentSender(), requestCode, null, 0, 0, 0); - } catch (SendIntentException e1) { + } catch (final SendIntentException ignored) { } } @@ -895,12 +915,11 @@ public class ConversationActivity extends XmppActivity implements public void run() { updateConversationList(); if (conversationList.size() == 0) { - startActivity(new Intent(getApplicationContext(), - StartConversationActivity.class)); - finish(); - } else { - ConversationActivity.this.mConversationFragment.updateMessages(); + startActivity(new Intent(getApplicationContext(), + StartConversationActivity.class)); + finish(); } + ConversationActivity.this.mConversationFragment.updateMessages(); } }); } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 8754b953..0742e17e 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -35,7 +35,6 @@ import net.java.otr4j.session.SessionStatus; import java.util.ArrayList; import java.util.List; -import java.util.Set; import java.util.concurrent.ConcurrentLinkedQueue; import eu.siacs.conversations.R; @@ -43,6 +42,9 @@ 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.Downloadable; +import eu.siacs.conversations.entities.DownloadableFile; +import eu.siacs.conversations.entities.DownloadablePlaceholder; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.Presences; @@ -53,7 +55,7 @@ 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 eu.siacs.conversations.xmpp.jid.Jid; public class ConversationFragment extends Fragment { @@ -92,11 +94,9 @@ public class ConversationFragment extends Fragment { } }; protected ListView messagesView; - protected LayoutInflater inflater; - protected List<Message> messageList = new ArrayList<Message>(); + protected List<Message> messageList = new ArrayList<>(); protected MessageAdapter messageListAdapter; protected Contact contact; - protected String queuedPqpMessage = null; private EditMessage mEditMessage; private ImageButton mSendButton; private RelativeLayout snackbar; @@ -147,7 +147,20 @@ public class ConversationFragment extends Fragment { } } }; - private ConcurrentLinkedQueue<Message> mEncryptedMessages = new ConcurrentLinkedQueue<Message>(); + protected OnClickListener clickToVerify = new OnClickListener() { + + @Override + public void onClick(View v) { + if (conversation.getOtrFingerprint() != null) { + Intent intent = new Intent(getActivity(), VerifyOTRActivity.class); + intent.setAction(VerifyOTRActivity.ACTION_VERIFY_CONTACT); + intent.putExtra("contact", conversation.getContact().getJid().toBareJid().toString()); + intent.putExtra("account", conversation.getAccount().getJid().toBareJid().toString()); + startActivity(intent); + } + } + }; + private ConcurrentLinkedQueue<Message> mEncryptedMessages = new ConcurrentLinkedQueue<>(); private boolean mDecryptJobRunning = false; private OnEditorActionListener mEditorActionListener = new OnEditorActionListener() { @@ -191,7 +204,7 @@ public class ConversationFragment extends Fragment { } if (mEditMessage.getText().length() < 1) { if (this.conversation.getMode() == Conversation.MODE_MULTI) { - conversation.setNextPresence(null); + conversation.setNextCounterpart(null); updateChatMsgHint(); } return; @@ -200,10 +213,10 @@ public class ConversationFragment extends Fragment { .toString(), conversation.getNextEncryption(activity .forceEncryption())); if (conversation.getMode() == Conversation.MODE_MULTI) { - if (conversation.getNextPresence() != null) { - message.setPresence(conversation.getNextPresence()); + if (conversation.getNextCounterpart() != null) { + message.setCounterpart(conversation.getNextCounterpart()); message.setType(Message.TYPE_PRIVATE); - conversation.setNextPresence(null); + conversation.setNextCounterpart(null); } } if (conversation.getNextEncryption(activity.forceEncryption()) == Message.ENCRYPTION_OTR) { @@ -217,10 +230,10 @@ public class ConversationFragment extends Fragment { public void updateChatMsgHint() { if (conversation.getMode() == Conversation.MODE_MULTI - && conversation.getNextPresence() != null) { + && conversation.getNextCounterpart() != null) { this.mEditMessage.setHint(getString( R.string.send_private_message_to, - conversation.getNextPresence())); + conversation.getNextCounterpart().getResourcepart())); } else { switch (conversation.getNextEncryption(activity.forceEncryption())) { case Message.ENCRYPTION_NONE: @@ -280,11 +293,12 @@ public class ConversationFragment extends Fragment { 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()); + if (message.getCounterpart() != null) { + if (!message.getCounterpart().isBareJid()) { + highlightInConference(message.getCounterpart().getResourcepart()); + } else { + highlightInConference(message.getCounterpart().toString()); + } } } else { Contact contact = message.getConversation() @@ -296,10 +310,10 @@ public class ConversationFragment extends Fragment { .getConversation()); } } - } else { + } else { Account account = message.getConversation().getAccount(); Intent intent = new Intent(activity, EditAccountActivity.class); - intent.putExtra("jid", account.getJid()); + intent.putExtra("jid", account.getJid().toBareJid().toString()); startActivity(intent); } } @@ -311,9 +325,7 @@ public class ConversationFragment extends Fragment { 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 { + if (message.getCounterpart() != null) { privateMessageWith(message.getCounterpart()); } } @@ -345,6 +357,7 @@ public class ConversationFragment extends Fragment { MenuItem sendAgain = menu.findItem(R.id.send_again); MenuItem copyUrl = menu.findItem(R.id.copy_url); MenuItem downloadImage = menu.findItem(R.id.download_image); + MenuItem cancelTransmission = menu.findItem(R.id.cancel_transmission); if (this.selectedMessage.getType() != Message.TYPE_TEXT || this.selectedMessage.getDownloadable() != null) { copyText.setVisible(false); @@ -361,12 +374,15 @@ public class ConversationFragment extends Fragment { || this.selectedMessage.getImageParams().url == null) { copyUrl.setVisible(false); } - if (this.selectedMessage.getType() != Message.TYPE_TEXT || this.selectedMessage.getDownloadable() != null || !this.selectedMessage.bodyContainsDownloadable()) { downloadImage.setVisible(false); } + if (this.selectedMessage.getDownloadable() == null + || this.selectedMessage.getDownloadable() instanceof DownloadablePlaceholder) { + cancelTransmission.setVisible(false); + } } } @@ -388,6 +404,9 @@ public class ConversationFragment extends Fragment { case R.id.download_image: downloadImage(selectedMessage); return true; + case R.id.cancel_transmission: + cancelTransmission(selectedMessage); + return true; default: return super.onContextItemSelected(item); } @@ -414,6 +433,14 @@ public class ConversationFragment extends Fragment { } private void resendMessage(Message message) { + if (message.getType() == Message.TYPE_FILE || message.getType() == Message.TYPE_IMAGE) { + DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message); + if (!file.exists()) { + Toast.makeText(activity,R.string.file_deleted,Toast.LENGTH_SHORT).show(); + message.setDownloadable(new DownloadablePlaceholder(Downloadable.STATUS_DELETED)); + return; + } + } activity.xmppConnectionService.resendFailedMessages(message); } @@ -430,9 +457,16 @@ public class ConversationFragment extends Fragment { .createNewConnection(message); } - protected void privateMessageWith(String counterpart) { + private void cancelTransmission(Message message) { + Downloadable downloadable = message.getDownloadable(); + if (downloadable!=null) { + downloadable.cancel(); + } + } + + protected void privateMessageWith(final Jid counterpart) { this.mEditMessage.setText(""); - this.conversation.setNextPresence(counterpart); + this.conversation.setNextCounterpart(counterpart); updateChatMsgHint(); } @@ -466,7 +500,7 @@ public class ConversationFragment extends Fragment { this.activity = (ConversationActivity) getActivity(); this.conversation = conversation; if (this.conversation.getMode() == Conversation.MODE_MULTI) { - this.conversation.setNextPresence(null); + this.conversation.setNextCounterpart(null); } this.mEditMessage.setText(""); this.mEditMessage.append(this.conversation.getNextMessage()); @@ -507,6 +541,39 @@ public class ConversationFragment extends Fragment { activity.switchToContactDetails(contact); } }); + } else if (conversation.getMode() == Conversation.MODE_SINGLE) { + makeFingerprintWarning(); + } else if (!conversation.getMucOptions().online() + && conversation.getAccount().getStatus() == Account.State.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; + } } for (Message message : this.conversation.getMessages()) { if (message.getEncryption() == Message.ENCRYPTION_PGP @@ -528,44 +595,6 @@ public class ConversationFragment extends Fragment { updateStatusMessages(); } this.messageListAdapter.notifyDataSetChanged(); - if (conversation.getMode() == Conversation.MODE_SINGLE) { - if (messageList.size() >= 1) { - makeFingerprintWarning(); - } - } 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; - } - } - } updateChatMsgHint(); if (!activity.isConversationsOverviewVisable() || !activity.isConversationsOverviewHideable()) { activity.xmppConnectionService.markRead(conversation, true); @@ -619,7 +648,7 @@ public class ConversationFragment extends Fragment { public void updateSendButton() { Conversation c = this.conversation; if (activity.useSendButtonToIndicateStatus() && c != null - && c.getAccount().getStatus() == Account.STATUS_ONLINE) { + && c.getAccount().getStatus() == Account.State.ONLINE) { if (c.getMode() == Conversation.MODE_SINGLE) { switch (c.getContact().getMostAvailableStatus()) { case Presences.CHAT: @@ -682,26 +711,11 @@ public class ConversationFragment extends Fragment { } protected void makeFingerprintWarning() { - Set<String> knownFingerprints = conversation.getContact() - .getOtrFingerprints(); - if (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(); - } - } - }); + if (conversation.smpRequested()) { + showSnackbar(R.string.smp_requested, R.string.verify, clickToVerify); + } else if (conversation.hasValidOtrSession() && (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) + && (!conversation.isOtrFingerprintVerified())) { + showSnackbar(R.string.unknown_otr_fingerprint, R.string.verify, clickToVerify); } } @@ -827,21 +841,16 @@ public class ConversationFragment extends Fragment { 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(); - } - }); - } + activity.selectPresence(message.getConversation(), + new OnPresenceSelected() { + + @Override + public void onPresenceSelected() { + message.setCounterpart(conversation.getNextCounterpart()); + xmppService.sendMessage(message); + messageSent(); + } + }); } public void appendText(String text) { diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index 29a6395d..22507483 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -1,15 +1,10 @@ package eu.siacs.conversations.ui; -import android.app.AlertDialog; import android.app.PendingIntent; import android.content.Intent; -import android.graphics.Bitmap; -import android.graphics.Color; -import android.graphics.Point; import android.os.Bundle; import android.text.Editable; import android.text.TextWatcher; -import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; @@ -27,29 +22,19 @@ import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; -import com.google.zxing.BarcodeFormat; -import com.google.zxing.EncodeHintType; -import com.google.zxing.WriterException; -import com.google.zxing.common.BitMatrix; -import com.google.zxing.integration.android.IntentIntegrator; -import com.google.zxing.integration.android.IntentResult; -import com.google.zxing.qrcode.QRCodeWriter; -import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; - -import java.util.Hashtable; - -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.services.XmppConnectionService.OnAccountUpdate; import eu.siacs.conversations.ui.adapter.KnownHostsAdapter; +import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.utils.Validator; import eu.siacs.conversations.xmpp.XmppConnection.Features; +import eu.siacs.conversations.xmpp.jid.InvalidJidException; +import eu.siacs.conversations.xmpp.jid.Jid; import eu.siacs.conversations.xmpp.pep.Avatar; -public class EditAccountActivity extends XmppActivity { +public class EditAccountActivity extends XmppActivity implements OnAccountUpdate { private AutoCompleteTextView mAccountJid; private EditText mPassword; @@ -68,7 +53,7 @@ public class EditAccountActivity extends XmppActivity { private RelativeLayout mOtrFingerprintBox; private ImageButton mOtrFingerprintToClipboardButton; - private String jidToEdit; + private Jid jidToEdit; private Account mAccount; private boolean mFetchingAvatar = false; @@ -78,7 +63,7 @@ public class EditAccountActivity extends XmppActivity { @Override public void onClick(View v) { if (mAccount != null - && mAccount.getStatus() == Account.STATUS_DISABLED) { + && mAccount.getStatus() == Account.State.DISABLED) { mAccount.setOption(Account.OPTION_DISABLED, false); xmppConnectionService.updateAccount(mAccount); return; @@ -89,15 +74,14 @@ public class EditAccountActivity extends XmppActivity { 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(); + final Jid jid; + try { + jid = Jid.fromString(mAccountJid.getText().toString()); + } catch (final InvalidJidException e) { + // TODO: Handle this error? + return; + } + String password = mPassword.getText().toString(); String passwordConfirm = mPasswordConfirm.getText().toString(); if (registerNewAccount) { if (!password.equals(passwordConfirm)) { @@ -109,19 +93,25 @@ public class EditAccountActivity extends XmppActivity { } if (mAccount != null) { mAccount.setPassword(password); - mAccount.setUsername(username); - mAccount.setServer(server); + try { + mAccount.setUsername(jid.hasLocalpart() ? jid.getLocalpart() : ""); + mAccount.setServer(jid.getDomainpart()); + } catch (final InvalidJidException ignored) { + } 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); + try { + if (xmppConnectionService.findAccountByJid(Jid.fromString(mAccountJid.getText().toString())) != null) { + mAccountJid + .setError(getString(R.string.account_already_exists)); + mAccountJid.requestFocus(); + return; + } + } catch (InvalidJidException e) { + return; + } + mAccount = new Account(jid.toBareJid(), password); mAccount.setOption(Account.OPTION_USETLS, true); mAccount.setOption(Account.OPTION_USECOMPRESSION, true); mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount); @@ -143,8 +133,6 @@ public class EditAccountActivity extends XmppActivity { finish(); } }; - private OnAccountUpdate mOnAccountUpdateListener = new OnAccountUpdate() { - @Override public void onAccountUpdate() { runOnUiThread(new Runnable() { @@ -152,13 +140,13 @@ public class EditAccountActivity extends XmppActivity { @Override public void run() { if (mAccount != null - && mAccount.getStatus() != Account.STATUS_ONLINE + && mAccount.getStatus() != Account.State.ONLINE && mFetchingAvatar) { startActivity(new Intent(getApplicationContext(), ManageAccountActivity.class)); finish(); } else if (jidToEdit == null && mAccount != null - && mAccount.getStatus() == Account.STATUS_ONLINE) { + && mAccount.getStatus() == Account.State.ONLINE) { if (!mFetchingAvatar) { mFetchingAvatar = true; xmppConnectionService.checkForAvatar(mAccount, @@ -173,7 +161,6 @@ public class EditAccountActivity extends XmppActivity { } }); } - }; private UiCallback<Avatar> mAvatarFetchCallback = new UiCallback<Avatar>() { @Override @@ -191,8 +178,7 @@ public class EditAccountActivity extends XmppActivity { finishInitialSetup(avatar); } }; - private KnownHostsAdapter mKnownHostsAdapter; - private TextWatcher mTextWatcher = new TextWatcher() { + private TextWatcher mTextWatcher = new TextWatcher() { @Override public void onTextChanged(CharSequence s, int start, int before, @@ -217,7 +203,7 @@ public class EditAccountActivity extends XmppActivity { if (mAccount!=null) { Intent intent = new Intent(getApplicationContext(), PublishProfilePictureActivity.class); - intent.putExtra("account", mAccount.getJid()); + intent.putExtra("account", mAccount.getJid().toBareJid().toString()); startActivity(intent); } } @@ -235,7 +221,7 @@ public class EditAccountActivity extends XmppActivity { } else { intent = new Intent(getApplicationContext(), PublishProfilePictureActivity.class); - intent.putExtra("account", mAccount.getJid()); + intent.putExtra("account", mAccount.getJid().toBareJid().toString()); intent.putExtra("setup", true); } startActivity(intent); @@ -244,26 +230,14 @@ public class EditAccountActivity extends XmppActivity { }); } - 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) { + && mAccount.getStatus() == Account.State.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) { + && mAccount.getStatus() == Account.State.DISABLED) { this.mSaveButton.setEnabled(true); this.mSaveButton.setTextColor(getPrimaryTextColor()); this.mSaveButton.setText(R.string.enable); @@ -272,7 +246,7 @@ public class EditAccountActivity extends XmppActivity { this.mSaveButton.setTextColor(getPrimaryTextColor()); if (jidToEdit != null) { if (mAccount != null - && mAccount.getStatus() == Account.STATUS_ONLINE) { + && mAccount.getStatus() == Account.State.ONLINE) { this.mSaveButton.setText(R.string.save); if (!accountInfoEdited()) { this.mSaveButton.setEnabled(false); @@ -288,7 +262,7 @@ public class EditAccountActivity extends XmppActivity { } protected boolean accountInfoEdited() { - return (!this.mAccount.getJid().equals( + return (!this.mAccount.getJid().toBareJid().equals( this.mAccountJid.getText().toString())) || (!this.mAccount.getPassword().equals( this.mPassword.getText().toString())); @@ -297,7 +271,7 @@ public class EditAccountActivity extends XmppActivity { @Override protected String getShareableUri() { if (mAccount!=null) { - return "xmpp:"+mAccount.getJid(); + return mAccount.getShareableUri(); } else { return ""; } @@ -358,8 +332,12 @@ public class EditAccountActivity extends XmppActivity { protected void onStart() { super.onStart(); if (getIntent() != null) { - this.jidToEdit = getIntent().getStringExtra("jid"); - if (this.jidToEdit != null) { + try { + this.jidToEdit = Jid.fromString(getIntent().getStringExtra("jid")); + } catch (final InvalidJidException | NullPointerException ignored) { + this.jidToEdit = null; + } + if (this.jidToEdit != null) { this.mRegisterNew.setVisibility(View.GONE); getActionBar().setTitle(getString(R.string.account_details)); } else { @@ -370,20 +348,10 @@ public class EditAccountActivity extends XmppActivity { } @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); + KnownHostsAdapter mKnownHostsAdapter = new KnownHostsAdapter(this, + android.R.layout.simple_list_item_1, + xmppConnectionService.getKnownHosts()); if (this.jidToEdit != null) { this.mAccount = xmppConnectionService.findAccountByJid(jidToEdit); updateAccountInformation(); @@ -393,12 +361,12 @@ public class EditAccountActivity extends XmppActivity { this.mCancelButton.setEnabled(false); this.mCancelButton.setTextColor(getSecondaryTextColor()); } - this.mAccountJid.setAdapter(this.mKnownHostsAdapter); + this.mAccountJid.setAdapter(mKnownHostsAdapter); updateSaveButton(); } private void updateAccountInformation() { - this.mAccountJid.setText(this.mAccount.getJid()); + this.mAccountJid.setText(this.mAccount.getJid().toBareJid().toString()); this.mPassword.setText(this.mAccount.getPassword()); if (this.jidToEdit != null) { this.mAvatar.setVisibility(View.VISIBLE); @@ -412,7 +380,7 @@ public class EditAccountActivity extends XmppActivity { this.mRegisterNew.setVisibility(View.GONE); this.mRegisterNew.setChecked(false); } - if (this.mAccount.getStatus() == Account.STATUS_ONLINE + if (this.mAccount.getStatus() == Account.State.ONLINE && !this.mFetchingAvatar) { this.mStats.setVisibility(View.VISIBLE); this.mSessionEst.setText(UIHelper.readableTimeDifferenceFull( @@ -435,11 +403,10 @@ public class EditAccountActivity extends XmppActivity { } else { this.mServerInfoPep.setText(R.string.server_info_unavailable); } - final String fingerprint = this.mAccount - .getOtrFingerprint(xmppConnectionService); + final String fingerprint = this.mAccount.getOtrFingerprint(); if (fingerprint != null) { this.mOtrFingerprintBox.setVisibility(View.VISIBLE); - this.mOtrFingerprint.setText(fingerprint); + this.mOtrFingerprint.setText(CryptoHelper.prettifyFingerprint(fingerprint)); this.mOtrFingerprintToClipboardButton .setVisibility(View.VISIBLE); this.mOtrFingerprintToClipboardButton @@ -461,8 +428,7 @@ public class EditAccountActivity extends XmppActivity { } } else { if (this.mAccount.errorStatus()) { - this.mAccountJid.setError(getString(this.mAccount - .getReadableStatusId())); + this.mAccountJid.setError(getString(this.mAccount.getStatus().getReadableId())); this.mAccountJid.requestFocus(); } this.mStats.setVisibility(View.GONE); diff --git a/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java index 77f8b68a..906a16cc 100644 --- a/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java @@ -22,15 +22,13 @@ import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.AdapterView.OnItemClickListener; import android.widget.ListView; -public class ManageAccountActivity extends XmppActivity { +public class ManageAccountActivity extends XmppActivity implements OnAccountUpdate { 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(); @@ -43,7 +41,6 @@ public class ManageAccountActivity extends XmppActivity { } }); } - }; @Override protected void onCreate(Bundle savedInstanceState) { @@ -81,20 +78,11 @@ public class ManageAccountActivity extends XmppActivity { } 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(); + menu.setHeaderTitle(this.selectedAccount.getJid().toBareJid().toString()); } @Override void onBackendConnected() { - xmppConnectionService.setOnAccountListChangedListener(accountChanged); this.accountList.clear(); this.accountList.addAll(xmppConnectionService.getAccounts()); mAccountAdapter.notifyDataSetChanged(); @@ -166,7 +154,7 @@ public class ManageAccountActivity extends XmppActivity { private void publishAvatar(Account account) { Intent intent = new Intent(getApplicationContext(), PublishProfilePictureActivity.class); - intent.putExtra("account", account.getJid()); + intent.putExtra("account", account.getJid().toString()); startActivity(intent); } diff --git a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java index 6aa40c41..10ee0cd5 100644 --- a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java @@ -14,6 +14,8 @@ 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.jid.InvalidJidException; +import eu.siacs.conversations.xmpp.jid.Jid; import eu.siacs.conversations.xmpp.pep.Avatar; public class PublishProfilePictureActivity extends XmppActivity { @@ -148,8 +150,13 @@ public class PublishProfilePictureActivity extends XmppActivity { @Override protected void onBackendConnected() { if (getIntent() != null) { - String jid = getIntent().getStringExtra("account"); - if (jid != null) { + Jid jid; + try { + jid = Jid.fromString(getIntent().getStringExtra("account")); + } catch (InvalidJidException e) { + jid = null; + } + if (jid != null) { this.account = xmppConnectionService.findAccountByJid(jid); if (this.account.getXmppConnection() != null) { this.support = this.account.getXmppConnection() @@ -180,7 +187,7 @@ public class PublishProfilePictureActivity extends XmppActivity { } else { loadImageIntoPreview(avatarUri); } - this.accountTextView.setText(this.account.getJid()); + this.accountTextView.setText(this.account.getJid().toBareJid().toString()); } } diff --git a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java index fc6308fc..b6f3a077 100644 --- a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java @@ -5,6 +5,8 @@ import java.util.Arrays; import java.util.Locale; import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.xmpp.jid.InvalidJidException; + import android.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.os.Build; @@ -62,12 +64,14 @@ public class SettingsActivity extends XmppActivity implements .toLowerCase(Locale.US); if (xmppConnectionServiceBound) { for (Account account : xmppConnectionService.getAccounts()) { - account.setResource(resource); - if (!account.isOptionSet(Account.OPTION_DISABLED)) { + account.setResource(resource); + if (!account.isOptionSet(Account.OPTION_DISABLED)) { xmppConnectionService.reconnectAccount(account, false); } } } + } else if (name.equals("keep_foreground_service")) { + xmppConnectionService.toggleForegroundService(); } } diff --git a/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java b/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java index 9fbc3db1..609dc280 100644 --- a/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java @@ -9,6 +9,9 @@ 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 eu.siacs.conversations.xmpp.jid.InvalidJidException; +import eu.siacs.conversations.xmpp.jid.Jid; + import android.app.PendingIntent; import android.content.Intent; import android.net.Uri; @@ -150,13 +153,23 @@ public class ShareWithActivity extends XmppActivity { } private void share() { - Account account = xmppConnectionService.findAccountByJid(share.account); - if (account == null) { + Account account; + try { + account = xmppConnectionService.findAccountByJid(Jid.fromString(share.account)); + } catch (final InvalidJidException e) { + account = null; + } + if (account == null) { return; } - Conversation conversation = xmppConnectionService - .findOrCreateConversation(account, share.contact, false); - share(conversation); + final Conversation conversation; + try { + conversation = xmppConnectionService + .findOrCreateConversation(account, Jid.fromString(share.contact), false); + } catch (final InvalidJidException e) { + return; + } + share(conversation); } private void share(final Conversation conversation) { diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java index ed6b2a85..21ca5153 100644 --- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java @@ -45,8 +45,6 @@ import android.widget.Spinner; import com.google.zxing.integration.android.IntentIntegrator; import com.google.zxing.integration.android.IntentResult; -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -59,64 +57,33 @@ 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.utils.XmppUri; 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; +import eu.siacs.conversations.xmpp.jid.InvalidJidException; +import eu.siacs.conversations.xmpp.jid.Jid; -public class StartConversationActivity extends XmppActivity { +public class StartConversationActivity extends XmppActivity implements OnRosterUpdate { + public int conference_context_id; + public int contact_context_id; private Tab mContactsTab; private Tab mConferencesTab; private ViewPager mViewPager; - private MyListFragment mContactsListFragment = new MyListFragment(); - private List<ListItem> contacts = new ArrayList<ListItem>(); + private List<ListItem> contacts = new ArrayList<>(); private ArrayAdapter<ListItem> mContactsAdapter; - private MyListFragment mConferenceListFragment = new MyListFragment(); private List<ListItem> conferences = new ArrayList<ListItem>(); private ArrayAdapter<ListItem> mConferenceAdapter; - private List<String> mActivatedAccounts = new ArrayList<String>(); private List<String> mKnownHosts; private List<String> mKnownConferenceHosts; - private Invite mPendingInvite = null; - private Menu mOptionsMenu; private EditText mSearchEditText; - - 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 @@ -145,6 +112,31 @@ public class StartConversationActivity extends XmppActivity { return true; } }; + 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 TextWatcher mSearchTextWatcher = new TextWatcher() { @Override @@ -162,23 +154,21 @@ public class StartConversationActivity extends XmppActivity { int count) { } }; - private OnRosterUpdate onRosterUpdate = new OnRosterUpdate() { + private MenuItem mMenuSearchView; + private String mInitialJid; - @Override - public void onRosterUpdate() { - runOnUiThread(new Runnable() { + @Override + public void onRosterUpdate() { + runOnUiThread(new Runnable() { - @Override - public void run() { - if (mSearchEditText != null) { - filter(mSearchEditText.getText().toString()); - } + @Override + public void run() { + if (mSearchEditText != null) { + filter(mSearchEditText.getText().toString()); } - }); - } - }; - private MenuItem mMenuSearchView; - private String mInitialJid; + } + }); + } @Override public void onCreate(Bundle savedInstanceState) { @@ -241,12 +231,6 @@ public class StartConversationActivity extends XmppActivity { } - @Override - public void onStop() { - super.onStop(); - xmppConnectionService.removeOnRosterUpdateListener(); - } - protected void openConversationForContact(int position) { Contact contact = (Contact) contacts.get(position); Conversation conversation = xmppConnectionService @@ -331,7 +315,7 @@ public class StartConversationActivity extends XmppActivity { } @SuppressLint("InflateParams") - protected void showCreateContactDialog(String prefilledJid) { + protected void showCreateContactDialog(final String prefilledJid, final String fingerprint) { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.create_contact); View dialogView = getLayoutInflater().inflate( @@ -343,6 +327,12 @@ public class StartConversationActivity extends XmppActivity { android.R.layout.simple_list_item_1, mKnownHosts)); if (prefilledJid != null) { jid.append(prefilledJid); + if (fingerprint!=null) { + jid.setFocusable(false); + jid.setFocusableInTouchMode(false); + jid.setClickable(false); + jid.setCursorVisible(false); + } } populateAccountSpinner(spinner); builder.setView(dialogView); @@ -359,20 +349,30 @@ public class StartConversationActivity extends XmppActivity { return; } if (Validator.isValidJid(jid.getText().toString())) { - String accountJid = (String) spinner - .getSelectedItem(); - String contactJid = jid.getText().toString(); + final Jid accountJid; + try { + accountJid = Jid.fromString((String) spinner + .getSelectedItem()); + } catch (final InvalidJidException e) { + return; + } + final Jid contactJid; + try { + contactJid = Jid.fromString(jid.getText().toString()); + } catch (final InvalidJidException e) { + return; + } Account account = xmppConnectionService .findAccountByJid(accountJid); if (account == null) { dialog.dismiss(); return; } - Contact contact = account.getRoster().getContact( - contactJid); + Contact contact = account.getRoster().getContact(contactJid); if (contact.showInRoster()) { jid.setError(getString(R.string.contact_already_exists)); } else { + contact.addOtrFingerprint(fingerprint); xmppConnectionService.createContact(contact); dialog.dismiss(); switchToConversation(contact); @@ -416,9 +416,18 @@ public class StartConversationActivity extends XmppActivity { return; } if (Validator.isValidJid(jid.getText().toString())) { - String accountJid = (String) spinner - .getSelectedItem(); - String conferenceJid = jid.getText().toString(); + final Jid accountJid; + try { + accountJid = Jid.fromString((String) spinner.getSelectedItem()); + } catch (final InvalidJidException e) { + return; + } + final Jid conferenceJid; + try { + conferenceJid = Jid.fromString(jid.getText().toString()); + } catch (final InvalidJidException e) { + return; // TODO: Do some error handling... + } Account account = xmppConnectionService .findAccountByJid(accountJid); if (account == null) { @@ -471,7 +480,7 @@ public class StartConversationActivity extends XmppActivity { } private void populateAccountSpinner(Spinner spinner) { - ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, + ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, mActivatedAccounts); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); spinner.setAdapter(adapter); @@ -508,7 +517,7 @@ public class StartConversationActivity extends XmppActivity { public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.action_create_contact: - showCreateContactDialog(null); + showCreateContactDialog(null,null); return true; case R.id.action_join_conference: showJoinConferenceDialog(null); @@ -550,11 +559,10 @@ public class StartConversationActivity extends XmppActivity { @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()); + if (account.getStatus() != Account.State.DISABLED) { + this.mActivatedAccounts.add(account.getJid().toBareJid().toString()); } } this.mKnownHosts = xmppConnectionService.getKnownHosts(); @@ -591,20 +599,20 @@ public class StartConversationActivity extends XmppActivity { for (Parcelable message : getIntent().getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)) { if (message instanceof NdefMessage) { Log.d(Config.LOGTAG, "received message=" + message); - for (NdefRecord record : ((NdefMessage)message).getRecords()) { + for (NdefRecord record : ((NdefMessage) message).getRecords()) { switch (record.getTnf()) { - case NdefRecord.TNF_WELL_KNOWN: - if (Arrays.equals(record.getType(), NdefRecord.RTD_URI)) { - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - return getInviteJellyBean(record).invite(); - } else { - byte[] payload = record.getPayload(); - if (payload[0] == 0) { - return new Invite(Uri.parse(new String(Arrays.copyOfRange( - payload, 1, payload.length)))).invite(); + case NdefRecord.TNF_WELL_KNOWN: + if (Arrays.equals(record.getType(), NdefRecord.RTD_URI)) { + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + return getInviteJellyBean(record).invite(); + } else { + byte[] payload = record.getPayload(); + if (payload[0] == 0) { + return new Invite(Uri.parse(new String(Arrays.copyOfRange( + payload, 1, payload.length)))).invite(); + } } } - } } } } @@ -613,22 +621,29 @@ public class StartConversationActivity extends XmppActivity { return false; } - private boolean handleJid(String jid) { - List<Contact> contacts = xmppConnectionService.findContacts(jid); + private boolean handleJid(Invite invite) { + List<Contact> contacts = xmppConnectionService.findContacts(invite.jid); if (contacts.size() == 0) { - showCreateContactDialog(jid); + showCreateContactDialog(invite.jid,invite.fingerprint); return false; } else if (contacts.size() == 1) { - switchToConversation(contacts.get(0)); + Contact contact = contacts.get(0); + if (invite.fingerprint != null) { + if (contact.addOtrFingerprint(invite.fingerprint)) { + Log.d(Config.LOGTAG,"added new fingerprint"); + xmppConnectionService.syncRosterToDisk(contact.getAccount()); + } + } + switchToConversation(contact); return true; } else { if (mMenuSearchView != null) { mMenuSearchView.expandActionView(); mSearchEditText.setText(""); - mSearchEditText.append(jid); - filter(jid); + mSearchEditText.append(invite.jid); + filter(invite.jid); } else { - mInitialJid = jid; + mInitialJid = invite.jid; } return true; } @@ -644,7 +659,7 @@ public class StartConversationActivity extends XmppActivity { protected void filterContacts(String needle) { this.contacts.clear(); for (Account account : xmppConnectionService.getAccounts()) { - if (account.getStatus() != Account.STATUS_DISABLED) { + if (account.getStatus() != Account.State.DISABLED) { for (Contact contact : account.getRoster().getContacts()) { if (contact.showInRoster() && contact.match(needle)) { this.contacts.add(contact); @@ -659,7 +674,7 @@ public class StartConversationActivity extends XmppActivity { protected void filterConferences(String needle) { this.conferences.clear(); for (Account account : xmppConnectionService.getAccounts()) { - if (account.getStatus() != Account.STATUS_DISABLED) { + if (account.getStatus() != Account.State.DISABLED) { for (Bookmark bookmark : account.getBookmarks()) { if (bookmark.match(needle)) { this.conferences.add(bookmark); @@ -738,20 +753,17 @@ public class StartConversationActivity extends XmppActivity { } } - private class Invite { + private class Invite extends XmppUri { private String jid; private boolean muc; + private String fingerprint; - Invite(Uri uri) { - parse(uri); + public Invite(Uri uri) { + super(uri); } - Invite(String uri) { - try { - parse(Uri.parse(uri)); - } catch (IllegalArgumentException e) { - jid = null; - } + public Invite(String uri) { + super(uri); } boolean invite() { @@ -759,29 +771,10 @@ public class StartConversationActivity extends XmppActivity { if (muc) { showJoinConferenceDialog(jid); } else { - return handleJid(jid); + return handleJid(this); } } return false; } - - void parse(Uri uri) { - String scheme = uri.getScheme(); - if ("xmpp".equals(scheme)) { - // sample: xmpp:jid@foo.com - muc = "join".equalsIgnoreCase(uri.getQuery()); - if (uri.getAuthority() != null) { - jid = uri.getAuthority(); - } else { - jid = uri.getSchemeSpecificPart().split("\\?")[0]; - } - } else if ("imto".equals(scheme)) { - // sample: imto://xmpp/jid@foo.com - try { - jid = URLDecoder.decode(uri.getEncodedPath(), "UTF-8").split("/")[1]; - } catch (UnsupportedEncodingException e) { - } - } - } } } diff --git a/src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java b/src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java new file mode 100644 index 00000000..233a5e99 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java @@ -0,0 +1,391 @@ +package eu.siacs.conversations.ui; + +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.RelativeLayout; +import android.widget.TextView; +import android.widget.Toast; + +import com.google.zxing.integration.android.IntentIntegrator; +import com.google.zxing.integration.android.IntentResult; + +import net.java.otr4j.OtrException; +import net.java.otr4j.session.Session; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.CryptoHelper; +import eu.siacs.conversations.utils.XmppUri; +import eu.siacs.conversations.xmpp.jid.InvalidJidException; +import eu.siacs.conversations.xmpp.jid.Jid; + +public class VerifyOTRActivity extends XmppActivity implements XmppConnectionService.OnConversationUpdate { + + public static final String ACTION_VERIFY_CONTACT = "verify_contact"; + + private RelativeLayout mVerificationAreaOne; + private RelativeLayout mVerificationAreaTwo; + private TextView mErrorNoSession; + private TextView mRemoteJid; + private TextView mRemoteFingerprint; + private TextView mYourFingerprint; + private EditText mSharedSecretHint; + private EditText mSharedSecretSecret; + private Button mButtonScanQrCode; + private Button mButtonShowQrCode; + private Button mButtonSharedSecretPositive; + private Button mButtonSharedSecretNegative; + private TextView mStatusMessage; + private Account mAccount; + private Conversation mConversation; + + private DialogInterface.OnClickListener mVerifyFingerprintListener = new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialogInterface, int click) { + mConversation.verifyOtrFingerprint(); + updateView(); + xmppConnectionService.syncRosterToDisk(mConversation.getAccount()); + } + }; + + private View.OnClickListener mShowQrCodeListener = new View.OnClickListener() { + @Override + public void onClick(final View view) { + showQrCode(); + } + }; + + private View.OnClickListener mScanQrCodeListener = new View.OnClickListener() { + + @Override + public void onClick(View view) { + new IntentIntegrator(VerifyOTRActivity.this).initiateScan(); + } + + }; + + private View.OnClickListener mCreateSharedSecretListener = new View.OnClickListener() { + @Override + public void onClick(final View view) { + if (isAccountOnline()) { + final String question = mSharedSecretHint.getText().toString(); + final String secret = mSharedSecretSecret.getText().toString(); + initSmp(question, secret); + updateView(); + } + } + }; + private View.OnClickListener mCancelSharedSecretListener = new View.OnClickListener() { + @Override + public void onClick(View view) { + if (isAccountOnline()) { + abortSmp(); + updateView(); + } + } + }; + private View.OnClickListener mRespondSharedSecretListener = new View.OnClickListener() { + + @Override + public void onClick(View view) { + if (isAccountOnline()) { + final String question = mSharedSecretHint.getText().toString(); + final String secret = mSharedSecretSecret.getText().toString(); + respondSmp(question, secret); + updateView(); + } + } + }; + private View.OnClickListener mRetrySharedSecretListener = new View.OnClickListener() { + @Override + public void onClick(View view) { + mConversation.smp().status = Conversation.Smp.STATUS_NONE; + mConversation.smp().hint = null; + mConversation.smp().secret = null; + updateView(); + } + }; + private View.OnClickListener mFinishListener = new View.OnClickListener() { + @Override + public void onClick(View view) { + mConversation.smp().status = Conversation.Smp.STATUS_NONE; + finish(); + } + }; + + private XmppUri mPendingUri = null; + + protected boolean initSmp(final String question, final String secret) { + final Session session = mConversation.getOtrSession(); + if (session!=null) { + try { + session.initSmp(question, secret); + mConversation.smp().status = Conversation.Smp.STATUS_WE_REQUESTED; + return true; + } catch (OtrException e) { + return false; + } + } else { + return false; + } + } + + protected boolean abortSmp() { + final Session session = mConversation.getOtrSession(); + if (session!=null) { + try { + session.abortSmp(); + mConversation.smp().status = Conversation.Smp.STATUS_NONE; + mConversation.smp().hint = null; + mConversation.smp().secret = null; + return true; + } catch (OtrException e) { + return false; + } + } else { + return false; + } + } + + protected boolean respondSmp(final String question, final String secret) { + final Session session = mConversation.getOtrSession(); + if (session!=null) { + try { + session.respondSmp(question,secret); + return true; + } catch (OtrException e) { + return false; + } + } else { + return false; + } + } + + protected void verifyWithUri(XmppUri uri) { + Contact contact = mConversation.getContact(); + if (this.mConversation.getContact().getJid().equals(uri.getJid()) && uri.getFingerprint() != null) { + contact.addOtrFingerprint(uri.getFingerprint()); + Toast.makeText(this,R.string.verified,Toast.LENGTH_SHORT).show(); + updateView(); + xmppConnectionService.syncRosterToDisk(contact.getAccount()); + } else { + Toast.makeText(this,R.string.could_not_verify_fingerprint,Toast.LENGTH_SHORT).show(); + } + } + + protected boolean isAccountOnline() { + if (this.mAccount.getStatus() != Account.State.ONLINE) { + Toast.makeText(this,R.string.not_connected_try_again,Toast.LENGTH_SHORT).show(); + return false; + } else { + return true; + } + } + + protected boolean handleIntent(Intent intent) { + if (intent.getAction().equals(ACTION_VERIFY_CONTACT)) { + try { + this.mAccount = this.xmppConnectionService.findAccountByJid(Jid.fromString(intent.getExtras().getString("account"))); + } catch (final InvalidJidException ignored) { + return false; + } + try { + this.mConversation = this.xmppConnectionService.find(this.mAccount,Jid.fromString(intent.getExtras().getString("contact"))); + if (this.mConversation == null) { + return false; + } + } catch (final InvalidJidException ignored) { + return false; + } + return true; + } else { + return false; + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent intent) { + if ((requestCode & 0xFFFF) == IntentIntegrator.REQUEST_CODE) { + IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent); + if (scanResult != null && scanResult.getFormatName() != null) { + String data = scanResult.getContents(); + XmppUri uri = new XmppUri(data); + if (xmppConnectionServiceBound) { + verifyWithUri(uri); + } else { + this.mPendingUri = uri; + } + } + } + super.onActivityResult(requestCode, requestCode, intent); + } + + @Override + protected void onBackendConnected() { + if (handleIntent(getIntent())) { + if (mPendingUri!=null) { + verifyWithUri(mPendingUri); + mPendingUri = null; + } + updateView(); + } + } + + protected void updateView() { + if (this.mConversation.hasValidOtrSession()) { + invalidateOptionsMenu(); + this.mVerificationAreaOne.setVisibility(View.VISIBLE); + this.mVerificationAreaTwo.setVisibility(View.VISIBLE); + this.mErrorNoSession.setVisibility(View.GONE); + this.mYourFingerprint.setText(CryptoHelper.prettifyFingerprint(this.mAccount.getOtrFingerprint())); + this.mRemoteFingerprint.setText(this.mConversation.getOtrFingerprint()); + this.mRemoteJid.setText(this.mConversation.getContact().getJid().toBareJid().toString()); + Conversation.Smp smp = mConversation.smp(); + Session session = mConversation.getOtrSession(); + if (mConversation.isOtrFingerprintVerified()) { + deactivateButton(mButtonScanQrCode, R.string.verified); + } else { + activateButton(mButtonScanQrCode, R.string.scan_qr_code, mScanQrCodeListener); + } + if (smp.status == Conversation.Smp.STATUS_NONE) { + activateButton(mButtonSharedSecretPositive, R.string.create, mCreateSharedSecretListener); + deactivateButton(mButtonSharedSecretNegative, R.string.cancel); + this.mSharedSecretHint.setFocusableInTouchMode(true); + this.mSharedSecretSecret.setFocusableInTouchMode(true); + this.mSharedSecretSecret.setText(""); + this.mSharedSecretHint.setText(""); + this.mSharedSecretHint.setVisibility(View.VISIBLE); + this.mSharedSecretSecret.setVisibility(View.VISIBLE); + this.mStatusMessage.setVisibility(View.GONE); + } else if (smp.status == Conversation.Smp.STATUS_CONTACT_REQUESTED) { + this.mSharedSecretHint.setFocusable(false); + this.mSharedSecretHint.setText(smp.hint); + this.mSharedSecretSecret.setFocusableInTouchMode(true); + this.mSharedSecretHint.setVisibility(View.VISIBLE); + this.mSharedSecretSecret.setVisibility(View.VISIBLE); + this.mStatusMessage.setVisibility(View.GONE); + deactivateButton(mButtonSharedSecretNegative, R.string.cancel); + activateButton(mButtonSharedSecretPositive, R.string.respond, mRespondSharedSecretListener); + } else if (smp.status == Conversation.Smp.STATUS_FAILED) { + activateButton(mButtonSharedSecretNegative, R.string.cancel, mFinishListener); + activateButton(mButtonSharedSecretPositive, R.string.try_again, mRetrySharedSecretListener); + this.mSharedSecretHint.setVisibility(View.GONE); + this.mSharedSecretSecret.setVisibility(View.GONE); + this.mStatusMessage.setVisibility(View.VISIBLE); + this.mStatusMessage.setText(R.string.secrets_do_not_match); + this.mStatusMessage.setTextColor(getWarningTextColor()); + } else if (smp.status == Conversation.Smp.STATUS_VERIFIED) { + this.mSharedSecretHint.setVisibility(View.GONE); + this.mSharedSecretSecret.setVisibility(View.GONE); + this.mStatusMessage.setVisibility(View.VISIBLE); + this.mStatusMessage.setText(R.string.verified); + this.mStatusMessage.setTextColor(getPrimaryColor()); + deactivateButton(mButtonSharedSecretNegative, R.string.cancel); + activateButton(mButtonSharedSecretPositive, R.string.finish, mFinishListener); + } else if (session != null && session.isSmpInProgress()) { + deactivateButton(mButtonSharedSecretPositive, R.string.in_progress); + activateButton(mButtonSharedSecretNegative, R.string.cancel, mCancelSharedSecretListener); + this.mSharedSecretHint.setVisibility(View.VISIBLE); + this.mSharedSecretSecret.setVisibility(View.VISIBLE); + this.mSharedSecretHint.setFocusable(false); + this.mSharedSecretSecret.setFocusable(false); + } + } else { + this.mVerificationAreaOne.setVisibility(View.GONE); + this.mVerificationAreaTwo.setVisibility(View.GONE); + this.mErrorNoSession.setVisibility(View.VISIBLE); + } + } + + protected void activateButton(Button button, int text, View.OnClickListener listener) { + button.setEnabled(true); + button.setTextColor(getPrimaryTextColor()); + button.setText(text); + button.setOnClickListener(listener); + } + + protected void deactivateButton(Button button, int text) { + button.setEnabled(false); + button.setTextColor(getSecondaryTextColor()); + button.setText(text); + button.setOnClickListener(null); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_verify_otr); + this.mRemoteFingerprint = (TextView) findViewById(R.id.remote_fingerprint); + this.mRemoteJid = (TextView) findViewById(R.id.remote_jid); + this.mYourFingerprint = (TextView) findViewById(R.id.your_fingerprint); + this.mButtonSharedSecretNegative = (Button) findViewById(R.id.button_shared_secret_negative); + this.mButtonSharedSecretPositive = (Button) findViewById(R.id.button_shared_secret_positive); + this.mButtonScanQrCode = (Button) findViewById(R.id.button_scan_qr_code); + this.mButtonShowQrCode = (Button) findViewById(R.id.button_show_qr_code); + this.mButtonShowQrCode.setOnClickListener(this.mShowQrCodeListener); + this.mSharedSecretSecret = (EditText) findViewById(R.id.shared_secret_secret); + this.mSharedSecretHint = (EditText) findViewById(R.id.shared_secret_hint); + this.mStatusMessage= (TextView) findViewById(R.id.status_message); + this.mVerificationAreaOne = (RelativeLayout) findViewById(R.id.verification_area_one); + this.mVerificationAreaTwo = (RelativeLayout) findViewById(R.id.verification_area_two); + this.mErrorNoSession = (TextView) findViewById(R.id.error_no_session); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.verify_otr, menu); + if (mConversation != null && mConversation.isOtrFingerprintVerified()) { + MenuItem manuallyVerifyItem = menu.findItem(R.id.manually_verify); + manuallyVerifyItem.setVisible(false); + } + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem menuItem) { + if (menuItem.getItemId() == R.id.manually_verify) { + showManuallyVerifyDialog(); + return true; + } else { + return super.onOptionsItemSelected(menuItem); + } + } + + private void showManuallyVerifyDialog() { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.manually_verify); + builder.setMessage(R.string.are_you_sure_verify_fingerprint); + builder.setNegativeButton(R.string.cancel, null); + builder.setPositiveButton(R.string.verify, mVerifyFingerprintListener); + builder.create().show(); + } + + @Override + protected String getShareableUri() { + if (mAccount!=null) { + return mAccount.getShareableUri(); + } else { + return ""; + } + } + + public void onConversationUpdate() { + runOnUiThread(new Runnable() { + @Override + public void run() { + updateView(); + } + }); + } +} diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java index 052385f6..98c9cdde 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -48,6 +48,8 @@ import com.google.zxing.common.BitMatrix; import com.google.zxing.qrcode.QRCodeWriter; import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; +import net.java.otr4j.session.SessionID; + import java.io.FileNotFoundException; import java.lang.ref.WeakReference; import java.util.Hashtable; @@ -60,11 +62,14 @@ 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.AvatarService; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder; import eu.siacs.conversations.utils.ExceptionHelper; +import eu.siacs.conversations.xmpp.jid.InvalidJidException; +import eu.siacs.conversations.xmpp.jid.Jid; public abstract class XmppActivity extends Activity { @@ -73,6 +78,7 @@ public abstract class XmppActivity extends Activity { public XmppConnectionService xmppConnectionService; public boolean xmppConnectionServiceBound = false; + protected boolean registeredListeners = false; protected int mPrimaryTextColor; protected int mSecondaryTextColor; @@ -101,6 +107,10 @@ public abstract class XmppActivity extends Activity { XmppConnectionBinder binder = (XmppConnectionBinder) service; xmppConnectionService = binder.getService(); xmppConnectionServiceBound = true; + if (!registeredListeners) { + registerListeners(); + registeredListeners = true; + } onBackendConnected(); } @@ -115,6 +125,12 @@ public abstract class XmppActivity extends Activity { super.onStart(); if (!xmppConnectionServiceBound) { connectToBackend(); + } else { + if (!registeredListeners) { + this.registerListeners(); + this.registeredListeners = true; + } + this.onBackendConnected(); } } @@ -129,6 +145,10 @@ public abstract class XmppActivity extends Activity { protected void onStop() { super.onStop(); if (xmppConnectionServiceBound) { + if (registeredListeners) { + this.unregisterListeners(); + this.registeredListeners = false; + } unbindService(mConnection); xmppConnectionServiceBound = false; } @@ -199,6 +219,36 @@ public abstract class XmppActivity extends Activity { abstract void onBackendConnected(); + protected void registerListeners() { + if (this instanceof XmppConnectionService.OnConversationUpdate) { + this.xmppConnectionService.setOnConversationListChangedListener((XmppConnectionService.OnConversationUpdate) this); + } + if (this instanceof XmppConnectionService.OnAccountUpdate) { + this.xmppConnectionService.setOnAccountListChangedListener((XmppConnectionService.OnAccountUpdate) this); + } + if (this instanceof XmppConnectionService.OnRosterUpdate) { + this.xmppConnectionService.setOnRosterUpdateListener((XmppConnectionService.OnRosterUpdate) this); + } + if (this instanceof MucOptions.OnRenameListener) { + this.xmppConnectionService.setOnRenameListener((MucOptions.OnRenameListener) this); + } + } + + protected void unregisterListeners() { + if (this instanceof XmppConnectionService.OnConversationUpdate) { + this.xmppConnectionService.removeOnConversationListChangedListener(); + } + if (this instanceof XmppConnectionService.OnAccountUpdate) { + this.xmppConnectionService.removeOnAccountListChangedListener(); + } + if (this instanceof XmppConnectionService.OnRosterUpdate) { + this.xmppConnectionService.removeOnRosterUpdateListener(); + } + if (this instanceof MucOptions.OnRenameListener) { + this.xmppConnectionService.setOnRenameListener(null); + } + } + public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.action_settings: @@ -275,14 +325,14 @@ public abstract class XmppActivity extends Activity { 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()); + intent.putExtra("account", contact.getAccount().getJid().toBareJid().toString()); + intent.putExtra("contact", contact.getJid().toString()); startActivity(intent); } public void switchToAccount(Account account) { Intent intent = new Intent(this, EditAccountActivity.class); - intent.putExtra("jid", account.getJid()); + intent.putExtra("jid", account.getJid().toBareJid().toString()); startActivity(intent); } @@ -303,7 +353,7 @@ public abstract class XmppActivity extends Activity { try { startIntentSenderForResult(pi.getIntentSender(), REQUEST_ANNOUNCE_PGP, null, 0, 0, 0); - } catch (SendIntentException e) { + } catch (final SendIntentException ignored) { } } @@ -347,9 +397,9 @@ public abstract class XmppActivity extends Activity { } protected void showAddToRosterDialog(final Conversation conversation) { - String jid = conversation.getContactJid(); + final Jid jid = conversation.getContactJid(); AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(jid); + builder.setTitle(jid.toString()); builder.setMessage(getString(R.string.not_in_roster)); builder.setNegativeButton(getString(R.string.cancel), null); builder.setPositiveButton(getString(R.string.add_contact), @@ -357,7 +407,7 @@ public abstract class XmppActivity extends Activity { @Override public void onClick(DialogInterface dialog, int which) { - String jid = conversation.getContactJid(); + final Jid jid = conversation.getContactJid(); Account account = conversation.getAccount(); Contact contact = account.getRoster().getContact(jid); xmppConnectionService.createContact(contact); @@ -369,7 +419,7 @@ public abstract class XmppActivity extends Activity { private void showAskForPresenceDialog(final Contact contact) { AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(contact.getJid()); + builder.setTitle(contact.getJid().toString()); builder.setMessage(R.string.request_presence_updates); builder.setNegativeButton(R.string.cancel, null); builder.setPositiveButton(R.string.request_now, @@ -391,14 +441,14 @@ public abstract class XmppActivity extends Activity { private void warnMutalPresenceSubscription(final Conversation conversation, final OnPresenceSelected listener) { AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(conversation.getContact().getJid()); + builder.setTitle(conversation.getContact().getJid().toString()); builder.setMessage(R.string.without_mutual_presence_updates); builder.setNegativeButton(R.string.cancel, null); builder.setPositiveButton(R.string.ignore, new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - conversation.setNextPresence(null); + conversation.setNextCounterpart(null); if (listener != null) { listener.onPresenceSelected(); } @@ -449,26 +499,40 @@ public abstract class XmppActivity extends Activity { public void selectPresence(final Conversation conversation, final OnPresenceSelected listener) { - Contact contact = conversation.getContact(); - if (!contact.showInRoster()) { + final Contact contact = conversation.getContact(); + if (conversation.hasValidOtrSession()) { + SessionID id = conversation.getOtrSession().getSessionID(); + Jid jid; + try { + jid = Jid.fromString(id.getAccountID() + "/" + id.getUserID()); + } catch (InvalidJidException e) { + jid = null; + } + conversation.setNextCounterpart(jid); + listener.onPresenceSelected(); + } else if (!contact.showInRoster()) { showAddToRosterDialog(conversation); } else { Presences presences = contact.getPresences(); if (presences.size() == 0) { if (!contact.getOption(Contact.Options.TO) && !contact.getOption(Contact.Options.ASKING) - && contact.getAccount().getStatus() == Account.STATUS_ONLINE) { + && contact.getAccount().getStatus() == Account.State.ONLINE) { showAskForPresenceDialog(contact); } else if (!contact.getOption(Contact.Options.TO) || !contact.getOption(Contact.Options.FROM)) { warnMutalPresenceSubscription(conversation, listener); } else { - conversation.setNextPresence(null); + conversation.setNextCounterpart(null); listener.onPresenceSelected(); } } else if (presences.size() == 1) { String presence = presences.asStringArray()[0]; - conversation.setNextPresence(presence); + try { + conversation.setNextCounterpart(Jid.fromParts(contact.getJid().getLocalpart(),contact.getJid().getDomainpart(),presence)); + } catch (InvalidJidException e) { + conversation.setNextCounterpart(null); + } listener.onPresenceSelected(); } else { final StringBuilder presence = new StringBuilder(); @@ -499,7 +563,11 @@ public abstract class XmppActivity extends Activity { @Override public void onClick(DialogInterface dialog, int which) { - conversation.setNextPresence(presence.toString()); + try { + conversation.setNextCounterpart(Jid.fromParts(contact.getJid().getLocalpart(),contact.getJid().getDomainpart(),presence.toString())); + } catch (InvalidJidException e) { + conversation.setNextCounterpart(null); + } listener.onPresenceSelected(); } }); @@ -567,11 +635,10 @@ public abstract class XmppActivity extends Activity { nfcAdapter.setNdefPushMessageCallback(new NfcAdapter.CreateNdefMessageCallback() { @Override public NdefMessage createNdefMessage(NfcEvent nfcEvent) { - NdefMessage msg = new NdefMessage(new NdefRecord[]{ - NdefRecord.createUri(getShareableUri()), - NdefRecord.createApplicationRecord("eu.siacs.conversations") - }); - return msg; + return new NdefMessage(new NdefRecord[]{ + NdefRecord.createUri(getShareableUri()), + NdefRecord.createApplicationRecord("eu.siacs.conversations") + }); } }, this); } @@ -618,9 +685,10 @@ public abstract class XmppActivity extends Activity { } protected Bitmap createQrCodeBitmap(String input, int size) { + Log.d(Config.LOGTAG,"qr code requested size: "+size); try { final QRCodeWriter QR_CODE_WRITER = new QRCodeWriter(); - final Hashtable<EncodeHintType, Object> hints = new Hashtable<EncodeHintType, Object>(); + final Hashtable<EncodeHintType, Object> hints = new Hashtable<>(); hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M); final BitMatrix result = QR_CODE_WRITER.encode(input, BarcodeFormat.QR_CODE, size, size, hints); final int width = result.getWidth(); @@ -633,6 +701,7 @@ public abstract class XmppActivity extends Activity { } } final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Log.d(Config.LOGTAG,"output size: "+width+"x"+height); bitmap.setPixels(pixels, 0, width, 0, 0, width, height); return bitmap; } catch (final WriterException e) { @@ -649,7 +718,7 @@ public abstract class XmppActivity extends Activity { private Message message = null; public BitmapWorkerTask(ImageView imageView) { - imageViewReference = new WeakReference<ImageView>(imageView); + imageViewReference = new WeakReference<>(imageView); } @Override @@ -665,7 +734,7 @@ public abstract class XmppActivity extends Activity { @Override protected void onPostExecute(Bitmap bitmap) { - if (imageViewReference != null && bitmap != null) { + if (bitmap != null) { final ImageView imageView = imageViewReference.get(); if (imageView != null) { imageView.setImageBitmap(bitmap); @@ -695,9 +764,8 @@ public abstract class XmppActivity extends Activity { imageView.setImageDrawable(asyncDrawable); try { task.execute(message); - } catch (RejectedExecutionException e) { - return; - } + } catch (final RejectedExecutionException ignored) { + } } } } @@ -734,7 +802,7 @@ public abstract class XmppActivity extends Activity { public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) { super(res, bitmap); - bitmapWorkerTaskReference = new WeakReference<BitmapWorkerTask>( + bitmapWorkerTaskReference = new WeakReference<>( bitmapWorkerTask); } diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java index e13b3204..139f3657 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java @@ -31,72 +31,24 @@ public class AccountAdapter extends ArrayAdapter<Account> { view = inflater.inflate(R.layout.account_row, parent, false); } TextView jid = (TextView) view.findViewById(R.id.account_jid); - jid.setText(account.getJid()); + jid.setText(account.getJid().toBareJid().toString()); TextView statusView = (TextView) view.findViewById(R.id.account_status); ImageView imageView = (ImageView) view.findViewById(R.id.account_image); imageView.setImageBitmap(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; - } - + statusView.setText(getContext().getString(account.getStatus().getReadableId())); + switch (account.getStatus()) { + case ONLINE: + statusView.setTextColor(activity.getPrimaryColor()); + break; + case DISABLED: + case CONNECTING: + statusView.setTextColor(activity.getSecondaryTextColor()); + break; + default: + statusView.setTextColor(activity.getWarningTextColor()); + break; + } return view; } } diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java index b5c20dc5..b81544e6 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java @@ -6,6 +6,7 @@ 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.DownloadableFile; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.ui.ConversationActivity; import eu.siacs.conversations.ui.XmppActivity; @@ -58,7 +59,7 @@ public class ConversationAdapter extends ArrayAdapter<Conversation> { || activity.useSubjectToIdentifyConference()) { convName.setText(conversation.getName()); } else { - convName.setText(conversation.getContactJid().split("/")[0]); + convName.setText(conversation.getContactJid().toBareJid().toString()); } TextView mLastMessage = (TextView) view .findViewById(R.id.conversation_lastmsg); @@ -75,7 +76,7 @@ public class ConversationAdapter extends ArrayAdapter<Conversation> { convName.setTypeface(null, Typeface.NORMAL); } - if (message.getType() == Message.TYPE_IMAGE + if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE || message.getDownloadable() != null) { Downloadable d = message.getDownloadable(); if (conversation.isRead()) { @@ -89,13 +90,35 @@ public class ConversationAdapter extends ArrayAdapter<Conversation> { 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); + if (message.getType() == Message.TYPE_FILE) { + mLastMessage.setText(getContext().getString(R.string.receiving_file,d.getMimeType(), d.getProgress())); + } else { + mLastMessage.setText(getContext().getString(R.string.receiving_image, d.getProgress())); + } } else if (d.getStatus() == Downloadable.STATUS_OFFER) { - mLastMessage.setText(R.string.image_offered_for_download); + if (message.getType() == Message.TYPE_FILE) { + mLastMessage.setText(R.string.file_offered_for_download); + } else { + 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); + if (message.getType() == Message.TYPE_FILE) { + mLastMessage.setText(R.string.file_deleted); + } else { + mLastMessage.setText(R.string.image_file_deleted); + } + } else if (d.getStatus() == Downloadable.STATUS_FAILED) { + if (message.getType() == Message.TYPE_FILE) { + mLastMessage.setText(R.string.file_transmission_failed); + } else { + mLastMessage.setText(R.string.image_transmission_failed); + } + } else if (message.getImageParams().width > 0) { + mLastMessage.setVisibility(View.GONE); + imagePreview.setVisibility(View.VISIBLE); + activity.loadBitmap(message, imagePreview); } else { mLastMessage.setText(""); } @@ -103,6 +126,11 @@ public class ConversationAdapter extends ArrayAdapter<Conversation> { imagePreview.setVisibility(View.GONE); mLastMessage.setVisibility(View.VISIBLE); mLastMessage.setText(R.string.encrypted_message_received); + } else if (message.getType() == Message.TYPE_FILE && message.getImageParams().width <= 0) { + DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message); + mLastMessage.setVisibility(View.VISIBLE); + imagePreview.setVisibility(View.GONE); + mLastMessage.setText(getContext().getString(R.string.file,file.getMimeType())); } else { mLastMessage.setVisibility(View.GONE); imagePreview.setVisibility(View.VISIBLE); diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java index efc6b4d9..d78dbd6a 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java @@ -34,7 +34,7 @@ public class ListItemAdapter extends ArrayAdapter<ListItem> { TextView jid = (TextView) view.findViewById(R.id.contact_jid); ImageView picture = (ImageView) view.findViewById(R.id.contact_photo); - jid.setText(item.getJid()); + jid.setText(item.getJid().toString()); name.setText(item.getDisplayName()); picture.setImageBitmap(activity.avatarService().get(item, activity.getPixel(48))); diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index f2227308..fc80c234 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -1,16 +1,21 @@ package eu.siacs.conversations.ui.adapter; import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; import android.graphics.Typeface; +import android.net.Uri; import android.text.Spannable; import android.text.SpannableString; import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; import android.util.DisplayMetrics; +import android.util.Log; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnLongClickListener; import android.view.ViewGroup; +import android.webkit.MimeTypeMap; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.ImageView; @@ -18,6 +23,9 @@ import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; import java.util.List; import eu.siacs.conversations.Config; @@ -25,10 +33,12 @@ 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.DownloadableFile; 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 eu.siacs.conversations.xmpp.jid.Jid; public class MessageAdapter extends ArrayAdapter<Message> { @@ -95,10 +105,11 @@ public class MessageAdapter extends ArrayAdapter<Message> { } boolean multiReceived = message.getConversation().getMode() == Conversation.MODE_MULTI && message.getMergedStatus() <= Message.STATUS_RECEIVED; - if (message.getType() == Message.TYPE_IMAGE - || message.getDownloadable() != null) { + if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE || message.getDownloadable() != null) { ImageParams params = message.getImageParams(); - if (params.size != 0) { + if (params.size > (1.5 * 1024 * 1024)) { + filesize = params.size / (1024 * 1024)+ " MB"; + } else if (params.size > 0) { filesize = params.size / 1024 + " KB"; } if (message.getDownloadable() != null && message.getDownloadable().getStatus() == Downloadable.STATUS_FAILED) { @@ -110,7 +121,12 @@ public class MessageAdapter extends ArrayAdapter<Message> { info = getContext().getString(R.string.waiting); break; case Message.STATUS_UNSEND: - info = getContext().getString(R.string.sending); + Downloadable d = message.getDownloadable(); + if (d!=null) { + info = getContext().getString(R.string.sending_file,d.getProgress()); + } else { + info = getContext().getString(R.string.sending); + } break; case Message.STATUS_OFFERED: info = getContext().getString(R.string.offering); @@ -135,11 +151,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { if (contact != null) { info = contact.getDisplayName(); } else { - if (message.getPresence() != null) { - info = message.getPresence(); - } else { - info = message.getCounterpart(); - } + info = getDisplayedMucCounterpart(message.getCounterpart()); } } break; @@ -184,13 +196,13 @@ public class MessageAdapter extends ArrayAdapter<Message> { } } - private void displayInfoMessage(ViewHolder viewHolder, int r) { + private void displayInfoMessage(ViewHolder viewHolder, String text) { 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.setText(text); viewHolder.messageBody.setTextColor(activity.getSecondaryTextColor()); viewHolder.messageBody.setTypeface(null, Typeface.ITALIC); viewHolder.messageBody.setTextIsSelectable(false); @@ -227,14 +239,13 @@ public class MessageAdapter extends ArrayAdapter<Message> { privateMarker = activity .getString(R.string.private_message); } else { - String to; - if (message.getPresence() != null) { - to = message.getPresence(); + final String to; + if (message.getCounterpart() != null) { + to = message.getCounterpart().getResourcepart(); } else { - to = message.getCounterpart(); + to = ""; } - privateMarker = activity.getString( - R.string.private_message_to, to); + privateMarker = activity.getString(R.string.private_message_to, to); } SpannableString span = new SpannableString(privateMarker + " " + message.getBody()); @@ -256,11 +267,11 @@ public class MessageAdapter extends ArrayAdapter<Message> { } private void displayDownloadableMessage(ViewHolder viewHolder, - final Message message, int resid) { + final Message message, String text) { viewHolder.image.setVisibility(View.GONE); viewHolder.messageBody.setVisibility(View.GONE); viewHolder.download_button.setVisibility(View.VISIBLE); - viewHolder.download_button.setText(resid); + viewHolder.download_button.setText(text); viewHolder.download_button.setOnClickListener(new OnClickListener() { @Override @@ -271,6 +282,22 @@ public class MessageAdapter extends ArrayAdapter<Message> { viewHolder.download_button.setOnLongClickListener(openContextMenu); } + private void displayOpenableMessage(ViewHolder viewHolder,final Message message) { + final DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message); + viewHolder.image.setVisibility(View.GONE); + viewHolder.messageBody.setVisibility(View.GONE); + viewHolder.download_button.setVisibility(View.VISIBLE); + viewHolder.download_button.setText(activity.getString(R.string.open_file,file.getMimeType())); + viewHolder.download_button.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + openDonwloadable(file); + } + }); + viewHolder.download_button.setOnLongClickListener(openContextMenu); + } + private void displayImageMessage(ViewHolder viewHolder, final Message message) { if (viewHolder.download_button != null) { @@ -305,6 +332,16 @@ public class MessageAdapter extends ArrayAdapter<Message> { viewHolder.image.setOnLongClickListener(openContextMenu); } + private String getDisplayedMucCounterpart(final Jid counterpart) { + if (counterpart==null) { + return ""; + } else if (!counterpart.isBareJid()) { + return counterpart.getResourcepart(); + } else { + return counterpart.toString(); + } + } + @Override public View getView(int position, View view, ViewGroup parent) { final Message item = getItem(position); @@ -413,17 +450,14 @@ public class MessageAdapter extends ArrayAdapter<Message> { if (contact != null) { viewHolder.contact_picture.setImageBitmap(activity.avatarService().get(contact, activity.getPixel(48))); } else if (item.getConversation().getMode() == Conversation.MODE_MULTI) { - String name = item.getPresence(); - if (name == null) { - name = item.getCounterpart(); - } - viewHolder.contact_picture.setImageBitmap(activity.avatarService().get(name, activity.getPixel(48))); + viewHolder.contact_picture.setImageBitmap(activity.avatarService().get(getDisplayedMucCounterpart(item.getCounterpart()), + activity.getPixel(48))); } } else if (type == SENT) { viewHolder.contact_picture.setImageBitmap(activity.avatarService().get(item.getConversation().getAccount(), activity.getPixel(48))); } - if (viewHolder.contact_picture != null) { + if (viewHolder != null && viewHolder.contact_picture != null) { viewHolder.contact_picture .setOnClickListener(new OnClickListener() { @@ -452,42 +486,52 @@ public class MessageAdapter extends ArrayAdapter<Message> { }); } - if (item.getType() == Message.TYPE_IMAGE - || item.getDownloadable() != null) { + if (item.getDownloadable() != null && item.getDownloadable().getStatus() != Downloadable.STATUS_UPLOADING) { 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 (d != null && d.getStatus() == Downloadable.STATUS_FAILED) { - displayInfoMessage(viewHolder, R.string.image_transmission_failed); - } 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); + if (d.getStatus() == Downloadable.STATUS_DOWNLOADING) { + if (item.getType() == Message.TYPE_FILE) { + displayInfoMessage(viewHolder,activity.getString(R.string.receiving_file,d.getMimeType(),d.getProgress())); + } else { + displayInfoMessage(viewHolder,activity.getString(R.string.receiving_image,d.getProgress())); + } + } else if (d.getStatus() == Downloadable.STATUS_CHECKING) { + displayInfoMessage(viewHolder,activity.getString(R.string.checking_image)); + } else if (d.getStatus() == Downloadable.STATUS_DELETED) { + if (item.getType() == Message.TYPE_FILE) { + displayInfoMessage(viewHolder, activity.getString(R.string.file_deleted)); + } else { + displayInfoMessage(viewHolder, activity.getString(R.string.image_file_deleted)); + } + } else if (d.getStatus() == Downloadable.STATUS_OFFER) { + if (item.getType() == Message.TYPE_FILE) { + displayDownloadableMessage(viewHolder,item,activity.getString(R.string.download_file,d.getMimeType())); + } else { + displayDownloadableMessage(viewHolder, item,activity.getString(R.string.download_image)); + } + } else if (d.getStatus() == Downloadable.STATUS_OFFER_CHECK_FILESIZE) { + displayDownloadableMessage(viewHolder, item,activity.getString(R.string.check_image_filesize)); + } else if (d.getStatus() == Downloadable.STATUS_FAILED) { + if (item.getType() == Message.TYPE_FILE) { + displayInfoMessage(viewHolder, activity.getString(R.string.file_transmission_failed)); + } else { + displayInfoMessage(viewHolder, activity.getString(R.string.image_transmission_failed)); + } + } + } else if (item.getType() == Message.TYPE_IMAGE && item.getEncryption() != Message.ENCRYPTION_PGP && item.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED) { + displayImageMessage(viewHolder, item); + } else if (item.getType() == Message.TYPE_FILE && item.getEncryption() != Message.ENCRYPTION_PGP && item.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED) { + if (item.getImageParams().width > 0) { + displayImageMessage(viewHolder,item); } else { - displayDecryptionFailed(viewHolder); + displayOpenableMessage(viewHolder, item); } - } else { - if (item.getEncryption() == Message.ENCRYPTION_PGP) { - if (activity.hasPgp()) { - displayInfoMessage(viewHolder, R.string.encrypted_message); - } else { - displayInfoMessage(viewHolder, - R.string.install_openkeychain); + } else if (item.getEncryption() == Message.ENCRYPTION_PGP) { + if (activity.hasPgp()) { + displayInfoMessage(viewHolder,activity.getString(R.string.encrypted_message)); + } else { + displayInfoMessage(viewHolder, + activity.getString(R.string.install_openkeychain)); + if (viewHolder != null) { viewHolder.message_box .setOnClickListener(new OnClickListener() { @@ -497,11 +541,11 @@ public class MessageAdapter extends ArrayAdapter<Message> { } }); } - } else if (item.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) { - displayDecryptionFailed(viewHolder); - } else { - displayTextMessage(viewHolder, item); } + } else if (item.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) { + displayDecryptionFailed(viewHolder); + } else { + displayTextMessage(viewHolder, item); } displayStatus(viewHolder, item); @@ -519,6 +563,22 @@ public class MessageAdapter extends ArrayAdapter<Message> { } } + public void openDonwloadable(DownloadableFile file) { + if (!file.exists()) { + Toast.makeText(activity,R.string.file_deleted,Toast.LENGTH_SHORT).show(); + return; + } + Intent openIntent = new Intent(Intent.ACTION_VIEW); + openIntent.setDataAndType(Uri.fromFile(file), file.getMimeType()); + PackageManager manager = activity.getPackageManager(); + List<ResolveInfo> infos = manager.queryIntentActivities(openIntent, 0); + if (infos.size() > 0) { + getContext().startActivity(openIntent); + } else { + Toast.makeText(activity,R.string.no_application_found_to_open_file,Toast.LENGTH_SHORT).show(); + } + } + public interface OnContactPictureClicked { public void onContactPictureClicked(Message message); } diff --git a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java index 47595c6e..b4a6e65c 100644 --- a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java @@ -1,20 +1,14 @@ 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; +import java.text.Normalizer; 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(); + final protected static char[] consonants = "bcdfghjklmnpqrstvwxyz".toCharArray(); + final public static byte[] ONE = new byte[] { 0, 0, 0, 1 }; public static String bytesToHex(byte[] bytes) { char[] hexChars = new char[bytes.length * 2]; @@ -36,64 +30,17 @@ public class CryptoHelper { 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); + public static String hexToString(final String hexString) { + return new String(hexToBytes(hexString)); } - private static byte[] concatenateByteArrays(byte[] a, byte[] b) { + public static byte[] concatenateByteArrays(byte[] a, byte[] b) { byte[] result = new byte[a.length + b.length]; System.arraycopy(a, 0, result, 0, a.length); System.arraycopy(b, 0, result, a.length, b.length); return result; } - 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); } @@ -109,4 +56,39 @@ public class CryptoHelper { } return builder.toString(); } + + /** + * Escapes usernames or passwords for SASL. + */ + public static String saslEscape(final String s) { + final StringBuilder sb = new StringBuilder((int) (s.length() * 1.1)); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + switch (c) { + case ',': + sb.append("=2C"); + break; + case '=': + sb.append("=3D"); + break; + default: + sb.append(c); + break; + } + } + return sb.toString(); + } + + public static String saslPrep(final String s) { + return saslEscape(Normalizer.normalize(s, Normalizer.Form.NFKC)); + } + + public static String prettifyFingerprint(String fingerprint) { + StringBuilder builder = new StringBuilder(fingerprint); + builder.insert(8, " "); + builder.insert(17, " "); + builder.insert(26, " "); + builder.insert(35, " "); + return builder.toString(); + } } diff --git a/src/main/java/eu/siacs/conversations/utils/DNSHelper.java b/src/main/java/eu/siacs/conversations/utils/DNSHelper.java index f101e883..8c1a8dea 100644 --- a/src/main/java/eu/siacs/conversations/utils/DNSHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/DNSHelper.java @@ -11,6 +11,7 @@ import de.measite.minidns.record.AAAA; import de.measite.minidns.record.Data; import de.measite.minidns.util.NameUtil; import eu.siacs.conversations.Config; +import eu.siacs.conversations.xmpp.jid.Jid; import java.io.IOException; import java.net.InetAddress; @@ -26,7 +27,8 @@ import android.util.Log; public class DNSHelper { protected static Client client = new Client(); - public static Bundle getSRVRecord(String host) throws IOException { + public static Bundle getSRVRecord(final Jid jid) throws IOException { + final String host = jid.getDomainpart(); String dns[] = client.findDNS(); if (dns != null) { @@ -62,9 +64,9 @@ public class DNSHelper { // 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>>(); + TreeMap<Integer, ArrayList<SRV>> priorities = new TreeMap<>(); + TreeMap<String, ArrayList<String>> ips4 = new TreeMap<>(); + TreeMap<String, ArrayList<String>> ips6 = new TreeMap<>(); for (Record[] rrset : new Record[][] { message.getAnswers(), message.getAdditionalResourceRecords() }) { @@ -97,7 +99,7 @@ public class DNSHelper { } Random rnd = new Random(); - ArrayList<SRV> result = new ArrayList<SRV>( + ArrayList<SRV> result = new ArrayList<>( priorities.size() * 2 + 1); for (ArrayList<SRV> s : priorities.values()) { @@ -136,7 +138,7 @@ public class DNSHelper { bundle.putString("error", "nosrv"); return bundle; } - ArrayList<Bundle> values = new ArrayList<Bundle>(); + ArrayList<Bundle> values = new ArrayList<>(); for (SRV srv : result) { Bundle namePort = new Bundle(); namePort.putString("name", srv.getName()); diff --git a/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java b/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java index 88fa18ff..0ad57fe2 100644 --- a/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java +++ b/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java @@ -31,6 +31,8 @@ public class ExceptionHandler implements UncaughtExceptionHandler { OutputStream os = context.openFileOutput("stacktrace.txt", Context.MODE_PRIVATE); os.write(stacktrace.getBytes()); + os.flush(); + os.close(); } catch (FileNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); diff --git a/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java b/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java index b5fc88bd..6f3152ca 100644 --- a/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java @@ -13,6 +13,9 @@ 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.xmpp.jid.InvalidJidException; +import eu.siacs.conversations.xmpp.jid.Jid; + import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; @@ -87,12 +90,15 @@ public class ExceptionHelper { public void onClick(DialogInterface dialog, int which) { Log.d(Config.LOGTAG, "using account=" - + finalAccount.getJid() + + finalAccount.getJid().toBareJid() + " to send in stack trace"); - Conversation conversation = service - .findOrCreateConversation(finalAccount, - "bugs@siacs.eu", false); - Message message = new Message(conversation, report + Conversation conversation = null; + try { + conversation = service.findOrCreateConversation(finalAccount, + Jid.fromString("bugs@siacs.eu"), false); + } catch (final InvalidJidException ignored) { + } + Message message = new Message(conversation, report .toString(), Message.ENCRYPTION_NONE); service.sendMessage(message); } @@ -103,15 +109,12 @@ public class ExceptionHelper { @Override public void onClick(DialogInterface dialog, int which) { preferences.edit().putBoolean("never_send", true) - .commit(); + .apply(); } }); builder.create().show(); - } catch (FileNotFoundException e) { - return; - } catch (IOException e) { - return; - } + } catch (final IOException ignored) { + } } } diff --git a/src/main/java/eu/siacs/conversations/utils/UIHelper.java b/src/main/java/eu/siacs/conversations/utils/UIHelper.java index 5141c83c..c4832d60 100644 --- a/src/main/java/eu/siacs/conversations/utils/UIHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/UIHelper.java @@ -110,7 +110,7 @@ public class UIHelper { List<Account> accounts) { NotificationManager mNotificationManager = (NotificationManager) context .getSystemService(Context.NOTIFICATION_SERVICE); - List<Account> accountsWproblems = new ArrayList<Account>(); + List<Account> accountsWproblems = new ArrayList<>(); for (Account account : accounts) { if (account.hasErrorStatus()) { accountsWproblems.add(account); @@ -124,7 +124,7 @@ public class UIHelper { } else if (accountsWproblems.size() == 1) { mBuilder.setContentTitle(context .getString(R.string.problem_connecting_to_account)); - mBuilder.setContentText(accountsWproblems.get(0).getJid()); + mBuilder.setContentText(accountsWproblems.get(0).getJid().toBareJid().toString()); } else { mBuilder.setContentTitle(context .getString(R.string.problem_connecting_to_accounts)); @@ -148,40 +148,6 @@ public class UIHelper { 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; diff --git a/src/main/java/eu/siacs/conversations/utils/XmppUri.java b/src/main/java/eu/siacs/conversations/utils/XmppUri.java new file mode 100644 index 00000000..09abd049 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/utils/XmppUri.java @@ -0,0 +1,79 @@ +package eu.siacs.conversations.utils; + +import android.net.Uri; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; + +import eu.siacs.conversations.utils.CryptoHelper; +import eu.siacs.conversations.xmpp.jid.InvalidJidException; +import eu.siacs.conversations.xmpp.jid.Jid; + +public class XmppUri { + + private String jid; + private boolean muc; + private String fingerprint; + + public XmppUri(String uri) { + try { + parse(Uri.parse(uri)); + } catch (IllegalArgumentException e) { + jid = null; + } + } + + public XmppUri(Uri uri) { + parse(uri); + } + + protected void parse(Uri uri) { + String scheme = uri.getScheme(); + if ("xmpp".equals(scheme)) { + // sample: xmpp:jid@foo.com + muc = "join".equalsIgnoreCase(uri.getQuery()); + if (uri.getAuthority() != null) { + jid = uri.getAuthority(); + } else { + jid = uri.getSchemeSpecificPart().split("\\?")[0]; + } + fingerprint = parseFingerprint(uri.getQuery()); + } else if ("imto".equals(scheme)) { + // sample: imto://xmpp/jid@foo.com + try { + jid = URLDecoder.decode(uri.getEncodedPath(), "UTF-8").split("/")[1]; + } catch (final UnsupportedEncodingException ignored) { + } + } + } + + protected String parseFingerprint(String query) { + if (query == null) { + return null; + } else { + final String NEEDLE = "otr-fingerprint="; + int index = query.indexOf(NEEDLE); + if (index >= 0 && query.length() >= (NEEDLE.length() + index + 40)) { + return CryptoHelper.prettifyFingerprint(query.substring(index + NEEDLE.length(), index + NEEDLE.length() + 40)); + } else { + return null; + } + } + } + + public Jid getJid() { + try { + return Jid.fromString(this.jid); + } catch (InvalidJidException e) { + return null; + } + } + + public String getFingerprint() { + return this.fingerprint; + } + + public boolean isMuc() { + return this.muc; + } +} diff --git a/src/main/java/eu/siacs/conversations/xml/Element.java b/src/main/java/eu/siacs/conversations/xml/Element.java index 4e11ee2c..02c3e695 100644 --- a/src/main/java/eu/siacs/conversations/xml/Element.java +++ b/src/main/java/eu/siacs/conversations/xml/Element.java @@ -5,12 +5,14 @@ import java.util.Hashtable; import java.util.List; import eu.siacs.conversations.utils.XmlHelper; +import eu.siacs.conversations.xmpp.jid.InvalidJidException; +import eu.siacs.conversations.xmpp.jid.Jid; public class Element { protected String name; - protected Hashtable<String, String> attributes = new Hashtable<String, String>(); + protected Hashtable<String, String> attributes = new Hashtable<>(); protected String content; - protected List<Element> children = new ArrayList<Element>(); + protected List<Element> children = new ArrayList<>(); public Element(String name) { this.name = name; @@ -103,6 +105,18 @@ public class Element { } } + public Jid getAttributeAsJid(String name) { + final String jid = this.getAttribute(name); + if (jid != null && !jid.isEmpty()) { + try { + return Jid.fromString(jid); + } catch (final InvalidJidException e) { + return null; + } + } + return null; + } + public Hashtable<String, String> getAttributes() { return this.attributes; } diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index d59a302d..9e6b8baf 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -12,6 +12,8 @@ import android.util.Log; import android.util.SparseArray; import org.apache.http.conn.ssl.StrictHostnameVerifier; +import org.json.JSONException; +import org.json.JSONObject; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; @@ -40,9 +42,12 @@ import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.X509TrustManager; import eu.siacs.conversations.Config; +import eu.siacs.conversations.crypto.sasl.DigestMd5; +import eu.siacs.conversations.crypto.sasl.Plain; +import eu.siacs.conversations.crypto.sasl.SaslMechanism; +import eu.siacs.conversations.crypto.sasl.ScramSha1; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.DNSHelper; import eu.siacs.conversations.utils.zlib.ZLibInputStream; import eu.siacs.conversations.utils.zlib.ZLibOutputStream; @@ -50,6 +55,8 @@ 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.jid.InvalidJidException; +import eu.siacs.conversations.xmpp.jid.Jid; import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived; import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; @@ -78,12 +85,16 @@ public class XmppConnection implements Runnable { private boolean shouldBind = true; private boolean shouldAuthenticate = true; private Element streamFeatures; - private HashMap<String, List<String>> disco = new HashMap<String, List<String>>(); + private HashMap<String, List<String>> disco = new HashMap<>(); + private String streamId = null; private int smVersion = 3; - private SparseArray<String> messageReceipts = new SparseArray<String>(); - private boolean usingCompression = false; - private boolean usingEncryption = false; + private SparseArray<String> messageReceipts = new SparseArray<>(); + + private boolean enabledCompression = false; + private boolean enabledEncryption = false; + private boolean enabledCarbons = false; + private int stanzasReceived = 0; private int stanzasSent = 0; private long lastPaketReceived = 0; @@ -91,7 +102,7 @@ public class XmppConnection implements Runnable { private long lastConnect = 0; private long lastSessionStarted = 0; private int attempt = 0; - private Hashtable<String, PacketReceived> packetCallbacks = new Hashtable<String, PacketReceived>(); + private Hashtable<String, PacketReceived> packetCallbacks = new Hashtable<>(); private OnPresencePacketReceived presenceListener = null; private OnJinglePacketReceived jingleListener = null; private OnIqPacketReceived unregisteredIqListener = null; @@ -101,24 +112,26 @@ public class XmppConnection implements Runnable { private OnMessageAcknowledged acknowledgedListener = null; private XmppConnectionService mXmppConnectionService = null; + private SaslMechanism saslMechanism; + public XmppConnection(Account account, XmppConnectionService service) { this.account = account; this.wakeLock = service.getPowerManager().newWakeLock( - PowerManager.PARTIAL_WAKE_LOCK, account.getJid()); + PowerManager.PARTIAL_WAKE_LOCK, account.getJid().toBareJid().toString()); tagWriter = new TagWriter(); mXmppConnectionService = service; applicationContext = service.getApplicationContext(); } - protected void changeStatus(int nextStatus) { + protected void changeStatus(final Account.State 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)) { + if ((nextStatus == Account.State.OFFLINE) + && (account.getStatus() != Account.State.CONNECTING) + && (account.getStatus() != Account.State.ONLINE) + && (account.getStatus() != Account.State.DISABLED)) { return; } - if (nextStatus == Account.STATUS_ONLINE) { + if (nextStatus == Account.State.ONLINE) { this.attempt = 0; } account.setStatus(nextStatus); @@ -129,24 +142,24 @@ public class XmppConnection implements Runnable { } protected void connect() { - Log.d(Config.LOGTAG, account.getJid() + ": connecting"); - usingCompression = false; - usingEncryption = false; + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": connecting"); + enabledCompression = false; + enabledEncryption = false; lastConnect = SystemClock.elapsedRealtime(); lastPingSent = SystemClock.elapsedRealtime(); this.attempt++; try { shouldAuthenticate = shouldBind = !account - .isOptionSet(Account.OPTION_REGISTER); + .isOptionSet(Account.OPTION_REGISTER); tagReader = new XmlReader(wakeLock); tagWriter = new TagWriter(); packetCallbacks.clear(); - this.changeStatus(Account.STATUS_CONNECTING); + this.changeStatus(Account.State.CONNECTING); Bundle result = DNSHelper.getSRVRecord(account.getServer()); ArrayList<Parcelable> values = result.getParcelableArrayList("values"); if ("timeout".equals(result.getString("error"))) { - Log.d(Config.LOGTAG, account.getJid() + ": dns timeout"); - this.changeStatus(Account.STATUS_OFFLINE); + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": dns timeout"); + this.changeStatus(Account.State.OFFLINE); return; } else if (values != null) { int i = 0; @@ -154,18 +167,24 @@ public class XmppConnection implements Runnable { while (socketError && values.size() > i) { Bundle namePort = (Bundle) values.get(i); try { - String srvRecordServer = namePort.getString("name"); + String srvRecordServer; + try { + srvRecordServer=IDN.toASCII(namePort.getString("name")); + } catch (final IllegalArgumentException e) { + // TODO: Handle me?` + srvRecordServer = ""; + } int srvRecordPort = namePort.getInt("port"); String srvIpServer = namePort.getString("ipv4"); InetSocketAddress addr; if (srvIpServer != null) { addr = new InetSocketAddress(srvIpServer, srvRecordPort); - Log.d(Config.LOGTAG, account.getJid() + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": using values from dns " + srvRecordServer + "[" + srvIpServer + "]:" + srvRecordPort); } else { addr = new InetSocketAddress(srvRecordServer, srvRecordPort); - Log.d(Config.LOGTAG, account.getJid() + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": using values from dns " + srvRecordServer + ":" + srvRecordPort); } @@ -173,30 +192,30 @@ public class XmppConnection implements Runnable { socket.connect(addr, 20000); socketError = false; } catch (UnknownHostException e) { - Log.d(Config.LOGTAG, account.getJid() + ": " + e.getMessage()); + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage()); i++; } catch (IOException e) { - Log.d(Config.LOGTAG, account.getJid() + ": " + e.getMessage()); + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage()); i++; } } if (socketError) { - this.changeStatus(Account.STATUS_SERVER_NOT_FOUND); + this.changeStatus(Account.State.SERVER_NOT_FOUND); if (wakeLock.isHeld()) { try { wakeLock.release(); - } catch (RuntimeException re) { + } catch (final RuntimeException ignored) { } } return; } } else if (result.containsKey("error") && "nosrv".equals(result.getString("error", null))) { - socket = new Socket(account.getServer(), 5222); + socket = new Socket(account.getServer().getDomainpart(), 5222); } else { - Log.d(Config.LOGTAG, account.getJid() + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": timeout in DNS resolution"); - changeStatus(Account.STATUS_OFFLINE); + changeStatus(Account.State.OFFLINE); return; } OutputStream out = socket.getOutputStream(); @@ -220,45 +239,32 @@ public class XmppConnection implements Runnable { socket.close(); } } catch (UnknownHostException e) { - this.changeStatus(Account.STATUS_SERVER_NOT_FOUND); + this.changeStatus(Account.State.SERVER_NOT_FOUND); if (wakeLock.isHeld()) { try { wakeLock.release(); - } catch (RuntimeException re) { + } catch (final RuntimeException ignored) { } } - return; - } catch (IOException e) { - Log.d(Config.LOGTAG, account.getJid() + ": " + e.getMessage()); - this.changeStatus(Account.STATUS_OFFLINE); + } catch (final IOException | XmlPullParserException e) { + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage()); + this.changeStatus(Account.State.OFFLINE); if (wakeLock.isHeld()) { try { wakeLock.release(); - } catch (RuntimeException re) { + } catch (final RuntimeException ignored) { } } - return; } catch (NoSuchAlgorithmException e) { - Log.d(Config.LOGTAG, account.getJid() + ": " + e.getMessage()); - this.changeStatus(Account.STATUS_OFFLINE); + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage()); + this.changeStatus(Account.State.OFFLINE); Log.d(Config.LOGTAG, "compression exception " + e.getMessage()); if (wakeLock.isHeld()) { try { wakeLock.release(); - } catch (RuntimeException re) { + } catch (final RuntimeException ignored) { } } - return; - } catch (XmlPullParserException e) { - Log.d(Config.LOGTAG, account.getJid() + ": " + e.getMessage()); - this.changeStatus(Account.STATUS_OFFLINE); - if (wakeLock.isHeld()) { - try { - wakeLock.release(); - } catch (RuntimeException re) { - } - } - return; } } @@ -268,137 +274,151 @@ public class XmppConnection implements Runnable { 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, mXmppConnectionService.getRNG())); - 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)); + private void processStream(final 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")) { + final String challenge = tagReader.readElement(nextTag).getContent(); + try { + saslMechanism.getResponse(challenge); + } catch (final SaslMechanism.AuthenticationException e) { + disconnect(true); + Log.e(Config.LOGTAG, String.valueOf(e)); + } + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": logged in"); + account.setKey(Account.PINNED_MECHANISM_KEY, + String.valueOf(saslMechanism.getPriority())); + tagReader.reset(); + sendStartStream(); + processStream(tagReader.readTag()); + break; + } else if (nextTag.isStart("failure")) { + tagReader.readElement(nextTag); + changeStatus(Account.State.UNAUTHORIZED); + } else if (nextTag.isStart("challenge")) { + final String challenge = tagReader.readElement(nextTag).getContent(); + final Element response = new Element("response"); + response.setAttribute("xmlns", + "urn:ietf:params:xml:ns:xmpp-sasl"); + try { + response.setContent(saslMechanism.getResponse(challenge)); + } catch (final SaslMechanism.AuthenticationException e) { + // TODO: Send auth abort tag. + Log.e(Config.LOGTAG, e.toString()); + } + tagWriter.writeElement(response); + } else if (nextTag.isStart("enabled")) { + Element enabled = tagReader.readElement(nextTag); + if ("true".equals(enabled.getAttribute("resume"))) { + this.streamId = enabled.getAttribute("id"); + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + + ": stream managment(" + smVersion + + ") enabled (resumable)"); + } else { + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + + ": stream 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().toBareJid().toString() + + ": session resumed with lost packages"); + stanzasSent = serverCount; + } else { + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + + ": 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 (final NumberFormatException ignored) { + + } + sendServiceDiscoveryInfo(account.getServer()); + sendServiceDiscoveryItems(account.getServer()); + 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().toBareJid().toString() + ": resumption failed"); + streamId = null; + if (account.getStatus() != Account.State.ONLINE) { + sendBindRequest(); + } + } else if (nextTag.isStart("iq")) { + processIq(nextTag); + } else if (nextTag.isStart("message")) { + processMessage(nextTag); + } else if (nextTag.isStart("presence")) { + processPresence(nextTag); + } + nextTag = tagReader.readTag(); + } + if (account.getStatus() == Account.State.ONLINE) { + account. setStatus(Account.State.OFFLINE); + if (statusListener != null) { + statusListener.onStatusChanged(account); } } - } - 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"); + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": sending intial ping"); IqPacket iq = new IqPacket(IqPacket.TYPE_GET); - iq.setFrom(account.getFullJid()); + iq.setFrom(account.getJid()); 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() + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": online with resource " + account.getResource()); - changeStatus(Account.STATUS_ONLINE); + changeStatus(Account.State.ONLINE); } }); } private Element processPacket(Tag currentTag, int packetType) - throws XmlPullParserException, IOException { + throws XmlPullParserException, IOException { Element element; switch (packetType) { case PACKET_IQ: @@ -425,10 +445,10 @@ public class XmppConnection implements Runnable { if (packetType == PACKET_IQ && "jingle".equals(child.getName()) && ("set".equalsIgnoreCase(type) || "get" - .equalsIgnoreCase(type))) { + .equalsIgnoreCase(type))) { element = new JinglePacket(); element.setAttributes(currentTag.getAttributes()); - } + } element.addChild(child); } nextTag = tagReader.readTag(); @@ -442,64 +462,64 @@ public class XmppConnection implements Runnable { } private void processIq(Tag currentTag) throws XmlPullParserException, - IOException { - IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ); + 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); - } + if (packet.getId() == null) { + return; // an iq packet without id is definitely invalid + } - packetCallbacks.remove(packet.getId()); - } else if ((packet.getType() == IqPacket.TYPE_GET || packet - .getType() == IqPacket.TYPE_SET) - && this.unregisteredIqListener != null) { - this.unregisteredIqListener.onIqPacketReceived(account, packet); - } - } + 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); - } + 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); - } + 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 { @@ -509,19 +529,19 @@ public class XmppConnection implements Runnable { 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 switchOverToZLib(final 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().toBareJid() + ": compression enabled"); + enabledCompression = true; + processStream(tagReader.readTag()); } private void sendStartTLS() throws IOException { @@ -532,119 +552,124 @@ public class XmppConnection implements Runnable { private SharedPreferences getPreferences() { return PreferenceManager - .getDefaultSharedPreferences(applicationContext); + .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.mXmppConnectionService.getMemorizingTrustManager()}, - mXmppConnectionService.getRNG()); - SSLSocketFactory factory = sc.getSocketFactory(); - - if (factory == null) { - throw new IOException("SSLSocketFactory was null"); - } - - HostnameVerifier verifier = this.mXmppConnectionService.getMemorizingTrustManager().wrapHostnameVerifier(new StrictHostnameVerifier()); - - if (socket == null || socket.isClosed()) { - throw new IOException("socket null or closed"); - } - final InetAddress address = socket.getInetAddress(); - if (address == null) { - throw new IOException("socket address was null"); - } - - final SSLSocket sslSocket = (SSLSocket) factory.createSocket(socket,address.getHostAddress(), socket.getPort(),true); + private void switchOverToTls(final Tag currentTag) throws XmlPullParserException, + IOException { + tagReader.readTag(); + try { + SSLContext sc = SSLContext.getInstance("TLS"); + sc.init(null, + new X509TrustManager[]{this.mXmppConnectionService.getMemorizingTrustManager()}, + mXmppConnectionService.getRNG()); + SSLSocketFactory factory = sc.getSocketFactory(); + + if (factory == null) { + throw new IOException("SSLSocketFactory was null"); + } - // 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); + final HostnameVerifier verifier = this.mXmppConnectionService.getMemorizingTrustManager().wrapHostnameVerifier(new StrictHostnameVerifier()); - if (verifier != null - && !verifier.verify(account.getServer(), - sslSocket.getSession())) { - sslSocket.close(); - throw new IOException("host mismatch in TLS connection"); - } - 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(); - } - } + if (socket == null || socket.isClosed()) { + throw new IOException("socket null or closed"); + } + final InetAddress address = socket.getInetAddress(); + if (address == null) { + throw new IOException("socket address was null"); + } - 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); - } + final SSLSocket sslSocket = (SSLSocket) factory.createSocket(socket,address.getHostAddress(), socket.getPort(),true); - 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); + // 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<>( + 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().getDomainpart(), + sslSocket.getSession())) { + Log.d(Config.LOGTAG,account.getJid().toBareJid()+": TLS certificate verification failed"); + disconnect(true); + changeStatus(Account.State.SECURITY_ERROR); + } + tagReader.setInputStream(sslSocket.getInputStream()); + tagWriter.setOutputStream(sslSocket.getOutputStream()); + sendStartStream(); + Log.d(Config.LOGTAG, account.getJid().toBareJid() + + ": TLS connection established"); + enabledEncryption = true; + processStream(tagReader.readTag()); + sslSocket.close(); + } catch (final NoSuchAlgorithmException | KeyManagementException e1) { + e1.printStackTrace(); + } } private void processStreamFeatures(Tag currentTag) - throws XmlPullParserException, IOException { + throws XmlPullParserException, IOException { this.streamFeatures = tagReader.readElement(currentTag); - if (this.streamFeatures.hasChild("starttls") && !usingEncryption) { + if (this.streamFeatures.hasChild("starttls") && !enabledEncryption) { sendStartTLS(); } else if (compressionAvailable()) { sendCompressionZlib(); } else if (this.streamFeatures.hasChild("register") && account.isOptionSet(Account.OPTION_REGISTER) - && usingEncryption) { + && enabledEncryption) { sendRegistryRequest(); } else if (!this.streamFeatures.hasChild("register") && account.isOptionSet(Account.OPTION_REGISTER)) { - changeStatus(Account.STATUS_REGISTRATION_NOT_SUPPORTED); + changeStatus(Account.State.REGISTRATION_NOT_SUPPORTED); disconnect(true); } else if (this.streamFeatures.hasChild("mechanisms") - && shouldAuthenticate && usingEncryption) { - List<String> mechanisms = extractMechanisms(streamFeatures + && shouldAuthenticate && enabledEncryption) { + final List<String> mechanisms = extractMechanisms(streamFeatures .findChild("mechanisms")); - if (mechanisms.contains("PLAIN")) { - sendSaslAuthPlain(); + final Element auth = new Element("auth"); + auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl"); + if (mechanisms.contains("SCRAM-SHA-1")) { + saslMechanism = new ScramSha1(tagWriter, account, mXmppConnectionService.getRNG()); } else if (mechanisms.contains("DIGEST-MD5")) { - sendSaslAuthDigestMd5(); + saslMechanism = new DigestMd5(tagWriter, account, mXmppConnectionService.getRNG()); + } else if (mechanisms.contains("PLAIN")) { + saslMechanism = new Plain(tagWriter, account); + } + final JSONObject keys = account.getKeys(); + try { + if (keys.has(Account.PINNED_MECHANISM_KEY) && + keys.getInt(Account.PINNED_MECHANISM_KEY) > saslMechanism.getPriority() ) { + Log.e(Config.LOGTAG, "Auth failed. Authentication mechanism " + saslMechanism.getMechanism() + + " has lower priority (" + String.valueOf(saslMechanism.getPriority()) + + ") than pinned priority (" + keys.getInt(Account.PINNED_MECHANISM_KEY) + + "). Possible downgrade attack?"); + disconnect(true); + changeStatus(Account.State.SECURITY_ERROR); + } + } catch (final JSONException e) { + Log.d(Config.LOGTAG, "Parse error while checking pinned auth mechanism"); } + Log.d(Config.LOGTAG,account.getJid().toString()+": Authenticating with " + saslMechanism.getMechanism()); + auth.setAttribute("mechanism", saslMechanism.getMechanism()); + if (!saslMechanism.getClientFirstMessage().isEmpty()) { + auth.setContent(saslMechanism.getClientFirstMessage()); + } + tagWriter.writeElement(auth); } else if (this.streamFeatures.hasChild("sm", "urn:xmpp:sm:" - + smVersion) + + smVersion) && streamId != null) { ResumePacket resume = new ResumePacket(this.streamId, stanzasReceived, smVersion); @@ -652,15 +677,14 @@ public class XmppConnection implements Runnable { } else if (this.streamFeatures.hasChild("bind") && shouldBind) { sendBindRequest(); } else { - Log.d(Config.LOGTAG, account.getJid() - + ": incompatible server. disconnecting"); disconnect(true); + changeStatus(Account.State.INCOMPATIBLE_SERVER); } } private boolean compressionAvailable() { if (!this.streamFeatures.hasChild("compression", - "http://jabber.org/features/compress")) + "http://jabber.org/features/compress")) return false; if (!ZLibOutputStream.SUPPORTED) return false; @@ -681,7 +705,7 @@ public class XmppConnection implements Runnable { } private List<String> extractMechanisms(Element stream) { - ArrayList<String> mechanisms = new ArrayList<String>(stream + ArrayList<String> mechanisms = new ArrayList<>(stream .getChildren().size()); for (Element child : stream.getChildren()) { mechanisms.add(child.getContent()); @@ -702,35 +726,35 @@ public class XmppConnection implements Runnable { && (packet.query().hasChild("password"))) { IqPacket register = new IqPacket(IqPacket.TYPE_SET); Element username = new Element("username") - .setContent(account.getUsername()); + .setContent(account.getUsername()); Element password = new Element("password") - .setContent(account.getPassword()); + .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) { + IqPacket packet) { if (packet.getType() == IqPacket.TYPE_RESULT) { account.setOption(Account.OPTION_REGISTER, false); - changeStatus(Account.STATUS_REGISTRATION_SUCCESSFULL); + changeStatus(Account.State.REGISTRATION_SUCCESSFUL); } else if (packet.hasChild("error") && (packet.findChild("error") - .hasChild("conflict"))) { - changeStatus(Account.STATUS_REGISTRATION_CONFLICT); + .hasChild("conflict"))) { + changeStatus(Account.State.REGISTRATION_CONFLICT); } else { - changeStatus(Account.STATUS_REGISTRATION_FAILED); + changeStatus(Account.State.REGISTRATION_FAILED); Log.d(Config.LOGTAG, packet.toString()); } disconnect(true); } }); } else { - changeStatus(Account.STATUS_REGISTRATION_FAILED); + changeStatus(Account.State.REGISTRATION_FAILED); disconnect(true); - Log.d(Config.LOGTAG, account.getJid() + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not register. instructions are" + instructions.getContent()); } @@ -741,15 +765,19 @@ public class XmppConnection implements Runnable { 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()); + .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"); + final Element jid = bind.findChild("jid"); if (jid != null && jid.getContent() != null) { - account.setResource(jid.getContent().split("/", 2)[1]); + try { + account.setResource(Jid.fromString(jid.getContent()).getResourcepart()); + } catch (final InvalidJidException e) { + // TODO: Handle the case where an external JID is technically invalid? + } if (streamFeatures.hasChild("sm", "urn:xmpp:sm:3")) { smVersion = 3; EnablePacket enable = new EnablePacket(smVersion); @@ -757,13 +785,15 @@ public class XmppConnection implements Runnable { stanzasSent = 0; messageReceipts.clear(); } else if (streamFeatures.hasChild("sm", - "urn:xmpp:sm:2")) { + "urn:xmpp:sm:2")) { smVersion = 2; EnablePacket enable = new EnablePacket(smVersion); tagWriter.writeStanzaAsync(enable); stanzasSent = 0; messageReceipts.clear(); } + enabledCarbons = false; + disco.clear(); sendServiceDiscoveryInfo(account.getServer()); sendServiceDiscoveryItems(account.getServer()); if (bindListener != null) { @@ -779,7 +809,7 @@ public class XmppConnection implements Runnable { } }); if (this.streamFeatures.hasChild("session")) { - Log.d(Config.LOGTAG, account.getJid() + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": sending deprecated session"); IqPacket startSession = new IqPacket(IqPacket.TYPE_SET); startSession.addChild("session", @@ -788,49 +818,61 @@ public class XmppConnection implements Runnable { } } - 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() { + private void sendServiceDiscoveryInfo(final Jid server) { + if (disco.containsKey(server.toDomainJid().toString())) { + if (account.getServer().equals(server.toDomainJid())) { + enableAdvancedStreamFeatures(); + } + } else { + final IqPacket iq = new IqPacket(IqPacket.TYPE_GET); + iq.setTo(server.toDomainJid()); + 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")); + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + final List<Element> elements = packet.query().getChildren(); + final List<String> features = new ArrayList<>(); + for (Element element : elements) { + if (element.getName().equals("feature")) { + features.add(element.getAttribute("var")); + } } - } - disco.put(server, features); + disco.put(server.toDomainJid().toString(), features); - if (account.getServer().equals(server)) { - enableAdvancedStreamFeatures(); + if (account.getServer().equals(server.toDomainJid())) { + enableAdvancedStreamFeatures(); + } } - } - }); + }); + } } private void enableAdvancedStreamFeatures() { if (getFeatures().carbons()) { - sendEnableCarbons(); + if (!enabledCarbons) { + sendEnableCarbons(); + } } } - private void sendServiceDiscoveryItems(final String server) { - IqPacket iq = new IqPacket(IqPacket.TYPE_GET); - iq.setTo(server); + private void sendServiceDiscoveryItems(final Jid server) { + final IqPacket iq = new IqPacket(IqPacket.TYPE_GET); + iq.setTo(server.toDomainJid()); iq.query("http://jabber.org/protocol/disco#items"); 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); + for (Element element : elements) { + if (element.getName().equals("item")) { + final String jid = element.getAttribute("jid"); + try { + sendServiceDiscoveryInfo(Jid.fromString(jid).toDomainJid()); + } catch (final InvalidJidException ignored) { + // TODO: Handle the case where an external JID is technically invalid? + } } } } @@ -845,10 +887,11 @@ public class XmppConnection implements Runnable { @Override public void onIqPacketReceived(Account account, IqPacket packet) { if (!packet.hasChild("error")) { - Log.d(Config.LOGTAG, account.getJid() + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": successfully enabled carbons"); + enabledCarbons = true; } else { - Log.d(Config.LOGTAG, account.getJid() + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": error enableing carbons " + packet.toString()); } } @@ -856,21 +899,21 @@ public class XmppConnection implements Runnable { } private void processStreamError(Tag currentTag) - throws XmlPullParserException, IOException { + throws XmlPullParserException, IOException { Element streamError = tagReader.readElement(currentTag); if (streamError != null && streamError.hasChild("conflict")) { - String resource = account.getResource().split("\\.")[0]; + final String resource = account.getResource().split("\\.")[0]; account.setResource(resource + "." + nextRandomId()); Log.d(Config.LOGTAG, - account.getJid() + ": switching resource due to conflict (" - + account.getResource() + ")"); + account.getJid().toBareJid() + ": 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("from", account.getJid().toBareJid().toString()); + stream.setAttribute("to", account.getServer().toString()); stream.setAttribute("version", "1.0"); stream.setAttribute("xml:lang", "en"); stream.setAttribute("xmlns", "jabber:client"); @@ -887,7 +930,7 @@ public class XmppConnection implements Runnable { String id = nextRandomId(); packet.setAttribute("id", id); } - packet.setFrom(account.getFullJid()); + packet.setFrom(account.getJid()); this.sendPacket(packet, callback); } @@ -908,11 +951,11 @@ public class XmppConnection implements Runnable { } private synchronized void sendPacket(final AbstractStanza packet, - PacketReceived callback) { + 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) { @@ -920,7 +963,7 @@ public class XmppConnection implements Runnable { + stanzasSent); this.messageReceipts.put(stanzasSent, packet.getId()); tagWriter.writeStanzaAsync(new RequestPacket(this.smVersion)); - } + } if (callback != null) { if (packet.getId() == null) { packet.setId(nextRandomId()); @@ -934,7 +977,7 @@ public class XmppConnection implements Runnable { tagWriter.writeStanzaAsync(new RequestPacket(smVersion)); } else { IqPacket iq = new IqPacket(IqPacket.TYPE_GET); - iq.setFrom(account.getFullJid()); + iq.setFrom(account.getJid()); iq.addChild("ping", "urn:xmpp:ping"); this.sendIqPacket(iq, null); } @@ -944,22 +987,22 @@ public class XmppConnection implements Runnable { 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; @@ -974,7 +1017,7 @@ public class XmppConnection implements Runnable { } public void disconnect(boolean force) { - Log.d(Config.LOGTAG, account.getJid() + ": disconnecting"); + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": disconnecting"); try { if (force) { socket.close(); @@ -1008,7 +1051,7 @@ public class XmppConnection implements Runnable { } public List<String> findDiscoItemsByFeature(String feature) { - List<String> items = new ArrayList<String>(); + final List<String> items = new ArrayList<>(); for (Entry<String, List<String>> cursor : disco.entrySet()) { if (cursor.getValue().contains(feature)) { items.add(cursor.getKey()); @@ -1084,11 +1127,9 @@ public class XmppConnection implements Runnable { this.connection = connection; } - private boolean hasDiscoFeature(String server, String feature) { - if (!connection.disco.containsKey(server)) { - return false; - } - return connection.disco.get(server).contains(feature); + private boolean hasDiscoFeature(final Jid server, final String feature) { + return connection.disco.containsKey(server.toDomainJid().toString()) && + connection.disco.get(server.toDomainJid().toString()).contains(feature); } public boolean carbons() { @@ -1100,12 +1141,7 @@ public class XmppConnection implements Runnable { } public boolean csi() { - if (connection.streamFeatures == null) { - return false; - } else { - return connection.streamFeatures.hasChild("csi", - "urn:xmpp:csi:0"); - } + return connection.streamFeatures != null && connection.streamFeatures.hasChild("csi", "urn:xmpp:csi:0"); } public boolean pubsub() { @@ -1118,20 +1154,16 @@ public class XmppConnection implements Runnable { } public boolean rosterVersioning() { - if (connection.streamFeatures == null) { - return false; - } else { - return connection.streamFeatures.hasChild("ver"); - } + return connection.streamFeatures != null && connection.streamFeatures.hasChild("ver"); } public boolean streamhost() { return connection - .findDiscoItemByFeature("http://jabber.org/protocol/bytestreams") != null; + .findDiscoItemByFeature("http://jabber.org/protocol/bytestreams") != null; } public boolean compression() { - return connection.usingCompression; + return connection.enabledCompression; } } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jid/InvalidJidException.java b/src/main/java/eu/siacs/conversations/xmpp/jid/InvalidJidException.java new file mode 100644 index 00000000..f1855263 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jid/InvalidJidException.java @@ -0,0 +1,48 @@ +package eu.siacs.conversations.xmpp.jid; + +public class InvalidJidException extends Exception { + + // This is probably not the "Java way", but the "Java way" means we'd have a ton of extra tiny, + // annoying classes floating around. I like this. + public final static String INVALID_LENGTH = "JID must be between 0 and 3071 characters"; + public final static String INVALID_PART_LENGTH = "JID part must be between 0 and 1023 characters"; + public final static String INVALID_CHARACTER = "JID contains an invalid character"; + public final static String STRINGPREP_FAIL = "The STRINGPREP operation has failed for the given JID"; + + /** + * Constructs a new {@code Exception} that includes the current stack trace. + */ + public InvalidJidException() { + } + + /** + * Constructs a new {@code Exception} with the current stack trace and the + * specified detail message. + * + * @param detailMessage the detail message for this exception. + */ + public InvalidJidException(final String detailMessage) { + super(detailMessage); + } + + /** + * Constructs a new {@code Exception} with the current stack trace, the + * specified detail message and the specified cause. + * + * @param detailMessage the detail message for this exception. + * @param throwable the cause of this exception. + */ + public InvalidJidException(final String detailMessage, final Throwable throwable) { + super(detailMessage, throwable); + } + + /** + * Constructs a new {@code Exception} with the current stack trace and the + * specified cause. + * + * @param throwable the cause of this exception. + */ + public InvalidJidException(final Throwable throwable) { + super(throwable); + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jid/Jid.java b/src/main/java/eu/siacs/conversations/xmpp/jid/Jid.java new file mode 100644 index 00000000..ebf8a6ed --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jid/Jid.java @@ -0,0 +1,191 @@ +package eu.siacs.conversations.xmpp.jid; + +import net.java.otr4j.session.SessionID; + +import java.net.IDN; + +import gnu.inet.encoding.Stringprep; +import gnu.inet.encoding.StringprepException; + +/** + * The `Jid' class provides an immutable representation of a JID. + */ +public final class Jid { + + private final String localpart; + private final String domainpart; + private final String resourcepart; + + // It's much more efficient to store the ful JID as well as the parts instead of figuring them + // all out every time (since some characters are displayed but aren't used for comparisons). + private final String displayjid; + + public String getLocalpart() { + return localpart; + } + + public String getDomainpart() { + return IDN.toUnicode(domainpart); + } + + public String getResourcepart() { + return resourcepart; + } + + public static Jid fromSessionID(SessionID id) throws InvalidJidException{ + if (id.getUserID().isEmpty()) { + return Jid.fromString(id.getAccountID()); + } else { + return Jid.fromString(id.getAccountID()+"/"+id.getUserID()); + } + } + + public static Jid fromString(final String jid) throws InvalidJidException { + return new Jid(jid); + } + + public static Jid fromParts(final String localpart, + final String domainpart, + final String resourcepart) throws InvalidJidException { + String out; + if (localpart == null || localpart.isEmpty()) { + out = domainpart; + } else { + out = localpart + "@" + domainpart; + } + if (resourcepart != null && !resourcepart.isEmpty()) { + out = out + "/" + resourcepart; + } + return new Jid(out); + } + + private Jid(final String jid) throws InvalidJidException { + // Hackish Android way to count the number of chars in a string... should work everywhere. + final int atCount = jid.length() - jid.replace("@", "").length(); + final int slashCount = jid.length() - jid.replace("/", "").length(); + + // Throw an error if there's anything obvious wrong with the JID... + if (jid.isEmpty() || jid.length() > 3071) { + throw new InvalidJidException(InvalidJidException.INVALID_LENGTH); + } + if (atCount > 1 || slashCount > 1 || + jid.startsWith("@") || jid.endsWith("@") || + jid.startsWith("/") || jid.endsWith("/")) { + throw new InvalidJidException(InvalidJidException.INVALID_CHARACTER); + } + + String finaljid; + + final int domainpartStart; + if (atCount == 1) { + final int atLoc = jid.indexOf("@"); + final String lp = jid.substring(0, atLoc); + try { + localpart = Stringprep.nodeprep(lp); + } catch (final StringprepException e) { + throw new InvalidJidException(InvalidJidException.STRINGPREP_FAIL, e); + } + if (localpart.isEmpty() || localpart.length() > 1023) { + throw new InvalidJidException(InvalidJidException.INVALID_PART_LENGTH); + } + domainpartStart = atLoc + 1; + finaljid = lp + "@"; + } else { + localpart = ""; + finaljid = ""; + domainpartStart = 0; + } + + final String dp; + if (slashCount == 1) { + final int slashLoc = jid.indexOf("/"); + final String rp = jid.substring(slashLoc + 1, jid.length()); + try { + resourcepart = Stringprep.resourceprep(rp); + } catch (final StringprepException e) { + throw new InvalidJidException(InvalidJidException.STRINGPREP_FAIL, e); + } + if (resourcepart.isEmpty() || resourcepart.length() > 1023) { + throw new InvalidJidException(InvalidJidException.INVALID_PART_LENGTH); + } + dp = IDN.toUnicode(jid.substring(domainpartStart, slashLoc), IDN.USE_STD3_ASCII_RULES); + finaljid = finaljid + dp + "/" + rp; + } else { + resourcepart = ""; + dp = IDN.toUnicode(jid.substring(domainpartStart, jid.length()), + IDN.USE_STD3_ASCII_RULES); + finaljid = finaljid + dp; + } + + // Remove trailing "." before storing the domain part. + if (dp.endsWith(".")) { + try { + domainpart = IDN.toASCII(dp.substring(0, dp.length() - 1), IDN.USE_STD3_ASCII_RULES); + } catch (final IllegalArgumentException e) { + throw new InvalidJidException(e); + } + } else { + try { + domainpart = IDN.toASCII(dp, IDN.USE_STD3_ASCII_RULES); + } catch (final IllegalArgumentException e) { + throw new InvalidJidException(e); + } + } + + // TODO: Find a proper domain validation library; validate individual parts, separators, etc. + if (domainpart.isEmpty() || domainpart.length() > 1023) { + throw new InvalidJidException(InvalidJidException.INVALID_PART_LENGTH); + } + + this.displayjid = finaljid; + } + + public Jid toBareJid() { + try { + return resourcepart.isEmpty() ? this : fromParts(localpart, domainpart, ""); + } catch (final InvalidJidException e) { + // This should never happen. + return null; + } + } + + public Jid toDomainJid() { + try { + return resourcepart.isEmpty() && localpart.isEmpty() ? this : fromString(getDomainpart()); + } catch (final InvalidJidException e) { + // This should never happen. + return null; + } + } + + @Override + public String toString() { + return displayjid; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final Jid jid = (Jid) o; + + return jid.hashCode() == this.hashCode(); + } + + @Override + public int hashCode() { + int result = localpart.hashCode(); + result = 31 * result + domainpart.hashCode(); + result = 31 * result + resourcepart.hashCode(); + return result; + } + + public boolean hasLocalpart() { + return !localpart.isEmpty(); + } + + public boolean isBareJid() { + return this.resourcepart.isEmpty(); + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java index 3e7c7b68..281ea3ca 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java @@ -4,6 +4,7 @@ import java.util.ArrayList; import java.util.List; import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.jid.Jid; public class JingleCandidate { @@ -17,7 +18,7 @@ public class JingleCandidate { private String host; private int port; private int type; - private String jid; + private Jid jid; private int priority; public JingleCandidate(String cid, boolean ours) { @@ -37,11 +38,11 @@ public class JingleCandidate { return this.host; } - public void setJid(String jid) { + public void setJid(final Jid jid) { this.jid = jid; } - public String getJid() { + public Jid getJid() { return this.jid; } @@ -58,13 +59,17 @@ public class JingleCandidate { } 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; - } + switch (type) { + case "proxy": + this.type = TYPE_PROXY; + break; + case "direct": + this.type = TYPE_DIRECT; + break; + default: + this.type = TYPE_UNKNOWN; + break; + } } public void setPriority(int i) { @@ -93,7 +98,7 @@ public class JingleCandidate { } public static List<JingleCandidate> parse(List<Element> canditates) { - List<JingleCandidate> parsedCandidates = new ArrayList<JingleCandidate>(); + List<JingleCandidate> parsedCandidates = new ArrayList<>(); for (Element c : canditates) { parsedCandidates.add(JingleCandidate.parse(c)); } @@ -104,7 +109,7 @@ public class JingleCandidate { JingleCandidate parsedCandidate = new JingleCandidate( candidate.getAttribute("cid"), false); parsedCandidate.setHost(candidate.getAttribute("host")); - parsedCandidate.setJid(candidate.getAttribute("jid")); + parsedCandidate.setJid(candidate.getAttributeAsJid("jid")); parsedCandidate.setType(candidate.getAttribute("type")); parsedCandidate.setPriority(Integer.parseInt(candidate .getAttribute("priority"))); @@ -118,7 +123,7 @@ public class JingleCandidate { element.setAttribute("cid", this.getCid()); element.setAttribute("host", this.getHost()); element.setAttribute("port", Integer.toString(this.getPort())); - element.setAttribute("jid", this.getJid()); + element.setAttribute("jid", this.getJid().toString()); element.setAttribute("priority", Integer.toString(this.getPriority())); if (this.getType() == TYPE_DIRECT) { element.setAttribute("type", "direct"); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java index 6b9ca9aa..3a1ba778 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java @@ -1,5 +1,6 @@ package eu.siacs.conversations.xmpp.jingle; +import java.net.URLConnection; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; @@ -9,18 +10,20 @@ import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; import android.content.Intent; -import android.graphics.BitmapFactory; import android.net.Uri; +import android.os.SystemClock; 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.DownloadablePlaceholder; 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.jid.Jid; import eu.siacs.conversations.xmpp.jingle.stanzas.Content; import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; import eu.siacs.conversations.xmpp.jingle.stanzas.Reason; @@ -28,9 +31,6 @@ 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; @@ -45,14 +45,14 @@ public class JingleConnection implements Downloadable { private int ibbBlockSize = 4096; private int mJingleStatus = -1; - private int mStatus = -1; + private int mStatus = Downloadable.STATUS_UNKNOWN; 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 Jid initiator; + private Jid responder; + private List<JingleCandidate> candidates = new ArrayList<>(); + private ConcurrentHashMap<String, JingleSocks5Transport> connections = new ConcurrentHashMap<>(); private String transportId; private Element fileOffer; @@ -61,6 +61,9 @@ public class JingleConnection implements Downloadable { private String contentName; private String contentCreator; + private int mProgress = 0; + private long mLastGuiRefresh = 0; + private boolean receivedCandidate = false; private boolean sentCandidate = false; @@ -73,7 +76,7 @@ public class JingleConnection implements Downloadable { @Override public void onIqPacketReceived(Account account, IqPacket packet) { if (packet.getType() == IqPacket.TYPE_ERROR) { - cancel(); + fail(); } } }; @@ -82,23 +85,21 @@ public class JingleConnection implements Downloadable { @Override public void onFileTransmitted(DownloadableFile file) { - if (responder.equals(account.getFullJid())) { + if (responder.equals(account.getJid())) { 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.getFileBackend().updateFileParams(message); mXmppConnectionService.databaseBackend.createMessage(message); mXmppConnectionService.markMessage(message, Message.STATUS_RECEIVED); + } else { + if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { + file.delete(); + } } Log.d(Config.LOGTAG, "sucessfully transmitted file:" + file.getAbsolutePath()); @@ -113,7 +114,7 @@ public class JingleConnection implements Downloadable { @Override public void onFileTransferAborted() { JingleConnection.this.sendCancel(); - JingleConnection.this.cancel(); + JingleConnection.this.fail(); } }; @@ -121,7 +122,7 @@ public class JingleConnection implements Downloadable { @Override public void success() { - if (initiator.equals(account.getFullJid())) { + if (initiator.equals(account.getJid())) { Log.d(Config.LOGTAG, "we were initiating. sending file"); transport.send(file, onFileTransmissionSatusChanged); } else { @@ -150,7 +151,7 @@ public class JingleConnection implements Downloadable { return this.account; } - public String getCounterPart() { + public Jid getCounterPart() { return this.message.getCounterpart(); } @@ -160,14 +161,14 @@ public class JingleConnection implements Downloadable { Reason reason = packet.getReason(); if (reason != null) { if (reason.hasChild("cancel")) { - this.cancel(); + this.fail(); } else if (reason.hasChild("success")) { this.receiveSuccess(); } else { - this.cancel(); + this.fail(); } } else { - this.cancel(); + this.fail(); } } else if (packet.isAction("session-accept")) { returnResult = receiveAccept(packet); @@ -202,8 +203,10 @@ public class JingleConnection implements Downloadable { this.contentCreator = "initiator"; this.contentName = this.mJingleConnectionManager.nextRandomId(); this.message = message; + this.message.setDownloadable(this); + this.mStatus = Downloadable.STATUS_UPLOADING; this.account = message.getConversation().getAccount(); - this.initiator = this.account.getFullJid(); + this.initiator = this.account.getJid(); this.responder = this.message.getCounterpart(); this.sessionId = this.mJingleConnectionManager.nextRandomId(); if (this.candidates.size() > 0) { @@ -254,17 +257,16 @@ public class JingleConnection implements Downloadable { this.mJingleStatus = JINGLE_STATUS_INITIATED; Conversation conversation = this.mXmppConnectionService .findOrCreateConversation(account, - packet.getFrom().split("/", 2)[0], false); + packet.getFrom().toBareJid(), 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]); + final Jid from = packet.getFrom(); + this.message.setCounterpart(from); this.account = account; this.initiator = packet.getFrom(); - this.responder = this.account.getFullJid(); + this.responder = this.account.getJid(); this.sessionId = packet.getSessionId(); Content content = packet.getJingleContent(); this.contentCreator = content.getAttribute("creator"); @@ -277,75 +279,83 @@ public class JingleConnection implements Downloadable { 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( + if (Arrays.asList(VALID_IMAGE_EXTENSIONS).contains( filename[filename.length - 1])) { - supportedFile = true; - } else if (Arrays.asList(this.cryptoExtensions).contains( + message.setType(Message.TYPE_IMAGE); + } else if (Arrays.asList(VALID_CRYPTO_EXTENSIONS).contains( filename[filename.length - 1])) { if (filename.length == 3) { - if (Arrays.asList(this.extensions).contains( + if (Arrays.asList(VALID_IMAGE_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); - } + message.setType(Message.TYPE_IMAGE); + } else { + message.setType(Message.TYPE_FILE); + } + if (filename[filename.length - 1].equals("otr")) { + message.setEncryption(Message.ENCRYPTION_OTR); + } else { + message.setEncryption(Message.ENCRYPTION_PGP); } } + } else { + message.setType(Message.TYPE_FILE); } - 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); + if (message.getType() == Message.TYPE_FILE) { + String suffix = ""; + if (!fileNameElement.getContent().isEmpty()) { + String parts[] = fileNameElement.getContent().split("/"); + suffix = parts[parts.length - 1]; + if (message.getEncryption() == Message.ENCRYPTION_OTR && suffix.endsWith(".otr")) { + suffix = suffix.substring(0,suffix.length() - 4); + } else if (message.getEncryption() == Message.ENCRYPTION_PGP && (suffix.endsWith(".pgp") || suffix.endsWith(".gpg"))) { + suffix = suffix.substring(0,suffix.length() - 4); } } - this.file.setExpectedSize(size); + message.setRelativeFilePath(message.getUuid()+"_"+suffix); + } + 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 { - this.sendCancel(); - this.cancel(); + 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.fail(); + return; + } else { + this.file.setKey(key); + } + } + this.file.setExpectedSize(size); } else { this.sendCancel(); - this.cancel(); + this.fail(); } } else { this.sendCancel(); - this.cancel(); + this.fail(); } } @@ -353,7 +363,7 @@ public class JingleConnection implements Downloadable { this.mXmppConnectionService.markMessage(this.message, Message.STATUS_OFFERED); JinglePacket packet = this.bootstrapPacket("session-initiate"); Content content = new Content(this.contentCreator, this.contentName); - if (message.getType() == Message.TYPE_IMAGE) { + if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) { content.setTransportId(this.transportId); this.file = this.mXmppConnectionService.getFileBackend().getFile( message, false); @@ -375,7 +385,7 @@ public class JingleConnection implements Downloadable { } private List<Element> getCandidatesAsElements() { - List<Element> elements = new ArrayList<Element>(); + List<Element> elements = new ArrayList<>(); for (JingleCandidate c : this.candidates) { elements.add(c.toElement()); } @@ -443,7 +453,7 @@ public class JingleConnection implements Downloadable { private JinglePacket bootstrapPacket(String action) { JinglePacket packet = new JinglePacket(); packet.setAction(action); - packet.setFrom(account.getFullJid()); + packet.setFrom(account.getJid()); packet.setTo(this.message.getCounterpart()); packet.setSessionId(this.sessionId); packet.setInitiator(this.initiator); @@ -484,7 +494,7 @@ public class JingleConnection implements Downloadable { } else { Log.d(Config.LOGTAG, "activated connection not found"); this.sendCancel(); - this.cancel(); + this.fail(); } } return true; @@ -531,8 +541,8 @@ public class JingleConnection implements Downloadable { this.transport = connection; if (connection == null) { Log.d(Config.LOGTAG, "could not find suitable candidate"); - this.disconnect(); - if (this.initiator.equals(account.getFullJid())) { + this.disconnectSocks5Connections(); + if (this.initiator.equals(account.getJid())) { this.sendFallbackToIbb(); } } else { @@ -547,7 +557,7 @@ public class JingleConnection implements Downloadable { activation.query("http://jabber.org/protocol/bytestreams") .setAttribute("sid", this.getSessionId()); activation.query().addChild("activate") - .setContent(this.getCounterPart()); + .setContent(this.getCounterPart().toString()); this.account.getXmppConnection().sendIqPacket(activation, new OnIqPacketReceived() { @@ -570,7 +580,7 @@ public class JingleConnection implements Downloadable { + " was a proxy. waiting for other party to activate"); } } else { - if (initiator.equals(account.getFullJid())) { + if (initiator.equals(account.getJid())) { Log.d(Config.LOGTAG, "we were initiating. sending file"); connection.send(file, onFileTransmissionSatusChanged); } else { @@ -600,7 +610,7 @@ public class JingleConnection implements Downloadable { } else if (connection.getCandidate().getPriority() == currentConnection .getCandidate().getPriority()) { // Log.d(Config.LOGTAG,"found two candidates with same priority"); - if (initiator.equals(account.getFullJid())) { + if (initiator.equals(account.getJid())) { if (currentConnection.getCandidate().isOurs()) { connection = currentConnection; } @@ -622,7 +632,7 @@ public class JingleConnection implements Downloadable { reason.addChild("success"); packet.setReason(reason); this.sendJinglePacket(packet); - this.disconnect(); + this.disconnectSocks5Connections(); this.mJingleStatus = JINGLE_STATUS_FINISHED; this.message.setStatus(Message.STATUS_RECEIVED); this.message.setDownloadable(null); @@ -653,8 +663,7 @@ public class JingleConnection implements Downloadable { } } this.transportId = packet.getJingleContent().getTransportId(); - this.transport = new JingleInbandTransport(this.account, - this.responder, this.transportId, this.ibbBlockSize); + this.transport = new JingleInbandTransport(this, this.transportId, this.ibbBlockSize); this.transport.receive(file, onFileTransmissionSatusChanged); JinglePacket answer = bootstrapPacket("transport-accept"); Content content = new Content("initiator", "a-file-offer"); @@ -676,8 +685,7 @@ public class JingleConnection implements Downloadable { this.ibbBlockSize = bs; } } - this.transport = new JingleInbandTransport(this.account, - this.responder, this.transportId, this.ibbBlockSize); + this.transport = new JingleInbandTransport(this, this.transportId, this.ibbBlockSize); this.transport.connect(new OnTransportConnected() { @Override @@ -701,20 +709,51 @@ public class JingleConnection implements Downloadable { this.mJingleStatus = JINGLE_STATUS_FINISHED; this.mXmppConnectionService.markMessage(this.message, Message.STATUS_SEND); - this.disconnect(); + this.disconnectSocks5Connections(); + if (this.transport != null && this.transport instanceof JingleInbandTransport) { + this.transport.disconnect(); + } + this.message.setDownloadable(null); this.mJingleConnectionManager.finishConnection(this); } public void cancel() { - this.mJingleStatus = JINGLE_STATUS_CANCELED; - this.disconnect(); + this.disconnectSocks5Connections(); + if (this.transport != null && this.transport instanceof JingleInbandTransport) { + this.transport.disconnect(); + } + this.sendCancel(); + this.mJingleConnectionManager.finishConnection(this); + if (this.responder.equals(account.getJid())) { + this.message.setDownloadable(new DownloadablePlaceholder(Downloadable.STATUS_FAILED)); + if (this.file!=null) { + file.delete(); + } + this.mXmppConnectionService.updateConversationUi(); + } else { + this.mXmppConnectionService.markMessage(this.message, + Message.STATUS_SEND_FAILED); + this.message.setDownloadable(null); + } + } + + private void fail() { + this.mJingleStatus = JINGLE_STATUS_FAILED; + this.disconnectSocks5Connections(); + if (this.transport != null && this.transport instanceof JingleInbandTransport) { + this.transport.disconnect(); + } if (this.message != null) { - if (this.responder.equals(account.getFullJid())) { - this.mStatus = Downloadable.STATUS_FAILED; + if (this.responder.equals(account.getJid())) { + this.message.setDownloadable(new DownloadablePlaceholder(Downloadable.STATUS_FAILED)); + if (this.file!=null) { + file.delete(); + } this.mXmppConnectionService.updateConversationUi(); } else { this.mXmppConnectionService.markMessage(this.message, Message.STATUS_SEND_FAILED); + this.message.setDownloadable(null); } } this.mJingleConnectionManager.finishConnection(this); @@ -763,7 +802,7 @@ public class JingleConnection implements Downloadable { }); } - private void disconnect() { + private void disconnectSocks5Connections() { Iterator<Entry<String, JingleSocks5Transport>> it = this.connections .entrySet().iterator(); while (it.hasNext()) { @@ -810,11 +849,11 @@ public class JingleConnection implements Downloadable { this.sendJinglePacket(packet); } - public String getInitiator() { + public Jid getInitiator() { return this.initiator; } - public String getResponder() { + public Jid getResponder() { return this.responder; } @@ -855,6 +894,14 @@ public class JingleConnection implements Downloadable { return null; } + public void updateProgress(int i) { + this.mProgress = i; + if (SystemClock.elapsedRealtime() - this.mLastGuiRefresh > Config.PROGRESS_UI_UPDATE_INTERVAL) { + this.mLastGuiRefresh = SystemClock.elapsedRealtime(); + mXmppConnectionService.updateConversationUi(); + } + } + interface OnProxyActivated { public void success(); @@ -870,7 +917,7 @@ public class JingleConnection implements Downloadable { } public boolean start() { - if (account.getStatus() == Account.STATUS_ONLINE) { + if (account.getStatus() == Account.State.ONLINE) { if (mJingleStatus == JINGLE_STATUS_INITIATED) { new Thread(new Runnable() { @@ -899,4 +946,29 @@ public class JingleConnection implements Downloadable { return 0; } } + + @Override + public int getProgress() { + return this.mProgress; + } + + @Override + public String getMimeType() { + if (this.message.getType() == Message.TYPE_FILE) { + String mime = null; + String path = this.message.getRelativeFilePath(); + if (path != null && !this.message.getRelativeFilePath().isEmpty()) { + mime = URLConnection.guessContentTypeFromName(this.message.getRelativeFilePath()); + if (mime!=null) { + return mime; + } else { + return ""; + } + } else { + return ""; + } + } else { + return "image/webp"; + } + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index d937146a..72c960d8 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -14,13 +14,15 @@ 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.jid.InvalidJidException; +import eu.siacs.conversations.xmpp.jid.Jid; import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; import eu.siacs.conversations.xmpp.stanzas.IqPacket; public class JingleConnectionManager extends AbstractConnectionManager { - private List<JingleConnection> connections = new CopyOnWriteArrayList<JingleConnection>(); + private List<JingleConnection> connections = new CopyOnWriteArrayList<>(); - private HashMap<String, JingleCandidate> primaryCandidates = new HashMap<String, JingleCandidate>(); + private HashMap<Jid, JingleCandidate> primaryCandidates = new HashMap<>(); @SuppressLint("TrulyRandom") private SecureRandom random = new SecureRandom(); @@ -61,7 +63,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { return connection; } - public JingleConnection createNewConnection(JinglePacket packet) { + public JingleConnection createNewConnection(final JinglePacket packet) { JingleConnection connection = new JingleConnection(this); this.connections.add(connection); return connection; @@ -73,13 +75,17 @@ public class JingleConnectionManager extends AbstractConnectionManager { public void getPrimaryCandidate(Account account, final OnPrimaryCandidateFound listener) { - if (!this.primaryCandidates.containsKey(account.getJid())) { + if (Config.NO_PROXY_LOOKUP) { + listener.onPrimaryCandidateFound(false, null); + return; + } + if (!this.primaryCandidates.containsKey(account.getJid().toBareJid())) { 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.setAttribute("to", proxy); iq.query(xmlns); account.getXmppConnection().sendIqPacket(iq, new OnIqPacketReceived() { @@ -101,9 +107,13 @@ public class JingleConnectionManager extends AbstractConnectionManager { .getAttribute("port"))); candidate .setType(JingleCandidate.TYPE_PROXY); - candidate.setJid(proxy); - candidate.setPriority(655360 + 65535); - primaryCandidates.put(account.getJid(), + try { + candidate.setJid(Jid.fromString(proxy)); + } catch (final InvalidJidException e) { + candidate.setJid(null); + } + candidate.setPriority(655360 + 65535); + primaryCandidates.put(account.getJid().toBareJid(), candidate); listener.onPrimaryCandidateFound(true, candidate); @@ -119,7 +129,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { } else { listener.onPrimaryCandidateFound(true, - this.primaryCandidates.get(account.getJid())); + this.primaryCandidates.get(account.getJid().toBareJid())); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java index cc1e92f6..04b225d0 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java @@ -8,17 +8,19 @@ 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.jid.Jid; import eu.siacs.conversations.xmpp.stanzas.IqPacket; public class JingleInbandTransport extends JingleTransport { private Account account; - private String counterpart; + private Jid counterpart; private int blockSize; private int bufferSize; private int seq = 0; @@ -26,11 +28,15 @@ public class JingleInbandTransport extends JingleTransport { private boolean established = false; + private boolean connected = true; + private DownloadableFile file; + private JingleConnection connection; private InputStream fileInputStream = null; - private OutputStream fileOutputStream; - private long remainingSize; + private OutputStream fileOutputStream = null; + private long remainingSize = 0; + private long fileSize = 0; private MessageDigest digest; private OnFileTransmissionStatusChanged onFileTransmissionStatusChanged; @@ -38,16 +44,16 @@ public class JingleInbandTransport extends JingleTransport { private OnIqPacketReceived onAckReceived = new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE_RESULT) { + if (connected && packet.getType() == IqPacket.TYPE_RESULT) { sendNextBlock(); } } }; - public JingleInbandTransport(Account account, String counterpart, - String sid, int blocksize) { - this.account = account; - this.counterpart = counterpart; + public JingleInbandTransport(final JingleConnection connection, final String sid, final int blocksize) { + this.connection = connection; + this.account = connection.getAccount(); + this.counterpart = connection.getCounterPart(); this.blockSize = blocksize; this.bufferSize = blocksize / 4; this.sessionId = sid; @@ -60,7 +66,7 @@ public class JingleInbandTransport extends JingleTransport { open.setAttribute("sid", this.sessionId); open.setAttribute("stanza", "iq"); open.setAttribute("block-size", Integer.toString(this.blockSize)); - + this.connected = true; this.account.getXmppConnection().sendIqPacket(iq, new OnIqPacketReceived() { @@ -91,13 +97,11 @@ public class JingleInbandTransport extends JingleTransport { callback.onFileTransferAborted(); return; } - this.remainingSize = file.getExpectedSize(); - } catch (NoSuchAlgorithmException e) { - callback.onFileTransferAborted(); - } catch (IOException e) { + this.remainingSize = this.fileSize = file.getExpectedSize(); + } catch (final NoSuchAlgorithmException | IOException e) { callback.onFileTransferAborted(); } - } + } @Override public void send(DownloadableFile file, @@ -105,6 +109,8 @@ public class JingleInbandTransport extends JingleTransport { this.onFileTransmissionStatusChanged = callback; this.file = file; try { + this.remainingSize = this.file.getSize(); + this.fileSize = this.remainingSize; this.digest = MessageDigest.getInstance("SHA-1"); this.digest.reset(); fileInputStream = this.file.createInputStream(); @@ -112,12 +118,33 @@ public class JingleInbandTransport extends JingleTransport { callback.onFileTransferAborted(); return; } - this.sendNextBlock(); + if (this.connected) { + this.sendNextBlock(); + } } catch (NoSuchAlgorithmException e) { callback.onFileTransferAborted(); } } + @Override + public void disconnect() { + this.connected = false; + if (this.fileOutputStream != null) { + try { + this.fileOutputStream.close(); + } catch (IOException e) { + + } + } + if (this.fileInputStream != null) { + try { + this.fileInputStream.close(); + } catch (IOException e) { + + } + } + } + private void sendNextBlock() { byte[] buffer = new byte[this.bufferSize]; try { @@ -127,6 +154,7 @@ public class JingleInbandTransport extends JingleTransport { fileInputStream.close(); this.onFileTransmissionStatusChanged.onFileTransmitted(file); } else { + this.remainingSize -= count; this.digest.update(buffer); String base64 = Base64.encodeToString(buffer, Base64.NO_WRAP); IqPacket iq = new IqPacket(IqPacket.TYPE_SET); @@ -141,6 +169,7 @@ public class JingleInbandTransport extends JingleTransport { this.account.getXmppConnection().sendIqPacket(iq, this.onAckReceived); this.seq++; + connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100)); } } catch (IOException e) { this.onFileTransmissionStatusChanged.onFileTransferAborted(); @@ -156,6 +185,7 @@ public class JingleInbandTransport extends JingleTransport { } this.remainingSize -= buffer.length; + this.fileOutputStream.write(buffer); this.digest.update(buffer); @@ -164,6 +194,8 @@ public class JingleInbandTransport extends JingleTransport { fileOutputStream.flush(); fileOutputStream.close(); this.onFileTransmissionStatusChanged.onFileTransmitted(file); + } else { + connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100)); } } catch (IOException e) { this.onFileTransmissionStatusChanged.onFileTransferAborted(); @@ -174,13 +206,14 @@ public class JingleInbandTransport extends JingleTransport { if (payload.getName().equals("open")) { if (!established) { established = true; + connected = 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")) { + } else if (connected && payload.getName().equals("data")) { this.receiveNextBlock(payload.getContent()); this.account.getXmppConnection().sendIqPacket( packet.generateRespone(IqPacket.TYPE_RESULT), null); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java index 1da2f0cd..c3419580 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java @@ -15,6 +15,7 @@ import eu.siacs.conversations.utils.CryptoHelper; public class JingleSocks5Transport extends JingleTransport { private JingleCandidate candidate; + private JingleConnection connection; private String destination; private OutputStream outputStream; private InputStream inputStream; @@ -25,16 +26,17 @@ public class JingleSocks5Transport extends JingleTransport { public JingleSocks5Transport(JingleConnection jingleConnection, JingleCandidate candidate) { this.candidate = candidate; + this.connection = jingleConnection; try { MessageDigest mDigest = MessageDigest.getInstance("SHA-1"); StringBuilder destBuilder = new StringBuilder(); destBuilder.append(jingleConnection.getSessionId()); if (candidate.isOurs()) { - destBuilder.append(jingleConnection.getAccount().getFullJid()); + destBuilder.append(jingleConnection.getAccount().getJid()); destBuilder.append(jingleConnection.getCounterPart()); } else { destBuilder.append(jingleConnection.getCounterPart()); - destBuilder.append(jingleConnection.getAccount().getFullJid()); + destBuilder.append(jingleConnection.getAccount().getJid()); } mDigest.reset(); this.destination = CryptoHelper.bytesToHex(mDigest @@ -102,11 +104,15 @@ public class JingleSocks5Transport extends JingleTransport { callback.onFileTransferAborted(); return; } + long size = file.getSize(); + long transmitted = 0; int count; byte[] buffer = new byte[8192]; while ((count = fileInputStream.read(buffer)) > 0) { outputStream.write(buffer, 0, count); digest.update(buffer, 0, count); + transmitted += count; + connection.updateProgress((int) ((((double) transmitted) / size) * 100)); } outputStream.flush(); file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest())); @@ -151,6 +157,7 @@ public class JingleSocks5Transport extends JingleTransport { callback.onFileTransferAborted(); return; } + double size = file.getExpectedSize(); long remainingSize = file.getExpectedSize(); byte[] buffer = new byte[8192]; int count = buffer.length; @@ -164,6 +171,7 @@ public class JingleSocks5Transport extends JingleTransport { digest.update(buffer, 0, count); remainingSize -= count; } + connection.updateProgress((int) (((size - remainingSize) / size) * 100)); } fileOutputStream.flush(); fileOutputStream.close(); @@ -189,6 +197,20 @@ public class JingleSocks5Transport extends JingleTransport { } public void disconnect() { + if (this.outputStream != null) { + try { + this.outputStream.close(); + } catch (IOException e) { + + } + } + if (this.inputStream != null) { + try { + this.inputStream.close(); + } catch (IOException e) { + + } + } if (this.socket != null) { try { this.socket.close(); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java index 1374e61c..e832d3f5 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java @@ -10,4 +10,6 @@ public abstract class JingleTransport { public abstract void send(final DownloadableFile file, final OnFileTransmissionStatusChanged callback); + + public abstract void disconnect(); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java index 77a73643..4f73a83a 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java @@ -1,6 +1,7 @@ package eu.siacs.conversations.xmpp.jingle.stanzas; import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.jid.Jid; import eu.siacs.conversations.xmpp.stanzas.IqPacket; public class JinglePacket extends IqPacket { @@ -85,8 +86,8 @@ public class JinglePacket extends IqPacket { return this.jingle.getAttribute("action"); } - public void setInitiator(String initiator) { - this.jingle.setAttribute("initiator", initiator); + public void setInitiator(final Jid initiator) { + this.jingle.setAttribute("initiator", initiator.toString()); } public boolean isAction(String action) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java b/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java index 154fadf6..9f5ac988 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java +++ b/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java @@ -1,6 +1,8 @@ package eu.siacs.conversations.xmpp.pep; import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.jid.Jid; + import android.util.Base64; public class Avatar { @@ -10,7 +12,7 @@ public class Avatar { public int height; public int width; public long size; - public String owner; + public Jid owner; public byte[] getImageAsBytes() { return Base64.decode(image, Base64.DEFAULT); diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java index eef41c79..eade220a 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java +++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java @@ -1,6 +1,8 @@ package eu.siacs.conversations.xmpp.stanzas; import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.jid.InvalidJidException; +import eu.siacs.conversations.xmpp.jid.Jid; public class AbstractStanza extends Element { @@ -8,27 +10,40 @@ public class AbstractStanza extends Element { super(name); } - public String getTo() { - return getAttribute("to"); + public Jid getTo() { + try { + return Jid.fromString(getAttribute("to")); + } catch (final InvalidJidException e) { + return null; + } } - public String getFrom() { - return getAttribute("from"); + public Jid getFrom() { + String from = getAttribute("from"); + if (from == null) { + return null; + } else { + try { + return Jid.fromString(from); + } catch (final InvalidJidException e) { + return null; + } + } } public String getId() { return this.getAttribute("id"); } - public void setTo(String to) { - setAttribute("to", to); + public void setTo(final Jid to) { + setAttribute("to", to.toString()); } - public void setFrom(String from) { - setAttribute("from", from); + public void setFrom(final Jid from) { + setAttribute("from", from.toString()); } - public void setId(String id) { + public void setId(final String id) { setAttribute("id", id); } } diff --git a/src/main/res/drawable-hdpi/ic_stat_communication_import_export.png b/src/main/res/drawable-hdpi/ic_stat_communication_import_export.png Binary files differnew file mode 100644 index 00000000..f929996e --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_stat_communication_import_export.png diff --git a/src/main/res/drawable-mdpi/ic_stat_communication_import_export.png b/src/main/res/drawable-mdpi/ic_stat_communication_import_export.png Binary files differnew file mode 100644 index 00000000..b581e174 --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_stat_communication_import_export.png diff --git a/src/main/res/drawable-xhdpi/ic_stat_communication_import_export.png b/src/main/res/drawable-xhdpi/ic_stat_communication_import_export.png Binary files differnew file mode 100644 index 00000000..a1d62d41 --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_stat_communication_import_export.png diff --git a/src/main/res/drawable-xxhdpi/ic_stat_communication_import_export.png b/src/main/res/drawable-xxhdpi/ic_stat_communication_import_export.png Binary files differnew file mode 100644 index 00000000..3539cd91 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_stat_communication_import_export.png diff --git a/src/main/res/layout/activity_verify_otr.xml b/src/main/res/layout/activity_verify_otr.xml new file mode 100644 index 00000000..73539e0e --- /dev/null +++ b/src/main/res/layout/activity_verify_otr.xml @@ -0,0 +1,189 @@ +<?xml version="1.0" encoding="utf-8"?> +<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:background="@color/secondarybackground"> + + <LinearLayout + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + <TextView + android:id="@+id/error_no_session" + android:layout_margin="16dp" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/no_otr_session_found" + android:layout_gravity="center_horizontal" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeBody" + /> + <RelativeLayout + android:id="@+id/verification_area_one" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:background="@drawable/infocard_border" + android:layout_margin="8dp"> + <LinearLayout + android:id="@+id/fingerprint_area" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:padding="16dp" + android:orientation="vertical"> + <TextView + android:id="@+id/remote_jid" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeHeadline"/> + <TextView + android:layout_marginTop="16dp" + android:id="@+id/your_fingerprint" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeBody" + android:typeface="monospace" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@color/secondarytext" + android:textSize="?attr/TextSizeInfo" + android:text="@string/your_fingerprint"/> + <TextView + android:layout_marginTop="16dp" + android:id="@+id/remote_fingerprint" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeBody" + android:typeface="monospace" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@color/secondarytext" + android:textSize="?attr/TextSizeInfo" + android:text="@string/remote_fingerprint"/> + </LinearLayout> + <LinearLayout + android:layout_below="@+id/fingerprint_area" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentBottom="true" + android:layout_alignParentLeft="true" + android:layout_alignParentRight="true" > + + <Button + android:id="@+id/button_show_qr_code" + style="?android:attr/borderlessButtonStyle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="@string/show_qr_code"/> + + <View + android:layout_width="1dp" + android:layout_height="fill_parent" + android:layout_marginBottom="7dp" + android:layout_marginTop="7dp" + android:background="@color/divider" /> + + <Button + android:id="@+id/button_scan_qr_code" + style="?android:attr/borderlessButtonStyle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="@string/scan_qr_code" + android:textColor="@color/primarytext" /> + </LinearLayout> + </RelativeLayout> + <RelativeLayout + android:id="@+id/verification_area_two" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_margin="8dp" + android:background="@drawable/infocard_border"> + <LinearLayout + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:orientation="vertical" + android:id="@+id/shared_secret_box" + android:padding="16dp"> + <TextView + android:text="@string/smp" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeHeadline" + android:layout_marginBottom="16dp" + /> + <TextView + android:id="@+id/status_message" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/verified" + android:layout_gravity="center_horizontal" + android:textSize="?attr/TextSizeHeadline" + android:textStyle="bold" + android:visibility="gone"/> + <EditText + android:id="@+id/shared_secret_hint" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:inputType="textAutoComplete" + android:hint="@string/shared_secret_hint" + android:textColor="@color/primarytext" + android:textColorHint="@color/secondarytext" + android:textSize="?attr/TextSizeBody" + android:layout_marginBottom="8dp"/> + <EditText + android:id="@+id/shared_secret_secret" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/shared_secret_secret" + android:inputType="textPassword" + android:textColor="@color/primarytext" + android:textColorHint="@color/secondarytext" + android:textSize="?attr/TextSizeBody" /> + </LinearLayout> + <LinearLayout + android:layout_below="@+id/shared_secret_box" + android:id="@+id/button_bar" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentBottom="true" + android:layout_alignParentLeft="true" + android:layout_alignParentRight="true" > + + <Button + android:id="@+id/button_shared_secret_negative" + style="?android:attr/borderlessButtonStyle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:enabled="false" + android:text="@string/cancel" + android:textColor="@color/secondarytext"/> + + <View + android:layout_width="1dp" + android:layout_height="fill_parent" + android:layout_marginBottom="7dp" + android:layout_marginTop="7dp" + android:background="@color/divider" /> + + <Button + android:id="@+id/button_shared_secret_positive" + style="?android:attr/borderlessButtonStyle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="@string/create" + android:textColor="@color/primarytext" /> + </LinearLayout> + </RelativeLayout> + </LinearLayout> +</ScrollView>
\ No newline at end of file diff --git a/src/main/res/layout/dialog_verify_otr.xml b/src/main/res/layout/dialog_verify_otr.xml deleted file mode 100644 index 499ef6cd..00000000 --- a/src/main/res/layout/dialog_verify_otr.xml +++ /dev/null @@ -1,60 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:orientation="vertical" - android:paddingBottom="16dp" - android:paddingLeft="8dp" - android:paddingRight="8dp" > - - <TextView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:paddingTop="8dp" - android:text="@string/account_settings_jabber_id" - android:textColor="@color/primarytext" - android:textSize="?attr/TextSizeHeadline" /> - - <TextView - android:id="@+id/verify_otr_jid" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:paddingLeft="8dp" - android:textColor="@color/secondarytext" - android:textSize="?attr/TextSizeBody" /> - - <TextView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:paddingTop="8dp" - android:text="@string/otr_fingerprint" - android:textColor="@color/primarytext" - android:textSize="?attr/TextSizeHeadline" /> - - <TextView - android:id="@+id/verify_otr_fingerprint" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:paddingLeft="8dp" - android:textColor="@color/secondarytext" - android:textSize="?attr/TextSizeBody" - android:typeface="monospace" /> - - <TextView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:paddingTop="8dp" - android:text="@string/your_fingerprint" - android:textColor="@color/primarytext" - android:textSize="?attr/TextSizeHeadline" /> - - <TextView - android:id="@+id/verify_otr_yourprint" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:paddingLeft="8dp" - android:textColor="@color/secondarytext" - android:textSize="?attr/TextSizeBody" - android:typeface="monospace" /> - -</LinearLayout>
\ No newline at end of file diff --git a/src/main/res/menu/attachment_choices.xml b/src/main/res/menu/attachment_choices.xml index 20932489..12b37c08 100644 --- a/src/main/res/menu/attachment_choices.xml +++ b/src/main/res/menu/attachment_choices.xml @@ -9,7 +9,6 @@ android:title="@string/attach_take_picture"/> <item android:id="@+id/attach_record_voice" - android:title="@string/attach_record_voice" - android:visible="false"/> + android:title="@string/choose_file"/> </menu>
\ No newline at end of file diff --git a/src/main/res/menu/message_context.xml b/src/main/res/menu/message_context.xml index 80d4d196..3be52442 100644 --- a/src/main/res/menu/message_context.xml +++ b/src/main/res/menu/message_context.xml @@ -16,5 +16,8 @@ <item android:id="@+id/download_image" android:title="@string/download_image"/> + <item + android:id="@+id/cancel_transmission" + android:title="@string/cancel_transmission" /> </menu>
\ No newline at end of file diff --git a/src/main/res/menu/verify_otr.xml b/src/main/res/menu/verify_otr.xml new file mode 100644 index 00000000..1d4a11b6 --- /dev/null +++ b/src/main/res/menu/verify_otr.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + + <item + android:id="@+id/manually_verify" + android:orderInCategory="10" + android:showAsAction="never" + android:title="@string/manually_verify" /> + + <item + android:id="@+id/action_accounts" + android:orderInCategory="90" + android:showAsAction="never" + android:title="@string/action_accounts" /> + <item + android:id="@+id/action_settings" + android:orderInCategory="100" + android:showAsAction="never" + android:title="@string/action_settings" /> +</menu>
\ No newline at end of file diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index ca190deb..6ec79a82 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -108,6 +108,7 @@ <string name="pref_never_send_crash_summary">Wenn du Absturzberichte einschickst, hilfst du Conversations stetig zu verbessern</string> <string name="pref_confirm_messages">Lesebestätigung senden</string> <string name="pref_confirm_messages_summary">Informiere deine Kontakte, wenn du eine Nachricht empfängst oder liest</string> + <string name="pref_ui_options">Benutzeroberfläche</string> <string name="openpgp_error">Fehler mit OpenKeychain</string> <string name="error_decrypting_file">Fehler beim Entschlüsseln der Datei</string> <string name="accept">Annehmen</string> @@ -231,9 +232,6 @@ <string name="server_info_session_established">Aktuelle Sitzung wiederhergestellt</string> <string name="additional_information">Zusätzliche Informationen</string> <string name="skip">Überspringen</string> - <string name="pref_ui_options">Benutzeroberfläche</string> - <string name="pref_use_indicate_received">Anfrage für Nachrichten Empfang</string> - <string name="pref_use_indicate_received_summary">Empfangene Nachrichten werden mit einem grünen Häckchen markiert. Bitte beachte, dass dies nicht in allen Fällen funktioniert.</string> <string name="disable_notifications">Benachrichtigungen deaktivieren</string> <string name="disable_notifications_for_this_conversation">Benachrichtigungen für diese Unterhaltung deaktivieren</string> <string name="notifications_disabled">Benachrichtigungen sind deaktiviert</string> @@ -256,13 +254,17 @@ <string name="pref_enable_legacy_ssl_summary">Aktiviert SSLv3-Unterstützung für alte Server. Achtung: SSLv3 ist unsicher.</string> <string name="pref_expert_options">Einstellungen für Experten</string> <string name="pref_expert_options_summary">Hier bitte vorsichtig sein</string> + <string name="title_activity_about">Über Conversations</string> + <string name="pref_about_conversations_summary">Versions- und Lizenzinformationen</string> <string name="pref_use_larger_font">Schrift vergrößern</string> <string name="pref_use_larger_font_summary">Überall in der App eine größere Schrift verwenden</string> <string name="pref_use_send_button_to_indicate_status">Absende-Knopf zeigt Online-Status an</string> <string name="pref_use_send_button_to_indicate_status_summary">Absende-Knopf einfärben, um den Online-Status des Kontakts zu signalisieren</string> + <string name="pref_use_indicate_received">Anfrage für Nachrichten Empfang</string> + <string name="pref_use_indicate_received_summary">Empfangene Nachrichten werden mit einem grünen Häkchen markiert. Bitte beachte, dass dies nicht in allen Fällen funktioniert.</string> <string name="pref_expert_options_other">Sonstiges</string> <string name="pref_conference_name">Konferenz-Name</string> - <string name="pref_conference_name_summary">Konferenz-Thema statt Raum-JID als Name verwenden</string> + <string name="pref_conference_name_summary">Konferenz-Thema statt Raum-JID als Namen verwenden</string> <string name="toast_message_otr_fingerprint">OTR Fingerabdruck in die Zwischenablage kopiert!</string> <string name="conference_banned">Du wurdest aus dem Konferenzraum verbannt</string> <string name="conference_members_only">Der Konferenzraum ist nur für Mitglieder</string> @@ -281,5 +283,8 @@ <string name="message_text">Nachrichtentext</string> <string name="url_copied_to_clipboard">URL in Zwischenablage kopiert</string> <string name="message_copied_to_clipboard">Nachricht in Zwischenablage kopiert</string> - + <string name="image_transmission_failed">Bild-Übertragung fehlgeschlagen</string> + <string name="scan_qr_code">Scanne QR-Code</string> + <string name="show_qr_code">Zeige QR-Code</string> + <string name="account_details">Account Details</string> </resources> diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index 47424d00..95a5b4d5 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -141,6 +141,8 @@ <string name="account_status_regis_conflict">El identificador ya está en uso</string> <string name="account_status_regis_success">Registro completado</string> <string name="account_status_regis_not_sup">El servidor no soporta registros</string> + <string name="account_status_security_error">Error de seguridad</string> + <string name="account_status_incompatible_server">Servidor incompatible</string> <string name="encryption_choice_none">Texto plano</string> <string name="encryption_choice_otr">OTR</string> <string name="encryption_choice_pgp">OpenPGP</string> @@ -254,6 +256,8 @@ <string name="pref_enable_legacy_ssl_summary">Habilita soporte SSLv3 para servidores heredados. Advertencia: SSLv3 se considera no seguro.</string> <string name="pref_expert_options">Ajustes avanzados</string> <string name="pref_expert_options_summary">Por favor, cuidado con estas opciones</string> + <string name="title_activity_about">Acerca de Conversations</string> + <string name="pref_about_conversations_summary">Información de compilación y licencia</string> <string name="pref_use_larger_font">Incrementar tamaño de fuente</string> <string name="pref_use_larger_font_summary">Usar fuentes grandes en toda la aplicación</string> <string name="pref_use_send_button_to_indicate_status">Botón enviar indica estado</string> @@ -281,5 +285,41 @@ <string name="message_text">Mensaje de texto</string> <string name="url_copied_to_clipboard">URL copiada al portapapeles</string> <string name="message_copied_to_clipboard">Mensaje copiado al portapapeles</string> - + <string name="image_transmission_failed">Falló la transferencia de la imagen</string> + <string name="scan_qr_code">Escanear código QR</string> + <string name="show_qr_code">Mostrar código QR</string> + <string name="account_details">Detalles de la cuenta</string> + <string name="verify_otr">Verificar OTR</string> + <string name="remote_fingerprint">Códigos OTR remotos</string> + <string name="scan">escanear</string> + <string name="or_touch_phones">(o une los teléfonos)</string> + <string name="smp">Protocolo del Socialista Millonario</string> + <string name="shared_secret_hint">Sugerencia o Pregunta</string> + <string name="shared_secret_secret">Compartir secreto</string> + <string name="confirm">Confirmar</string> + <string name="in_progress">En progreso</string> + <string name="respond">Responder</string> + <string name="failed">Falló</string> + <string name="secrets_do_not_match">Los secretos no coinciden</string> + <string name="try_again">Intentar de nuevo</string>; + <string name="finish">Terminar</string> + <string name="verified">Verificado!</string> + <string name="smp_requested">El contacto solicita verificación SMP</string> + <string name="no_otr_session_found">No se ha encontrado una sesión OTR válida!</string> + <string name="conversations_foreground_service">Conversations</string> + <string name="touch_to_disable">Pulsa para deshabilitar servicio en segundo plano</string> + <string name="pref_keep_foreground_service">Mantener servicio en segundo plano</string> + <string name="pref_keep_foreground_service_summary">Previene que el sistema cierre la conexión</string> + <string name="choose_file">Elige archivo</string> + <string name="receiving_file">Recibiendo archivo %1$s (%2$d%% completedo)</string> + <string name="download_file">Descargar archivo %s</string> + <string name="open_file">Abrir %s como archivo</string> + <string name="sending_file">Enviando (%1$d%% completedo)</string> + <string name="preparing_file">Preparando transferencia de archivo</string> + <string name="file_offered_for_download">Archivo ofrecido para descarga</string> + <string name="file">Archivo %s</string> + <string name="cancel_transmission">Cancelar transferencia</string> + <string name="file_transmission_failed">falló la transferencia del archivo</string> + <string name="file_deleted">El archivo ha sido eliminado</string> + <string name="no_application_found_to_open_file">No se ha encontrado ninguna aplicación para abrir el archivo</string> </resources>
\ No newline at end of file diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml index 0bb0e05e..3b613e9d 100644 --- a/src/main/res/values-it/strings.xml +++ b/src/main/res/values-it/strings.xml @@ -81,9 +81,9 @@ <string name="offering">offrendo…</string> <string name="waiting">in attesa…</string> <string name="no_pgp_key">Nessuna chiave OpenPGP trovata</string> - <string name="contact_has_no_pgp_key">Conversations non è in grado di cifrare i tuoi messaggi perchè il contatto non sta annunciando la sua chiave pubblica.\n\n<small>Per favore chiedi al tuo contatto di configurare OpenPGP.</small></string> + <string name="contact_has_no_pgp_key">Conversations non è in grado di cifrare i tuoi messaggi perché il contatto non sta annunciando la sua chiave pubblica.\n\n<small>Per favore chiedi al tuo contatto di configurare OpenPGP.</small></string> <string name="no_pgp_keys">Nessuna chiave OpenPGP trovata</string> - <string name="contacts_have_no_pgp_keys">Conversations non è in grado di cifrare i tuoi messaggi perchè i contatti non stanno annunciando la propria chiave pubblica.\n\n<small>Per favore chiedi ai tuoi contatti di configurare OpenPGP.</small></string> + <string name="contacts_have_no_pgp_keys">Conversations non è in grado di cifrare i tuoi messaggi perché i contatti non stanno annunciando la propria chiave pubblica.\n\n<small>Per favore chiedi ai tuoi contatti di configurare OpenPGP.</small></string> <string name="encrypted_message_received"><i>Messaggio cifrato ricevuto. Tocca per decifrare.</i></string> <string name="encrypted_image_received"><i>Immagine cifrata ricevuta. Tocca per decifrare e mostrare.</i></string> <string name="image_file"><i>Immagine ricevuta. Tocca per mostrare</i></string> diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 28a1e326..2912d895 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -58,7 +58,7 @@ <string name="add_contact">Add contact</string> <string name="send_failed">delivery failed</string> <string name="send_rejected">rejected</string> - <string name="receiving_image">Receiving image file. Please wait…</string> + <string name="receiving_image">Receiving image file (%1$d%%)</string> <string name="preparing_image">Preparing image for transmission</string> <string name="action_clear_history">Clear history</string> <string name="clear_conversation_history">Clear Conversation History</string> @@ -141,6 +141,8 @@ <string name="account_status_regis_conflict">Username already in use</string> <string name="account_status_regis_success">Registration completed</string> <string name="account_status_regis_not_sup">Server does not support registration</string> + <string name="account_status_security_error">Security error</string> + <string name="account_status_incompatible_server">Incompatible server</string> <string name="encryption_choice_none">Plain text</string> <string name="encryption_choice_otr">OTR</string> <string name="encryption_choice_pgp">OpenPGP</string> @@ -256,21 +258,30 @@ <string name="pref_expert_options_summary">Please be careful with these</string> <string name="title_activity_about">About Conversations</string> <string name="pref_about_conversations_summary">Build and licensing information</string> - <string name="pref_about_message">Conversations © 2014 Daniel Gultsch\n - \nThis program is free software; you can redistribute it and/or modify it - under the terms of the GNU General Public License version 3 as published - by the Free Software Foundation. - \nhttps://www.gnu.org/licenses/gpl-3.0.html\n - \nOpenPGP API is licensed under the Apache License, Version 2.0 - \nhttps://www.apache.org/licenses/LICENSE-2.0\n - \nMinidns © 2014 Rene Treffer and is provided under the WTFPL - \nhttp://wtfpl.org\n - \nMemorizingTrustManager © 2010 Georg Lukas under the terms of the MIT - License - \nhttp://opensource.org/licenses/MIT\n - \nDownload the full source code at - \nhttps://github.com/siacs/Conversations - </string> + <string name="pref_about_message" translatable="false"> + Conversations • the very last word in instant messaging. + \n\nCopyright © 2014 Daniel Gultsch + \n\nThis program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + \n\nThis program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + \n\nYou should have received a copy of the GNU General Public License + along with this program. If not, see https://www.gnu.org/licenses + \n\nDownload the full source code at https://github.com/siacs/Conversations + \n\n\nLibraries + \n\nhttps://www.bouncycastle.org\n(The MIT License (MIT)) + \n\nhttps://www.gnu.org/software/libidn\n(Apache License, Version 2.0) + \n\nhttps://github.com/ge0rg/MemorizingTrustManager\n(The MIT License (MIT)) + \n\nhttps://github.com/rtreffer/minidns\n(WTFPL) + \n\nhttps://github.com/open-keychain/openkeychain-api-lib\n(Apache License, Version 2.0) + \n\nhttps://github.com/jitsi/otr4j\n(LGPL-3.0) + \n\nhttps://developer.android.com/tools/support-library\n(Apache License, Version 2.0) + \n\nhttps://github.com/zxing/zxing\n(Apache License, Version 2.0) + </string> <string name="pref_use_larger_font">Increase font size</string> <string name="pref_use_larger_font_summary">Use larger font sizes across the entire app</string> <string name="pref_use_send_button_to_indicate_status">Send button indicates status</string> @@ -302,4 +313,40 @@ <string name="scan_qr_code">Scan QR code</string> <string name="show_qr_code">Show QR code</string> <string name="account_details">Account details</string> + <string name="verify_otr">Verify OTR</string> + <string name="remote_fingerprint">Remote Fingerprint</string> + <string name="scan">scan</string> + <string name="or_touch_phones">(or touch phones)</string> + <string name="smp">Socialist Millionaire Protocol</string> + <string name="shared_secret_hint">Hint or Question</string> + <string name="shared_secret_secret">Shared Secret</string> + <string name="confirm">Confirm</string> + <string name="in_progress">In progress</string> + <string name="respond">Respond</string> + <string name="failed">Failed</string> + <string name="secrets_do_not_match">Secrets do not match</string> + <string name="try_again">Try again</string>; + <string name="finish">Finish</string> + <string name="verified">Verified!</string> + <string name="smp_requested">Contact requested SMP verification</string> + <string name="no_otr_session_found">No valid OTR session has been found!</string> + <string name="conversations_foreground_service">Conversations</string> + <string name="touch_to_disable">Touch to disable foreground service</string> + <string name="pref_keep_foreground_service">Keep service in foreground</string> + <string name="pref_keep_foreground_service_summary">Prevents the operating system from killing your connection</string> + <string name="choose_file">Choose file</string> + <string name="receiving_file">Receiving %1$s file (%2$d%% completed)</string> + <string name="download_file">Download %s file</string> + <string name="open_file">Open %s file</string> + <string name="sending_file">sending (%1$d%% completed)</string> + <string name="preparing_file">Preparing file for transmission</string> + <string name="file_offered_for_download">File offered for download</string> + <string name="file">%s file</string> + <string name="cancel_transmission">Cancel transmission</string> + <string name="file_transmission_failed">file transmission failed</string> + <string name="file_deleted">The file has been deleted</string> + <string name="no_application_found_to_open_file">No application found to open file</string> + <string name="could_not_verify_fingerprint">Could not verify fingerprint</string> + <string name="manually_verify">Manually verify</string> + <string name="are_you_sure_verify_fingerprint">Are you sure that you want to verify your contacts OTR fingerprint?</string> </resources> diff --git a/src/main/res/xml/preferences.xml b/src/main/res/xml/preferences.xml index 15a61e87..3be65b7a 100644 --- a/src/main/res/xml/preferences.xml +++ b/src/main/res/xml/preferences.xml @@ -101,6 +101,11 @@ android:key="indicate_received" android:summary="@string/pref_use_indicate_received_summary" android:title="@string/pref_use_indicate_received" /> + <CheckBoxPreference + android:defaultValue="false" + android:key="keep_foreground_service" + android:title="@string/pref_keep_foreground_service" + android:summary="@string/pref_keep_foreground_service_summary" /> </PreferenceCategory> </PreferenceScreen> |