diff options
Diffstat (limited to 'src')
48 files changed, 3589 insertions, 2343 deletions
diff --git a/src/eu/siacs/conversations/crypto/PgpEngine.java b/src/eu/siacs/conversations/crypto/PgpEngine.java index 48750e24..c0d8ca07 100644 --- a/src/eu/siacs/conversations/crypto/PgpEngine.java +++ b/src/eu/siacs/conversations/crypto/PgpEngine.java @@ -17,6 +17,7 @@ import org.openintents.openpgp.util.OpenPgpApi.IOpenPgpCallback; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.ui.UiCallback; @@ -35,17 +36,19 @@ public class PgpEngine { this.mXmppConnectionService = service; } - public void decrypt(final Message message, final UiCallback callback) { - Log.d("xmppService","decrypting message "+message.getUuid()); + public void decrypt(final Message message, + final UiCallback<Message> callback) { + Log.d("xmppService", "decrypting message " + message.getUuid()); Intent params = new Intent(); params.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY); params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, message .getConversation().getAccount().getJid()); if (message.getType() == Message.TYPE_TEXT) { - InputStream is = new ByteArrayInputStream(message.getBody().getBytes()); + InputStream is = new ByteArrayInputStream(message.getBody() + .getBytes()); final OutputStream os = new ByteArrayOutputStream(); api.executeApiAsync(params, is, os, new IOpenPgpCallback() { - + @Override public void onReturn(Intent result) { switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, @@ -53,14 +56,15 @@ public class PgpEngine { case OpenPgpApi.RESULT_CODE_SUCCESS: message.setBody(os.toString()); message.setEncryption(Message.ENCRYPTION_DECRYPTED); - callback.success(); + callback.success(message); return; case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: callback.userInputRequried((PendingIntent) result - .getParcelableExtra(OpenPgpApi.RESULT_INTENT)); + .getParcelableExtra(OpenPgpApi.RESULT_INTENT), + message); return; case OpenPgpApi.RESULT_CODE_ERROR: - callback.error(R.string.openpgp_error); + callback.error(R.string.openpgp_error, message); return; default: return; @@ -69,13 +73,15 @@ public class PgpEngine { }); } else if (message.getType() == Message.TYPE_IMAGE) { try { - final JingleFile inputFile = this.mXmppConnectionService.getFileBackend().getJingleFile(message, false); - final JingleFile outputFile = this.mXmppConnectionService.getFileBackend().getJingleFile(message,true); + final JingleFile inputFile = this.mXmppConnectionService + .getFileBackend().getJingleFile(message, false); + final JingleFile outputFile = this.mXmppConnectionService + .getFileBackend().getJingleFile(message, true); outputFile.createNewFile(); InputStream is = new FileInputStream(inputFile); OutputStream os = new FileOutputStream(outputFile); api.executeApiAsync(params, is, os, new IOpenPgpCallback() { - + @Override public void onReturn(Intent result) { switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, @@ -83,21 +89,27 @@ public class PgpEngine { case OpenPgpApi.RESULT_CODE_SUCCESS: BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; - BitmapFactory.decodeFile(outputFile.getAbsolutePath(),options); + BitmapFactory.decodeFile( + outputFile.getAbsolutePath(), options); int imageHeight = options.outHeight; int imageWidth = options.outWidth; - message.setBody(""+outputFile.getSize()+","+imageWidth+","+imageHeight); + message.setBody("" + outputFile.getSize() + "," + + imageWidth + "," + imageHeight); message.setEncryption(Message.ENCRYPTION_DECRYPTED); - PgpEngine.this.mXmppConnectionService.updateMessage(message); - PgpEngine.this.mXmppConnectionService.updateUi(message.getConversation(), false); - callback.success(); + PgpEngine.this.mXmppConnectionService + .updateMessage(message); + PgpEngine.this.mXmppConnectionService.updateUi( + message.getConversation(), false); + callback.success(message); return; case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: - callback.userInputRequried((PendingIntent) result - .getParcelableExtra(OpenPgpApi.RESULT_INTENT)); + callback.userInputRequried( + (PendingIntent) result + .getParcelableExtra(OpenPgpApi.RESULT_INTENT), + message); return; case OpenPgpApi.RESULT_CODE_ERROR: - callback.error(R.string.openpgp_error); + callback.error(R.string.openpgp_error, message); return; default: return; @@ -105,28 +117,38 @@ public class PgpEngine { } }); } catch (FileNotFoundException e) { - callback.error(R.string.error_decrypting_file); + callback.error(R.string.error_decrypting_file, message); } catch (IOException e) { - callback.error(R.string.error_decrypting_file); + callback.error(R.string.error_decrypting_file, message); } - + } } - public void encrypt(final Message message,final UiCallback callback) { - long[] keys = { message.getConversation().getContact().getPgpKeyId() }; + public void encrypt(final Message message, + final UiCallback<Message> callback) { + Intent params = new Intent(); params.setAction(OpenPgpApi.ACTION_ENCRYPT); - params.putExtra(OpenPgpApi.EXTRA_KEY_IDS, keys); - params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, message.getConversation().getAccount().getJid()); - + if (message.getConversation().getMode() == Conversation.MODE_SINGLE) { + long[] keys = { message.getConversation().getContact() + .getPgpKeyId() }; + params.putExtra(OpenPgpApi.EXTRA_KEY_IDS, keys); + } else { + params.putExtra(OpenPgpApi.EXTRA_KEY_IDS, message.getConversation() + .getMucOptions().getPgpKeyIds()); + } + params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, message + .getConversation().getAccount().getJid()); + if (message.getType() == Message.TYPE_TEXT) { params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); - - InputStream is = new ByteArrayInputStream(message.getBody().getBytes()); + + InputStream is = new ByteArrayInputStream(message.getBody() + .getBytes()); final OutputStream os = new ByteArrayOutputStream(); api.executeApiAsync(params, is, os, new IOpenPgpCallback() { - + @Override public void onReturn(Intent result) { switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, @@ -137,53 +159,59 @@ public class PgpEngine { for (int i = 3; i < lines.length - 1; ++i) { encryptedMessageBody.append(lines[i].trim()); } - message.setEncryptedBody(encryptedMessageBody.toString()); - callback.success(); + message.setEncryptedBody(encryptedMessageBody + .toString()); + callback.success(message); break; case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: callback.userInputRequried((PendingIntent) result - .getParcelableExtra(OpenPgpApi.RESULT_INTENT)); + .getParcelableExtra(OpenPgpApi.RESULT_INTENT), + message); break; case OpenPgpApi.RESULT_CODE_ERROR: - callback.error(R.string.openpgp_error); + callback.error(R.string.openpgp_error, message); break; } } }); } else if (message.getType() == Message.TYPE_IMAGE) { try { - JingleFile inputFile = this.mXmppConnectionService.getFileBackend().getJingleFile(message, true); - JingleFile outputFile = this.mXmppConnectionService.getFileBackend().getJingleFile(message, false); + JingleFile inputFile = this.mXmppConnectionService + .getFileBackend().getJingleFile(message, true); + JingleFile outputFile = this.mXmppConnectionService + .getFileBackend().getJingleFile(message, false); outputFile.createNewFile(); InputStream is = new FileInputStream(inputFile); OutputStream os = new FileOutputStream(outputFile); api.executeApiAsync(params, is, os, new IOpenPgpCallback() { - + @Override public void onReturn(Intent result) { switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { case OpenPgpApi.RESULT_CODE_SUCCESS: - callback.success(); + callback.success(message); break; case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: - callback.userInputRequried((PendingIntent) result - .getParcelableExtra(OpenPgpApi.RESULT_INTENT)); + callback.userInputRequried( + (PendingIntent) result + .getParcelableExtra(OpenPgpApi.RESULT_INTENT), + message); break; case OpenPgpApi.RESULT_CODE_ERROR: - callback.error(R.string.openpgp_error); + callback.error(R.string.openpgp_error, message); break; } } }); } catch (FileNotFoundException e) { - Log.d("xmppService","file not found: "+e.getMessage()); + Log.d("xmppService", "file not found: " + e.getMessage()); } catch (IOException e) { - Log.d("xmppService","io exception during file encrypt"); + Log.d("xmppService", "io exception during file encrypt"); } } } - + public long fetchKeyId(Account account, String status, String signature) { if ((signature == null) || (api == null)) { return 0; @@ -221,18 +249,20 @@ public class PgpEngine { return 0; } case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: - Log.d("xmppService","openpgp user interaction requeried"); return 0; case OpenPgpApi.RESULT_CODE_ERROR: - Log.d("xmppService","openpgp error: "+((OpenPgpError) result - .getParcelableExtra(OpenPgpApi.RESULT_ERROR)).getMessage()); + Log.d("xmppService", + "openpgp error: " + + ((OpenPgpError) result + .getParcelableExtra(OpenPgpApi.RESULT_ERROR)) + .getMessage()); return 0; } return 0; } public void generateSignature(final Account account, String status, - final UiCallback callback) { + final UiCallback<Account> callback) { Intent params = new Intent(); params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); params.setAction(OpenPgpApi.ACTION_SIGN); @@ -251,51 +281,66 @@ public class PgpEngine { signatureBuilder.append(lines[i].trim()); } account.setKey("pgp_signature", signatureBuilder.toString()); - callback.success(); + callback.success(account); return; case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: callback.userInputRequried((PendingIntent) result - .getParcelableExtra(OpenPgpApi.RESULT_INTENT)); + .getParcelableExtra(OpenPgpApi.RESULT_INTENT), + account); return; case OpenPgpApi.RESULT_CODE_ERROR: - callback.error(R.string.openpgp_error); + callback.error(R.string.openpgp_error, account); return; } } }); } - - public void hasKey(Contact contact, final UiCallback callback) { + + public void hasKey(final Contact contact, final UiCallback<Contact> callback) { Intent params = new Intent(); params.setAction(OpenPgpApi.ACTION_GET_KEY); params.putExtra(OpenPgpApi.EXTRA_KEY_ID, contact.getPgpKeyId()); - params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, contact.getAccount().getJid()); + params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, contact.getAccount() + .getJid()); api.executeApiAsync(params, null, null, new IOpenPgpCallback() { - + @Override public void onReturn(Intent result) { switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) { case OpenPgpApi.RESULT_CODE_SUCCESS: - callback.success(); + callback.success(contact); return; case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: callback.userInputRequried((PendingIntent) result - .getParcelableExtra(OpenPgpApi.RESULT_INTENT)); + .getParcelableExtra(OpenPgpApi.RESULT_INTENT), + contact); return; case OpenPgpApi.RESULT_CODE_ERROR: - callback.error(R.string.openpgp_error); + callback.error(R.string.openpgp_error, contact); return; } } }); } - + public PendingIntent getIntentForKey(Contact contact) { Intent params = new Intent(); params.setAction(OpenPgpApi.ACTION_GET_KEY); params.putExtra(OpenPgpApi.EXTRA_KEY_ID, contact.getPgpKeyId()); - params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, contact.getAccount().getJid()); + params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, contact.getAccount() + .getJid()); + Intent result = api.executeApi(params, null, null); + return (PendingIntent) result + .getParcelableExtra(OpenPgpApi.RESULT_INTENT); + } + + public PendingIntent getIntentForKey(Account account, long pgpKeyId) { + Intent params = new Intent(); + params.setAction(OpenPgpApi.ACTION_GET_KEY); + params.putExtra(OpenPgpApi.EXTRA_KEY_ID, pgpKeyId); + params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, account.getJid()); Intent result = api.executeApi(params, null, null); - return (PendingIntent) result.getParcelableExtra(OpenPgpApi.RESULT_INTENT); + return (PendingIntent) result + .getParcelableExtra(OpenPgpApi.RESULT_INTENT); } } diff --git a/src/eu/siacs/conversations/entities/Account.java b/src/eu/siacs/conversations/entities/Account.java index 35870aaa..b9c87eac 100644 --- a/src/eu/siacs/conversations/entities/Account.java +++ b/src/eu/siacs/conversations/entities/Account.java @@ -65,6 +65,8 @@ public class Account extends AbstractEntity{ private String otrFingerprint; + private Roster roster = null; + public Account() { this.uuid = "0"; } @@ -135,7 +137,7 @@ public class Account extends AbstractEntity{ } public boolean hasErrorStatus() { - return getStatus() > STATUS_NO_INTERNET; + return getStatus() > STATUS_NO_INTERNET && (getXmppConnection().getAttempt() >= 2); } public void setResource(String resource) { @@ -287,4 +289,11 @@ public class Account extends AbstractEntity{ return null; } } + + public Roster getRoster() { + if (this.roster==null) { + this.roster = new Roster(this); + } + return this.roster; + } } diff --git a/src/eu/siacs/conversations/entities/Contact.java b/src/eu/siacs/conversations/entities/Contact.java index 599aa8de..a0047cdf 100644 --- a/src/eu/siacs/conversations/entities/Contact.java +++ b/src/eu/siacs/conversations/entities/Contact.java @@ -1,8 +1,6 @@ package eu.siacs.conversations.entities; -import java.io.Serializable; import java.util.HashSet; -import java.util.Hashtable; import java.util.Set; import org.json.JSONArray; @@ -10,27 +8,24 @@ import org.json.JSONException; import org.json.JSONObject; import eu.siacs.conversations.xml.Element; - import android.content.ContentValues; import android.database.Cursor; -import android.util.Log; - -public class Contact extends AbstractEntity implements Serializable { - private static final long serialVersionUID = -4570817093119419962L; +public class Contact { public static final String TABLENAME = "contacts"; - public static final String DISPLAYNAME = "name"; + public static final String SYSTEMNAME = "systemname"; + public static final String SERVERNAME = "servername"; public static final String JID = "jid"; - public static final String SUBSCRIPTION = "subscription"; + public static final String OPTIONS = "options"; public static final String SYSTEMACCOUNT = "systemaccount"; public static final String PHOTOURI = "photouri"; public static final String KEYS = "pgpkey"; - public static final String PRESENCES = "presences"; public static final String ACCOUNT = "accountUuid"; protected String accountUuid; - protected String displayName; + protected String systemName; + protected String serverName; protected String jid; protected int subscription = 0; protected String systemAccount; @@ -41,27 +36,15 @@ public class Contact extends AbstractEntity implements Serializable { protected Account account; protected boolean inRoster = true; + + public Lastseen lastseen = new Lastseen(); - public Contact(Account account, String displayName, String jid, - String photoUri) { - if (account == null) { - this.accountUuid = null; - } else { - this.accountUuid = account.getUuid(); - } - this.account = account; - this.displayName = displayName; - this.jid = jid; - this.photoUri = photoUri; - this.uuid = java.util.UUID.randomUUID().toString(); - } - - public Contact(String uuid, String account, String displayName, String jid, - int subscription, String photoUri, String systemAccount, - String keys, String presences) { - this.uuid = uuid; + public Contact(String account, String systemName, String serverName, + String jid, int subscription, String photoUri, + String systemAccount, String keys) { this.accountUuid = account; - this.displayName = displayName; + this.systemName = systemName; + this.serverName = serverName; this.jid = jid; this.subscription = subscription; this.photoUri = photoUri; @@ -74,11 +57,20 @@ public class Contact extends AbstractEntity implements Serializable { } catch (JSONException e) { this.keys = new JSONObject(); } - this.presences = Presences.fromJsonString(presences); + } + + public Contact(String jid) { + this.jid = jid; } public String getDisplayName() { - return this.displayName; + if (this.systemName != null) { + return this.systemName; + } else if (this.serverName != null) { + return this.serverName; + } else { + return this.jid.split("@")[0]; + } } public String getProfilePhoto() { @@ -90,35 +82,32 @@ public class Contact extends AbstractEntity implements Serializable { } public boolean match(String needle) { - return (jid.toLowerCase().contains(needle.toLowerCase()) || (displayName + return (jid.toLowerCase().contains(needle.toLowerCase()) || (getDisplayName() .toLowerCase().contains(needle.toLowerCase()))); } - @Override public ContentValues getContentValues() { ContentValues values = new ContentValues(); - values.put(UUID, uuid); values.put(ACCOUNT, accountUuid); - values.put(DISPLAYNAME, displayName); + values.put(SYSTEMNAME, systemName); + values.put(SERVERNAME, serverName); values.put(JID, jid); - values.put(SUBSCRIPTION, subscription); + values.put(OPTIONS, subscription); values.put(SYSTEMACCOUNT, systemAccount); values.put(PHOTOURI, photoUri); values.put(KEYS, keys.toString()); - values.put(PRESENCES, presences.toJsonString()); return values; } public static Contact fromCursor(Cursor cursor) { - return new Contact(cursor.getString(cursor.getColumnIndex(UUID)), - cursor.getString(cursor.getColumnIndex(ACCOUNT)), - cursor.getString(cursor.getColumnIndex(DISPLAYNAME)), + return new Contact(cursor.getString(cursor.getColumnIndex(ACCOUNT)), + cursor.getString(cursor.getColumnIndex(SYSTEMNAME)), + cursor.getString(cursor.getColumnIndex(SERVERNAME)), cursor.getString(cursor.getColumnIndex(JID)), - cursor.getInt(cursor.getColumnIndex(SUBSCRIPTION)), + cursor.getInt(cursor.getColumnIndex(OPTIONS)), cursor.getString(cursor.getColumnIndex(PHOTOURI)), cursor.getString(cursor.getColumnIndex(SYSTEMACCOUNT)), - cursor.getString(cursor.getColumnIndex(KEYS)), - cursor.getString(cursor.getColumnIndex(PRESENCES))); + cursor.getString(cursor.getColumnIndex(KEYS))); } public int getSubscription() { @@ -138,10 +127,6 @@ public class Contact extends AbstractEntity implements Serializable { return this.account; } - public void setUuid(String uuid) { - this.uuid = uuid; - } - public boolean couldBeMuc() { String[] split = this.getJid().split("@"); if (split.length != 2) { @@ -153,15 +138,17 @@ public class Contact extends AbstractEntity implements Serializable { } else { return (domainParts[0].equals("conf") || domainParts[0].equals("conference") + || domainParts[0].equals("room") || domainParts[0].equals("muc") + || domainParts[0].equals("chat") || domainParts[0].equals("sala") || domainParts[0].equals("salas")); } } } - public Hashtable<String, Integer> getPresences() { - return this.presences.getPresences(); + public Presences getPresences() { + return this.presences; } public void updatePresence(String resource, int status) { @@ -188,8 +175,12 @@ public class Contact extends AbstractEntity implements Serializable { this.photoUri = uri; } - public void setDisplayName(String name) { - this.displayName = name; + public void setServerName(String serverName) { + this.serverName = serverName; + } + + public void setSystemName(String systemName) { + this.systemName = systemName; } public String getSystemAccount() { @@ -249,58 +240,76 @@ public class Contact extends AbstractEntity implements Serializable { } } - public void setSubscriptionOption(int option) { + public void setOption(int option) { this.subscription |= 1 << option; } - public void resetSubscriptionOption(int option) { + public void resetOption(int option) { this.subscription &= ~(1 << option); } - public boolean getSubscriptionOption(int option) { + public boolean getOption(int option) { return ((this.subscription & (1 << option)) != 0); } + public boolean showInRoster() { + return (this.getOption(Contact.Options.IN_ROSTER) && (!this + .getOption(Contact.Options.DIRTY_DELETE))) + || (this.getOption(Contact.Options.DIRTY_PUSH)); + } + public void parseSubscriptionFromElement(Element item) { String ask = item.getAttribute("ask"); String subscription = item.getAttribute("subscription"); if (subscription != null) { if (subscription.equals("to")) { - this.resetSubscriptionOption(Contact.Subscription.FROM); - this.setSubscriptionOption(Contact.Subscription.TO); + this.resetOption(Contact.Options.FROM); + this.setOption(Contact.Options.TO); } else if (subscription.equals("from")) { - this.resetSubscriptionOption(Contact.Subscription.TO); - this.setSubscriptionOption(Contact.Subscription.FROM); + this.resetOption(Contact.Options.TO); + this.setOption(Contact.Options.FROM); } else if (subscription.equals("both")) { - this.setSubscriptionOption(Contact.Subscription.TO); - this.setSubscriptionOption(Contact.Subscription.FROM); + this.setOption(Contact.Options.TO); + this.setOption(Contact.Options.FROM); + } else if (subscription.equals("none")) { + this.resetOption(Contact.Options.FROM); + this.resetOption(Contact.Options.TO); } } - if ((ask != null) && (ask.equals("subscribe"))) { - this.setSubscriptionOption(Contact.Subscription.ASKING); - } else { - this.resetSubscriptionOption(Contact.Subscription.ASKING); + // do NOT override asking if pending push request + if (!this.getOption(Contact.Options.DIRTY_PUSH)) { + if ((ask != null) && (ask.equals("subscribe"))) { + this.setOption(Contact.Options.ASKING); + } else { + this.resetOption(Contact.Options.ASKING); + } + } + } + + public Element asElement() { + Element item = new Element("item"); + item.setAttribute("jid", this.jid); + if (this.serverName != null) { + item.setAttribute("name", this.serverName); } + return item; } - public class Subscription { + public class Options { public static final int TO = 0; public static final int FROM = 1; public static final int ASKING = 2; - public static final int PREEMPTIVE_GRANT = 4; - } - - public void flagAsNotInRoster() { - this.inRoster = false; - } - - public boolean isInRoster() { - return this.inRoster; - } - - public String getAccountUuid() { - return this.accountUuid; + public static final int PREEMPTIVE_GRANT = 3; + public static final int IN_ROSTER = 4; + public static final int PENDING_SUBSCRIPTION_REQUEST = 5; + public static final int DIRTY_PUSH = 6; + public static final int DIRTY_DELETE = 7; + } + + public class Lastseen { + public long time = 0; + public String presence = null; } } diff --git a/src/eu/siacs/conversations/entities/Conversation.java b/src/eu/siacs/conversations/entities/Conversation.java index 5674f84a..640d89e9 100644 --- a/src/eu/siacs/conversations/entities/Conversation.java +++ b/src/eu/siacs/conversations/entities/Conversation.java @@ -4,13 +4,14 @@ import java.security.interfaces.DSAPublicKey; import java.util.ArrayList; import java.util.List; +import eu.siacs.conversations.services.XmppConnectionService; + import net.java.otr4j.OtrException; import net.java.otr4j.crypto.OtrCryptoEngineImpl; import net.java.otr4j.crypto.OtrCryptoException; import net.java.otr4j.session.SessionID; import net.java.otr4j.session.SessionImpl; import net.java.otr4j.session.SessionStatus; - import android.content.ContentValues; import android.content.Context; import android.database.Cursor; @@ -44,12 +45,11 @@ public class Conversation extends AbstractEntity { private int status; private long created; private int mode; - + private String nextPresence; private transient List<Message> messages = null; private transient Account account = null; - private transient Contact contact; private transient SessionImpl otrSession; @@ -60,6 +60,10 @@ public class Conversation extends AbstractEntity { private transient MucOptions mucOptions = null; + private transient String latestMarkableMessageId; + + private byte[] symmetricKey; + public Conversation(String name, Account account, String contactJid, int mode) { this(java.util.UUID.randomUUID().toString(), name, null, account @@ -101,15 +105,26 @@ public class Conversation extends AbstractEntity { } public void markRead() { - if (this.messages == null) + if (this.messages == null) { return; + } for (int i = this.messages.size() - 1; i >= 0; --i) { - if (messages.get(i).isRead()) - return; + if (messages.get(i).isRead()) { + break; + } this.messages.get(i).markRead(); } } + public void markRead(XmppConnectionService service) { + markRead(); + if (service.confirmMessages() && this.latestMarkableMessageId != null) { + service.sendConfirmMessage(getAccount(), getContactJid(), + this.latestMarkableMessageId); + this.latestMarkableMessageId = null; + } + } + public Message getLatestMessage() { if ((this.messages == null) || (this.messages.size() == 0)) { Message message = new Message(this, "", Message.ENCRYPTION_NONE); @@ -127,21 +142,16 @@ public class Conversation extends AbstractEntity { } public String getName(boolean useSubject) { - if ((getMode() == MODE_MULTI) && (getMucOptions().getSubject() != null) && useSubject) { + if ((getMode() == MODE_MULTI) && (getMucOptions().getSubject() != null) + && useSubject) { return getMucOptions().getSubject(); - } else if (this.contact != null) { - return this.contact.getDisplayName(); } else { - return this.name; + return this.getContact().getDisplayName(); } } public String getProfilePhotoString() { - if (this.contact == null) { - return null; - } else { - return this.contact.getProfilePhoto(); - } + return this.getContact().getProfilePhoto(); } public String getAccountUuid() { @@ -153,14 +163,7 @@ public class Conversation extends AbstractEntity { } public Contact getContact() { - return this.contact; - } - - public void setContact(Contact contact) { - this.contact = contact; - if (contact != null) { - this.contactUuid = contact.getUuid(); - } + return this.account.getRoster().getContact(this.contactJid); } public void setAccount(Account account) { @@ -222,14 +225,15 @@ public class Conversation extends AbstractEntity { this.mode = mode; } - public SessionImpl startOtrSession(Context context, String presence, boolean sendStart) { + public SessionImpl startOtrSession(Context context, String presence, + boolean sendStart) { if (this.otrSession != null) { return this.otrSession; } else { SessionID sessionId = new SessionID(this.getContactJid(), presence, "xmpp"); - this.otrSession = new SessionImpl(sessionId, getAccount().getOtrEngine( - context)); + this.otrSession = new SessionImpl(sessionId, getAccount() + .getOtrEngine(context)); try { if (sendStart) { this.otrSession.startSession(); @@ -240,13 +244,13 @@ public class Conversation extends AbstractEntity { return null; } } - + } public SessionImpl getOtrSession() { return this.otrSession; } - + public void resetOtrSession() { this.otrSession = null; } @@ -260,21 +264,14 @@ public class Conversation extends AbstractEntity { } catch (OtrException e) { this.resetOtrSession(); } + } else { + this.resetOtrSession(); } } } public boolean hasValidOtrSession() { - if (this.otrSession == null) { - return false; - } else { - String foreignPresence = this.otrSession.getSessionID().getUserID(); - if (!getContact().getPresences().containsKey(foreignPresence)) { - this.resetOtrSession(); - return false; - } - return true; - } + return this.otrSession != null; } public String getOtrFingerprint() { @@ -298,7 +295,7 @@ public class Conversation extends AbstractEntity { public synchronized MucOptions getMucOptions() { if (this.mucOptions == null) { - this.mucOptions = new MucOptions(); + this.mucOptions = new MucOptions(this.getAccount()); } this.mucOptions.setConversation(this); return this.mucOptions; @@ -311,40 +308,59 @@ public class Conversation extends AbstractEntity { public void setContactJid(String jid) { this.contactJid = jid; } - + public void setNextPresence(String presence) { this.nextPresence = presence; } - + public String getNextPresence() { return this.nextPresence; } - + public int getLatestEncryption() { int latestEncryption = this.getLatestMessage().getEncryption(); - if ((latestEncryption == Message.ENCRYPTION_DECRYPTED) || (latestEncryption == Message.ENCRYPTION_DECRYPTION_FAILED)) { + if ((latestEncryption == Message.ENCRYPTION_DECRYPTED) + || (latestEncryption == Message.ENCRYPTION_DECRYPTION_FAILED)) { return Message.ENCRYPTION_PGP; } else { return latestEncryption; } } - + public int getNextEncryption() { if (this.nextMessageEncryption == -1) { return this.getLatestEncryption(); } return this.nextMessageEncryption; } - + public void setNextEncryption(int encryption) { this.nextMessageEncryption = encryption; } - + public String getNextMessage() { - return this.nextMessage; + if (this.nextMessage == null) { + return ""; + } else { + return this.nextMessage; + } } - + public void setNextMessage(String message) { this.nextMessage = message; } + + public void setLatestMarkableMessageId(String id) { + if (id != null) { + this.latestMarkableMessageId = id; + } + } + + public void setSymmetricKey(byte[] key) { + this.symmetricKey = key; + } + + public byte[] getSymmetricKey() { + return this.symmetricKey; + } } diff --git a/src/eu/siacs/conversations/entities/Message.java b/src/eu/siacs/conversations/entities/Message.java index 33f7a8d4..1e82fe6a 100644 --- a/src/eu/siacs/conversations/entities/Message.java +++ b/src/eu/siacs/conversations/entities/Message.java @@ -19,7 +19,10 @@ public class Message extends AbstractEntity { public static final int STATUS_SEND = 2; public static final int STATUS_SEND_FAILED = 3; public static final int STATUS_SEND_REJECTED = 4; + public static final int STATUS_WAITING = 5; public static final int STATUS_OFFERED = 6; + public static final int STATUS_SEND_RECEIVED = 7; + public static final int STATUS_SEND_DISPLAYED = 8; public static final int ENCRYPTION_NONE = 0; public static final int ENCRYPTION_PGP = 1; @@ -29,6 +32,8 @@ 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_STATUS = 3; public static String CONVERSATION = "conversationUuid"; public static String COUNTERPART = "counterpart"; @@ -51,6 +56,10 @@ public class Message extends AbstractEntity { protected transient Conversation conversation = null; protected transient JingleConnection jingleConnection = null; + + private Message() { + + } public Message(Conversation conversation, String body, int encryption) { this(java.util.UUID.randomUUID().toString(), conversation.getUuid(), @@ -192,7 +201,20 @@ public class Message extends AbstractEntity { } public void setPresence(String presence) { - this.counterpart = this.counterpart.split("/")[0] + "/" + presence; + if (presence == null) { + this.counterpart = this.counterpart.split("/")[0]; + } else { + this.counterpart = this.counterpart.split("/")[0] + "/" + presence; + } + } + + public String getPresence() { + String[] counterparts = this.counterpart.split("/"); + if (counterparts.length == 2) { + return counterparts[1]; + } else { + return null; + } } public void setJingleConnection(JingleConnection connection) { @@ -202,4 +224,11 @@ public class Message extends AbstractEntity { public JingleConnection getJingleConnection() { return this.jingleConnection; } + + public static Message createStatusMessage(Conversation conversation) { + Message message = new Message(); + message.setType(Message.TYPE_STATUS); + message.setConversation(conversation); + return message; + } } diff --git a/src/eu/siacs/conversations/entities/MucOptions.java b/src/eu/siacs/conversations/entities/MucOptions.java index fbca8340..0f8e3565 100644 --- a/src/eu/siacs/conversations/entities/MucOptions.java +++ b/src/eu/siacs/conversations/entities/MucOptions.java @@ -3,11 +3,10 @@ package eu.siacs.conversations.entities; import java.util.ArrayList; import java.util.List; -import eu.siacs.conversations.entities.MucOptions.User; +import eu.siacs.conversations.crypto.PgpEngine; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.stanzas.PresencePacket; import android.annotation.SuppressLint; -import android.util.Log; @SuppressLint("DefaultLocale") public class MucOptions { @@ -31,6 +30,7 @@ public class MucOptions { private int role; private int affiliation; private String name; + private long pgpKeyId = 0; public String getName() { return name; @@ -70,7 +70,15 @@ public class MucOptions { this.affiliation = AFFILIATION_NONE; } } + public void setPgpKeyId(long id) { + this.pgpKeyId = id; + } + + public long getPgpKeyId() { + return this.pgpKeyId; + } } + private Account account; private ArrayList<User> users = new ArrayList<User>(); private Conversation conversation; private boolean isOnline = false; @@ -80,6 +88,9 @@ public class MucOptions { private User self = new User(); private String subject = null; + public MucOptions(Account account) { + this.account = account; + } public void deleteUser(String name) { for(int i = 0; i < users.size(); ++i) { @@ -100,7 +111,7 @@ public class MucOptions { users.add(user); } - public void processPacket(PresencePacket packet) { + public void processPacket(PresencePacket packet, PgpEngine pgp) { String[] fromParts = packet.getFrom().split("/"); if (fromParts.length>=2) { String name = fromParts[1]; @@ -119,6 +130,20 @@ public class MucOptions { } else { addUser(user); } + if (pgp != null) { + Element x = packet.findChild("x", + "jabber:x:signed"); + if (x != null) { + Element status = packet.findChild("status"); + String msg; + if (status != null) { + msg = status.getContent(); + } else { + msg = ""; + } + user.setPgpKeyId(pgp.fetchKeyId(account,msg, x.getContent())); + } + } } else if (type.equals("unavailable")) { if (name.equals(getNick())) { Element item = packet.findChild("x","http://jabber.org/protocol/muc#user").findChild("item"); @@ -211,4 +236,36 @@ public class MucOptions { public void flagAboutToRename() { this.aboutToRename = true; } + + public long[] getPgpKeyIds() { + List<Long> ids = new ArrayList<Long>(); + for(User user : getUsers()) { + if(user.getPgpKeyId()!=0) { + ids.add(user.getPgpKeyId()); + } + } + long[] primitivLongArray = new long[ids.size()]; + for(int i = 0; i < ids.size(); ++i) { + primitivLongArray[i] = ids.get(i); + } + return primitivLongArray; + } + + public boolean pgpKeysInUse() { + for(User user : getUsers()) { + if (user.getPgpKeyId()!=0) { + return true; + } + } + return false; + } + + public boolean everybodyHasKeys() { + for(User user : getUsers()) { + if (user.getPgpKeyId()==0) { + return false; + } + } + return true; + } }
\ No newline at end of file diff --git a/src/eu/siacs/conversations/entities/Presences.java b/src/eu/siacs/conversations/entities/Presences.java index acbaafca..77891648 100644 --- a/src/eu/siacs/conversations/entities/Presences.java +++ b/src/eu/siacs/conversations/entities/Presences.java @@ -4,10 +4,6 @@ import java.util.Hashtable; import java.util.Iterator; import java.util.Map.Entry; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - import eu.siacs.conversations.xml.Element; public class Presences { @@ -47,39 +43,6 @@ public class Presences { return status; } - public String toJsonString() { - JSONArray json = new JSONArray(); - Iterator<Entry<String, Integer>> it = presences.entrySet().iterator(); - - while (it.hasNext()) { - Entry<String, Integer> entry = it.next(); - JSONObject jObj = new JSONObject(); - try { - jObj.put("resource", entry.getKey()); - jObj.put("status", entry.getValue()); - } catch (JSONException e) { - - } - json.put(jObj); - } - return json.toString(); - } - - public static Presences fromJsonString(String jsonString) { - Presences presences = new Presences(); - try { - JSONArray json = new JSONArray(jsonString); - for (int i = 0; i < json.length(); ++i) { - JSONObject jObj = json.getJSONObject(i); - presences.updatePresence(jObj.getString("resource"), - jObj.getInt("status")); - } - } catch (JSONException e1) { - - } - return presences; - } - public static int parseShow(Element show) { if (show == null) { return Presences.ONLINE; @@ -99,4 +62,14 @@ public class Presences { public int size() { return presences.size(); } + + public String[] asStringArray() { + final String[] presencesArray = new String[presences.size()]; + presences.keySet().toArray(presencesArray); + return presencesArray; + } + + public boolean has(String presence) { + return presences.containsKey(presence); + } } diff --git a/src/eu/siacs/conversations/entities/Roster.java b/src/eu/siacs/conversations/entities/Roster.java new file mode 100644 index 00000000..c1e40dbc --- /dev/null +++ b/src/eu/siacs/conversations/entities/Roster.java @@ -0,0 +1,74 @@ +package eu.siacs.conversations.entities; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +public class Roster { + Account account; + ConcurrentHashMap<String, Contact> contacts = new ConcurrentHashMap<String, Contact>(); + private String version = null; + + public Roster(Account account) { + this.account = account; + } + + public boolean hasContact(String jid) { + String cleanJid = jid.split("/")[0]; + return contacts.containsKey(cleanJid); + } + + public Contact getContact(String jid) { + String cleanJid = jid.split("/")[0]; + if (contacts.containsKey(cleanJid)) { + return contacts.get(cleanJid); + } else { + Contact contact = new Contact(cleanJid); + contact.setAccount(account); + contacts.put(cleanJid, contact); + return contact; + } + } + + public void clearPresences() { + for(Contact contact : getContacts()) { + contact.clearPresences(); + } + } + + public void markAllAsNotInRoster() { + for(Contact contact : getContacts()) { + contact.resetOption(Contact.Options.IN_ROSTER); + } + } + + public void clearSystemAccounts() { + for(Contact contact : getContacts()) { + contact.setPhotoUri(null); + contact.setSystemName(null); + contact.setSystemAccount(null); + } + } + + public List<Contact> getContacts() { + return new ArrayList<Contact>(this.contacts.values()); + } + + public void initContact(Contact contact) { + contact.setAccount(account); + contact.setOption(Contact.Options.IN_ROSTER); + contacts.put(contact.getJid(),contact); + } + + public void setVersion(String version) { + this.version = version; + } + + public String getVersion() { + return this.version; + } + + public Account getAccount() { + return this.account; + } +} diff --git a/src/eu/siacs/conversations/parser/AbstractParser.java b/src/eu/siacs/conversations/parser/AbstractParser.java new file mode 100644 index 00000000..2bd839f1 --- /dev/null +++ b/src/eu/siacs/conversations/parser/AbstractParser.java @@ -0,0 +1,53 @@ +package eu.siacs.conversations.parser; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; + +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xml.Element; + +public abstract class AbstractParser { + + protected XmppConnectionService mXmppConnectionService; + + protected AbstractParser(XmppConnectionService service) { + this.mXmppConnectionService = service; + } + + protected long getTimestamp(Element packet) { + if (packet.hasChild("delay")) { + try { + String stamp = packet.findChild("delay").getAttribute( + "stamp"); + stamp = stamp.replace("Z", "+0000"); + Date date = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") + .parse(stamp); + return date.getTime(); + } catch (ParseException e) { + return System.currentTimeMillis(); + } + } else { + return System.currentTimeMillis(); + } + } + + protected void updateLastseen(Element packet, Account account, boolean presenceOverwrite) { + String[] fromParts = packet.getAttribute("from").split("/"); + String from = fromParts[0]; + String presence = null; + if (fromParts.length >= 2) { + presence = fromParts[1]; + } + Contact contact = account.getRoster().getContact(from); + long timestamp = getTimestamp(packet); + if (timestamp >= contact.lastseen.time) { + contact.lastseen.time = timestamp; + if ((presence!=null)&&(presenceOverwrite)) { + contact.lastseen.presence = presence; + } + } + } +} diff --git a/src/eu/siacs/conversations/parser/MessageParser.java b/src/eu/siacs/conversations/parser/MessageParser.java new file mode 100644 index 00000000..a435d055 --- /dev/null +++ b/src/eu/siacs/conversations/parser/MessageParser.java @@ -0,0 +1,223 @@ +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.entities.Account; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.CryptoHelper; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.stanzas.MessagePacket; + +public class MessageParser extends AbstractParser { + + public MessageParser(XmppConnectionService service) { + super(service); + } + + public Message parseChat(MessagePacket packet, Account account) { + String[] fromParts = packet.getFrom().split("/"); + Conversation conversation = mXmppConnectionService + .findOrCreateConversation(account, fromParts[0], false); + conversation.setLatestMarkableMessageId(getMarkableMessageId(packet)); + updateLastseen(packet, account,true); + String pgpBody = getPgpBody(packet); + if (pgpBody != null) { + return new Message(conversation, packet.getFrom(), pgpBody, + Message.ENCRYPTION_PGP, Message.STATUS_RECIEVED); + } else { + return new Message(conversation, packet.getFrom(), + packet.getBody(), Message.ENCRYPTION_NONE, + Message.STATUS_RECIEVED); + } + } + + public Message parseOtrChat(MessagePacket packet, Account account) { + boolean properlyAddressed = (packet.getTo().split("/").length == 2) + || (account.countPresences() == 1); + String[] fromParts = packet.getFrom().split("/"); + Conversation conversation = mXmppConnectionService + .findOrCreateConversation(account, fromParts[0], false); + updateLastseen(packet, account,true); + String body = packet.getBody(); + if (!conversation.hasValidOtrSession()) { + if (properlyAddressed) { + conversation.startOtrSession( + mXmppConnectionService.getApplicationContext(), + fromParts[1], false); + } else { + return null; + } + } else { + String foreignPresence = conversation.getOtrSession() + .getSessionID().getUserID(); + if (!foreignPresence.equals(fromParts[1])) { + conversation.resetOtrSession(); + if (properlyAddressed) { + conversation.startOtrSession( + mXmppConnectionService.getApplicationContext(), + fromParts[1], false); + } else { + return null; + } + } + } + try { + Session otrSession = conversation.getOtrSession(); + SessionStatus before = otrSession.getSessionStatus(); + body = otrSession.transformReceiving(body); + SessionStatus after = otrSession.getSessionStatus(); + if ((before != after) && (after == SessionStatus.ENCRYPTED)) { + mXmppConnectionService.onOtrSessionEstablished(conversation); + } else if ((before != after) && (after == SessionStatus.FINISHED)) { + conversation.resetOtrSession(); + } + if ((body == null) || (body.isEmpty())) { + return null; + } + if (body.startsWith(CryptoHelper.FILETRANSFER)) { + String key = body.substring(CryptoHelper.FILETRANSFER.length()); + conversation.setSymmetricKey(CryptoHelper.hexToBytes(key)); + Log.d("xmppService","new symmetric key: "+CryptoHelper.bytesToHex(conversation.getSymmetricKey())); + return null; + } + conversation.setLatestMarkableMessageId(getMarkableMessageId(packet)); + Message finishedMessage = new Message(conversation, packet.getFrom(), body, + Message.ENCRYPTION_OTR, Message.STATUS_RECIEVED); + finishedMessage.setTime(getTimestamp(packet)); + return finishedMessage; + } catch (Exception e) { + conversation.resetOtrSession(); + return null; + } + } + + public Message parseGroupchat(MessagePacket packet, Account account) { + int status; + String[] fromParts = packet.getFrom().split("/"); + Conversation conversation = mXmppConnectionService + .findOrCreateConversation(account, fromParts[0], true); + if (packet.hasChild("subject")) { + conversation.getMucOptions().setSubject( + packet.findChild("subject").getContent()); + mXmppConnectionService.updateUi(conversation, false); + return null; + } + if ((fromParts.length == 1)) { + return null; + } + String counterPart = fromParts[1]; + if (counterPart.equals(conversation.getMucOptions().getNick())) { + if (mXmppConnectionService.markMessage(conversation, + packet.getId(), Message.STATUS_SEND)) { + return null; + } else { + status = Message.STATUS_SEND; + } + } else { + status = Message.STATUS_RECIEVED; + } + String pgpBody = getPgpBody(packet); + conversation.setLatestMarkableMessageId(getMarkableMessageId(packet)); + Message finishedMessage; + if (pgpBody == null) { + finishedMessage = new Message(conversation, counterPart, packet.getBody(), + Message.ENCRYPTION_NONE, status); + } else { + finishedMessage= new Message(conversation, counterPart, pgpBody, + Message.ENCRYPTION_PGP, status); + } + finishedMessage.setTime(getTimestamp(packet)); + return finishedMessage; + } + + public Message parseCarbonMessage(MessagePacket packet, Account account) { + int status; + String fullJid; + Element forwarded; + if (packet.hasChild("received")) { + forwarded = packet.findChild("received").findChild("forwarded"); + status = Message.STATUS_RECIEVED; + } else if (packet.hasChild("sent")) { + forwarded = packet.findChild("sent").findChild("forwarded"); + status = Message.STATUS_SEND; + } else { + return null; + } + if (forwarded == null) { + return null; + } + Element message = forwarded.findChild("message"); + if ((message == null) || (!message.hasChild("body"))) + return null; // either malformed or boring + if (status == Message.STATUS_RECIEVED) { + fullJid = message.getAttribute("from"); + updateLastseen(message, account,true); + } else { + fullJid = message.getAttribute("to"); + } + String[] parts = fullJid.split("/"); + Conversation conversation = mXmppConnectionService + .findOrCreateConversation(account, parts[0], false); + conversation.setLatestMarkableMessageId(getMarkableMessageId(packet)); + String pgpBody = getPgpBody(message); + Message finishedMessage; + if (pgpBody != null) { + finishedMessage = new Message(conversation, fullJid, pgpBody,Message.ENCRYPTION_PGP, status); + } else { + String body = message.findChild("body").getContent(); + finishedMessage= new Message(conversation, fullJid, body,Message.ENCRYPTION_NONE, status); + } + finishedMessage.setTime(getTimestamp(message)); + return finishedMessage; + } + + public void parseError(MessagePacket packet, Account account) { + String[] fromParts = packet.getFrom().split("/"); + mXmppConnectionService.markMessage(account, fromParts[0], + packet.getId(), Message.STATUS_SEND_FAILED); + } + + public void parseNormal(MessagePacket packet, Account account) { + if (packet.hasChild("displayed","urn:xmpp:chat-markers:0")) { + String id = packet.findChild("displayed","urn:xmpp:chat-markers:0").getAttribute("id"); + String[] fromParts = packet.getFrom().split("/"); + updateLastseen(packet, account,true); + mXmppConnectionService.markMessage(account,fromParts[0], id, Message.STATUS_SEND_DISPLAYED); + } else if (packet.hasChild("received","urn:xmpp:chat-markers:0")) { + String id = packet.findChild("received","urn:xmpp:chat-markers:0").getAttribute("id"); + String[] fromParts = packet.getFrom().split("/"); + updateLastseen(packet, account,false); + mXmppConnectionService.markMessage(account,fromParts[0], id, Message.STATUS_SEND_RECEIVED); + } else if (packet.hasChild("x")) { + Element x = packet.findChild("x"); + if (x.hasChild("invite")) { + Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, packet.getFrom(), + true); + mXmppConnectionService.updateUi(conversation, false); + } + + } + } + + private String getPgpBody(Element message) { + Element child = message.findChild("x", "jabber:x:encrypted"); + if (child == null) { + return null; + } else { + return child.getContent(); + } + } + + private String getMarkableMessageId(Element message) { + if (message.hasChild("markable", "urn:xmpp:chat-markers:0")) { + return message.getAttribute("id"); + } else { + return null; + } + } + + +} diff --git a/src/eu/siacs/conversations/parser/PresenceParser.java b/src/eu/siacs/conversations/parser/PresenceParser.java new file mode 100644 index 00000000..2003d4cd --- /dev/null +++ b/src/eu/siacs/conversations/parser/PresenceParser.java @@ -0,0 +1,104 @@ +package eu.siacs.conversations.parser; + +import eu.siacs.conversations.crypto.PgpEngine; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Presences; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.stanzas.PresencePacket; + +public class PresenceParser extends AbstractParser { + + public PresenceParser(XmppConnectionService service) { + super(service); + } + + public void parseConferencePresence(PresencePacket packet, Account account) { + PgpEngine mPgpEngine = mXmppConnectionService.getPgpEngine(); + if (packet.hasChild("x", "http://jabber.org/protocol/muc#user")) { + Conversation muc = mXmppConnectionService.findMuc(packet + .getAttribute("from").split("/")[0], account); + if (muc != null) { + muc.getMucOptions().processPacket(packet, mPgpEngine); + } + } else if (packet.hasChild("x", "http://jabber.org/protocol/muc")) { + Conversation muc = mXmppConnectionService.findMuc(packet + .getAttribute("from").split("/")[0], account); + if (muc != null) { + int error = muc.getMucOptions().getError(); + muc.getMucOptions().processPacket(packet, mPgpEngine); + if (muc.getMucOptions().getError() != error) { + mXmppConnectionService.updateUi(muc, false); + } + } + } + } + + public void parseContactPresence(PresencePacket packet, Account account) { + String[] fromParts = packet.getAttribute("from").split("/"); + String type = packet.getAttribute("type"); + if (fromParts[0].equals(account.getJid())) { + if (fromParts.length == 2) { + if (type == null) { + account.updatePresence(fromParts[1], + Presences.parseShow(packet.findChild("show"))); + } else if (type.equals("unavailable")) { + account.removePresence(fromParts[1]); + } + } + + } else { + Contact contact = account.getRoster().getContact(packet.getFrom()); + if (type == null) { + if (fromParts.length == 2) { + int sizeBefore = contact.getPresences().size(); + contact.updatePresence(fromParts[1], + Presences.parseShow(packet.findChild("show"))); + PgpEngine pgp = mXmppConnectionService.getPgpEngine(); + if (pgp != null) { + Element x = packet.findChild("x", "jabber:x:signed"); + if (x != null) { + Element status = packet.findChild("status"); + String msg; + if (status != null) { + msg = status.getContent(); + } else { + msg = ""; + } + contact.setPgpKeyId(pgp.fetchKeyId(account, msg, + x.getContent())); + } + } + boolean online = sizeBefore < contact.getPresences().size(); + updateLastseen(packet, account,true); + mXmppConnectionService.onContactStatusChanged + .onContactStatusChanged(contact,online); + } + } else if (type.equals("unavailable")) { + if (fromParts.length != 2) { + contact.clearPresences(); + } else { + contact.removePresence(fromParts[1]); + } + mXmppConnectionService.onContactStatusChanged + .onContactStatusChanged(contact,false); + } else if (type.equals("subscribe")) { + if (contact.getOption(Contact.Options.PREEMPTIVE_GRANT)) { + mXmppConnectionService.sendPresenceUpdatesTo(contact); + contact.setOption(Contact.Options.FROM); + contact.resetOption(Contact.Options.PREEMPTIVE_GRANT); + if ((contact.getOption(Contact.Options.ASKING)) + && (!contact.getOption(Contact.Options.TO))) { + mXmppConnectionService + .requestPresenceUpdatesFrom(contact); + } + } else { + contact.setOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST); + } + } + } + } + +} diff --git a/src/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/eu/siacs/conversations/persistance/DatabaseBackend.java index 5a34dac6..fbf45d25 100644 --- a/src/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -2,20 +2,17 @@ package eu.siacs.conversations.persistance; import java.util.ArrayList; import java.util.List; -import java.util.UUID; import java.util.concurrent.CopyOnWriteArrayList; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.entities.Presences; -import android.content.ContentValues; +import eu.siacs.conversations.entities.Roster; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; -import android.os.Bundle; import android.util.Log; public class DatabaseBackend extends SQLiteOpenHelper { @@ -23,7 +20,15 @@ public class DatabaseBackend extends SQLiteOpenHelper { private static DatabaseBackend instance = null; private static final String DATABASE_NAME = "history"; - private static final int DATABASE_VERSION = 3; + private static final int DATABASE_VERSION = 5; + + private static String CREATE_CONTATCS_STATEMENT = "create table " + + Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, " + Contact.SERVERNAME + " TEXT, " + + Contact.SYSTEMNAME + " TEXT," + Contact.JID + " TEXT," + + Contact.KEYS + " TEXT," + Contact.PHOTOURI + " TEXT," + + Contact.OPTIONS + " NUMBER," + Contact.SYSTEMACCOUNT + + " NUMBER, " + "FOREIGN KEY(" + Contact.ACCOUNT + ") REFERENCES " + + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, UNIQUE("+Contact.ACCOUNT+", "+Contact.JID+") ON CONFLICT REPLACE);"; public DatabaseBackend(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); @@ -36,7 +41,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { + " TEXT PRIMARY KEY," + Account.USERNAME + " TEXT," + Account.SERVER + " TEXT," + Account.PASSWORD + " TEXT," + Account.ROSTERVERSION + " TEXT," + Account.OPTIONS - + " NUMBER, "+Account.KEYS+" TEXT)"); + + " NUMBER, " + Account.KEYS + " TEXT)"); db.execSQL("create table " + Conversation.TABLENAME + " (" + Conversation.UUID + " TEXT PRIMARY KEY, " + Conversation.NAME + " TEXT, " + Conversation.CONTACT + " TEXT, " @@ -50,31 +55,28 @@ public class DatabaseBackend extends SQLiteOpenHelper { + " TEXT PRIMARY KEY, " + Message.CONVERSATION + " TEXT, " + Message.TIME_SENT + " NUMBER, " + Message.COUNTERPART + " TEXT, " + Message.BODY + " TEXT, " + Message.ENCRYPTION - + " NUMBER, " + Message.STATUS + " NUMBER," +Message.TYPE +" NUMBER, FOREIGN KEY(" - + Message.CONVERSATION + ") REFERENCES " - + Conversation.TABLENAME + "(" + Conversation.UUID - + ") ON DELETE CASCADE);"); - db.execSQL("create table " + Contact.TABLENAME + "(" + Contact.UUID - + " TEXT PRIMARY KEY, " + Contact.ACCOUNT + " TEXT, " - + Contact.DISPLAYNAME + " TEXT," + Contact.JID + " TEXT," - + Contact.PRESENCES + " TEXT, " + Contact.KEYS - + " TEXT," + Contact.PHOTOURI + " TEXT," + Contact.SUBSCRIPTION - + " NUMBER," + Contact.SYSTEMACCOUNT + " NUMBER, " - + "FOREIGN KEY(" + Contact.ACCOUNT + ") REFERENCES " - + Account.TABLENAME + "(" + Account.UUID - + ") ON DELETE CASCADE);"); + + " NUMBER, " + Message.STATUS + " NUMBER," + Message.TYPE + + " NUMBER, FOREIGN KEY(" + Message.CONVERSATION + + ") REFERENCES " + Conversation.TABLENAME + "(" + + Conversation.UUID + ") ON DELETE CASCADE);"); + + db.execSQL(CREATE_CONTATCS_STATEMENT); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (oldVersion < 2 && newVersion >= 2) { - // enable compression by default. - db.execSQL("update " + Account.TABLENAME - + " set " + Account.OPTIONS + " = " + Account.OPTIONS + " | 8"); + db.execSQL("update " + Account.TABLENAME + " set " + + Account.OPTIONS + " = " + Account.OPTIONS + " | 8"); } if (oldVersion < 3 && newVersion >= 3) { - //add field type to message - db.execSQL("ALTER TABLE "+Message.TABLENAME+" ADD COLUMN "+Message.TYPE+" NUMBER");; + db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + + Message.TYPE + " NUMBER"); + } + if (oldVersion < 5 && newVersion >= 5) { + db.execSQL("DROP TABLE "+Contact.TABLENAME); + db.execSQL(CREATE_CONTATCS_STATEMENT); + db.execSQL("UPDATE "+Account.TABLENAME+ " SET "+Account.ROSTERVERSION+" = NULL"); } } @@ -99,7 +101,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { SQLiteDatabase db = this.getWritableDatabase(); db.insert(Account.TABLENAME, null, account.getContentValues()); } - + public void createContact(Contact contact) { SQLiteDatabase db = this.getWritableDatabase(); db.insert(Contact.TABLENAME, null, contact.getContentValues()); @@ -126,14 +128,26 @@ public class DatabaseBackend extends SQLiteOpenHelper { } return list; } + + public List<Message> getMessages(Conversation conversations, int limit) { + return getMessages(conversations, limit,-1); + } - public List<Message> getMessages(Conversation conversation, int limit) { + public List<Message> getMessages(Conversation conversation, int limit, long timestamp) { List<Message> list = new CopyOnWriteArrayList<Message>(); SQLiteDatabase db = this.getReadableDatabase(); - String[] selectionArgs = { conversation.getUuid() }; - Cursor cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION - + "=?", selectionArgs, null, null, Message.TIME_SENT + " DESC", - String.valueOf(limit)); + Cursor cursor; + if (timestamp==-1) { + String[] selectionArgs = { conversation.getUuid() }; + cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION + + "=?", selectionArgs, null, null, Message.TIME_SENT + " DESC", + String.valueOf(limit)); + } else { + String[] selectionArgs = { conversation.getUuid() , ""+timestamp}; + cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION + + "=? and "+Message.TIME_SENT+"<?", selectionArgs, null, null, Message.TIME_SENT + " DESC", + String.valueOf(limit)); + } if (cursor.getCount() > 0) { cursor.moveToLast(); do { @@ -145,10 +159,10 @@ public class DatabaseBackend extends SQLiteOpenHelper { public Conversation findConversation(Account account, String contactJid) { SQLiteDatabase db = this.getReadableDatabase(); - String[] selectionArgs = { account.getUuid(), contactJid+"%" }; + String[] selectionArgs = { account.getUuid(), contactJid + "%" }; Cursor cursor = db.query(Conversation.TABLENAME, null, - Conversation.ACCOUNT + "=? AND " + Conversation.CONTACTJID + " like ?", - selectionArgs, null, null, null); + Conversation.ACCOUNT + "=? AND " + Conversation.CONTACTJID + + " like ?", selectionArgs, null, null, null); if (cursor.getCount() == 0) return null; cursor.moveToFirst(); @@ -200,87 +214,32 @@ public class DatabaseBackend extends SQLiteOpenHelper { db.update(Message.TABLENAME, message.getContentValues(), Message.UUID + "=?", args); } - - public void updateContact(Contact contact, boolean updatePresences) { - SQLiteDatabase db = this.getWritableDatabase(); - String[] args = { contact.getUuid() }; - ContentValues values = contact.getContentValues(); - if (!updatePresences) { - values.remove(Contact.PRESENCES); - } else { - values.remove(Contact.DISPLAYNAME); - values.remove(Contact.PHOTOURI); - values.remove(Contact.SYSTEMACCOUNT); - } - db.update(Contact.TABLENAME, contact.getContentValues(), Contact.UUID - + "=?", args); - } - - public void clearPresences(Account account) { - SQLiteDatabase db = this.getWritableDatabase(); - String[] args = { account.getUuid() }; - ContentValues values = new ContentValues(); - values.put(Contact.PRESENCES,"[]"); - db.update(Contact.TABLENAME, values, Contact.ACCOUNT - + "=?", args); - } - - public void mergeContacts(List<Contact> contacts) { - SQLiteDatabase db = this.getWritableDatabase(); - for (int i = 0; i < contacts.size(); i++) { - Contact contact = contacts.get(i); - String[] columns = {Contact.UUID, Contact.PRESENCES}; - String[] args = {contact.getAccount().getUuid(), contact.getJid()}; - Cursor cursor = db.query(Contact.TABLENAME, columns,Contact.ACCOUNT+"=? AND "+Contact.JID+"=?", args, null, null, null); - if (cursor.getCount()>=1) { - cursor.moveToFirst(); - contact.setUuid(cursor.getString(0)); - updateContact(contact,false); - } else { - contact.setUuid(UUID.randomUUID().toString()); - createContact(contact); - } - } - } - public List<Contact> getContactsByAccount(Account account) { - List<Contact> list = new ArrayList<Contact>(); + public void readRoster(Roster roster) { SQLiteDatabase db = this.getReadableDatabase(); Cursor cursor; - if (account==null) { - cursor = db.query(Contact.TABLENAME, null, null, null, null, - null, null); - } else { - String args[] = {account.getUuid()}; - cursor = db.query(Contact.TABLENAME, null, Contact.ACCOUNT+"=?", args, null, - null, null); - } + String args[] = { roster.getAccount().getUuid() }; + cursor = db.query(Contact.TABLENAME, null, Contact.ACCOUNT + "=?", + args, null, null, null); while (cursor.moveToNext()) { - list.add(Contact.fromCursor(cursor)); + roster.initContact(Contact.fromCursor(cursor)); } - return list; } - public List<Contact> getContacts(String where) { - List<Contact> list = new ArrayList<Contact>(); - SQLiteDatabase db = this.getReadableDatabase(); - Cursor cursor = db.query(Contact.TABLENAME, null, where, null, null, null, null); - while (cursor.moveToNext()) { - list.add(Contact.fromCursor(cursor)); + public void writeRoster(Roster roster) { + Account account = roster.getAccount(); + SQLiteDatabase db = this.getWritableDatabase(); + for(Contact contact : roster.getContacts()) { + if (contact.getOption(Contact.Options.IN_ROSTER)) { + db.insert(Contact.TABLENAME, null, contact.getContentValues()); + } else { + String where = Contact.ACCOUNT + "=? AND "+Contact.JID+"=?"; + String[] whereArgs = {account.getUuid(), contact.getJid()}; + db.delete(Contact.TABLENAME, where, whereArgs); + } } - return list; - } - - public Contact findContact(Account account, String jid) { - SQLiteDatabase db = this.getReadableDatabase(); - String[] selectionArgs = { account.getUuid(), jid }; - Cursor cursor = db.query(Contact.TABLENAME, null, - Contact.ACCOUNT + "=? AND " + Contact.JID + "=?", - selectionArgs, null, null, null); - if (cursor.getCount() == 0) - return null; - cursor.moveToFirst(); - return Contact.fromCursor(cursor); + account.setRosterVersion(roster.getVersion()); + updateAccount(account); } public void deleteMessage(Message message) { @@ -288,34 +247,18 @@ public class DatabaseBackend extends SQLiteOpenHelper { String[] args = { message.getUuid() }; db.delete(Message.TABLENAME, Message.UUID + "=?", args); } - + public void deleteMessagesInConversation(Conversation conversation) { SQLiteDatabase db = this.getWritableDatabase(); String[] args = { conversation.getUuid() }; db.delete(Message.TABLENAME, Message.CONVERSATION + "=?", args); } - public void deleteContact(Contact contact) { - SQLiteDatabase db = this.getWritableDatabase(); - String[] args = { contact.getUuid() }; - db.delete(Contact.TABLENAME, Contact.UUID + "=?", args); - } - - public Contact getContact(String uuid) { - SQLiteDatabase db = this.getWritableDatabase(); - String[] args = { uuid }; - Cursor cursor = db.query(Contact.TABLENAME, null, Contact.UUID + "=?", args, null, null, null); - if (cursor.getCount() == 0) { - return null; - } - cursor.moveToFirst(); - return Contact.fromCursor(cursor); - } - public Conversation findConversationByUuid(String conversationUuid) { SQLiteDatabase db = this.getReadableDatabase(); String[] selectionArgs = { conversationUuid }; - Cursor cursor = db.query(Conversation.TABLENAME, null, Conversation.UUID + "=?", selectionArgs, null, null, null); + Cursor cursor = db.query(Conversation.TABLENAME, null, + Conversation.UUID + "=?", selectionArgs, null, null, null); if (cursor.getCount() == 0) { return null; } @@ -326,18 +269,20 @@ public class DatabaseBackend extends SQLiteOpenHelper { public Message findMessageByUuid(String messageUuid) { SQLiteDatabase db = this.getReadableDatabase(); String[] selectionArgs = { messageUuid }; - Cursor cursor = db.query(Message.TABLENAME, null, Message.UUID + "=?", selectionArgs, null, null, null); + Cursor cursor = db.query(Message.TABLENAME, null, Message.UUID + "=?", + selectionArgs, null, null, null); if (cursor.getCount() == 0) { return null; } cursor.moveToFirst(); return Message.fromCursor(cursor); } - + public Account findAccountByUuid(String accountUuid) { SQLiteDatabase db = this.getReadableDatabase(); String[] selectionArgs = { accountUuid }; - Cursor cursor = db.query(Account.TABLENAME, null, Account.UUID + "=?", selectionArgs, null, null, null); + Cursor cursor = db.query(Account.TABLENAME, null, Account.UUID + "=?", + selectionArgs, null, null, null); if (cursor.getCount() == 0) { return null; } diff --git a/src/eu/siacs/conversations/persistance/FileBackend.java b/src/eu/siacs/conversations/persistance/FileBackend.java index becb1ee3..ca6360b9 100644 --- a/src/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/eu/siacs/conversations/persistance/FileBackend.java @@ -1,12 +1,12 @@ package eu.siacs.conversations.persistance; import java.io.File; +import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.lang.ref.WeakReference; import android.content.Context; import android.graphics.Bitmap; @@ -14,14 +14,9 @@ import android.graphics.BitmapFactory; import android.net.Uri; import android.util.Log; import android.util.LruCache; -import android.view.View; -import android.widget.ImageView; -import android.widget.TextView; - -import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xmpp.jingle.JingleFile; public class FileBackend { @@ -43,11 +38,11 @@ public class FileBackend { }; } - + public LruCache<String, Bitmap> getThumbnailCache() { return thumbnailCache; } - + public JingleFile getJingleFile(Message message) { return getJingleFile(message, true); } @@ -58,10 +53,14 @@ public class FileBackend { String path = prefix + "/" + conversation.getAccount().getJid() + "/" + conversation.getContactJid(); String filename; - if ((decrypted)||(message.getEncryption() == Message.ENCRYPTION_NONE)) { + if ((decrypted) || (message.getEncryption() == Message.ENCRYPTION_NONE)) { filename = message.getUuid() + ".webp"; } else { - filename = message.getUuid() + ".webp.pgp"; + if (message.getEncryption() == Message.ENCRYPTION_OTR) { + filename = message.getUuid() + ".webp"; + } else { + filename = message.getUuid() + ".webp.pgp"; + } } return new JingleFile(path + "/" + filename); } @@ -86,37 +85,64 @@ public class FileBackend { return originalBitmap; } } + + public JingleFile copyImageToPrivateStorage(Message message, Uri image) throws ImageCopyException { + return this.copyImageToPrivateStorage(message, image,0); + } - public JingleFile copyImageToPrivateStorage(Message message, Uri image) { + private JingleFile copyImageToPrivateStorage(Message message, Uri image, int sampleSize) + throws ImageCopyException { try { - Log.d("xmppService","copying file: "+image.toString()+ " to internal storage"); - InputStream is = context.getContentResolver() - .openInputStream(image); + InputStream is; + if (image != null) { + is = context.getContentResolver().openInputStream(image); + } else { + is = new FileInputStream(getIncomingFile()); + } JingleFile file = getJingleFile(message); file.getParentFile().mkdirs(); file.createNewFile(); - OutputStream os = new FileOutputStream(file); - Bitmap originalBitmap = BitmapFactory.decodeStream(is); + Bitmap originalBitmap; + BitmapFactory.Options options = new BitmapFactory.Options(); + int inSampleSize = (int) Math.pow(2, sampleSize); + Log.d("xmppService","reading bitmap with sample size "+inSampleSize); + options.inSampleSize = inSampleSize; + originalBitmap = BitmapFactory.decodeStream(is, null, options); is.close(); + if (originalBitmap == null) { + throw new ImageCopyException(R.string.error_not_an_image_file); + } + if (image == null) { + getIncomingFile().delete(); + } Bitmap scalledBitmap = resize(originalBitmap, IMAGE_SIZE); + OutputStream os = new FileOutputStream(file); boolean success = scalledBitmap.compress( Bitmap.CompressFormat.WEBP, 75, os); if (!success) { - return null; + throw new ImageCopyException(R.string.error_compressing_image); } os.flush(); os.close(); long size = file.getSize(); int width = scalledBitmap.getWidth(); int height = scalledBitmap.getHeight(); - message.setBody(""+size+","+width+","+height); + message.setBody("" + size + "," + width + "," + height); return file; } catch (FileNotFoundException e) { - return null; + throw new ImageCopyException(R.string.error_file_not_found); } catch (IOException e) { - return null; + throw new ImageCopyException(R.string.error_io_exception); } catch (SecurityException e) { - return null; + throw new ImageCopyException( + R.string.error_security_exception_during_image_copy); + } catch (OutOfMemoryError e) { + ++sampleSize; + if (sampleSize<=3) { + return copyImageToPrivateStorage(message, image, sampleSize); + } else { + throw new ImageCopyException(R.string.error_out_of_memory); + } } } @@ -128,7 +154,7 @@ public class FileBackend { public Bitmap getThumbnail(Message message, int size, boolean cacheOnly) throws FileNotFoundException { Bitmap thumbnail = thumbnailCache.get(message.getUuid()); - if ((thumbnail == null)&&(!cacheOnly)) { + if ((thumbnail == null) && (!cacheOnly)) { Bitmap fullsize = BitmapFactory.decodeFile(getJingleFile(message) .getAbsolutePath()); if (fullsize == null) { @@ -160,4 +186,21 @@ public class FileBackend { } f.delete(); } + + public File getIncomingFile() { + return new File(context.getFilesDir().getAbsolutePath() + "/incoming"); + } + + public class ImageCopyException extends Exception { + private static final long serialVersionUID = -1010013599132881427L; + private int resId; + + public ImageCopyException(int resId) { + this.resId = resId; + } + + public int getResId() { + return resId; + } + } } diff --git a/src/eu/siacs/conversations/services/ImageProvider.java b/src/eu/siacs/conversations/services/ImageProvider.java index c1bae661..80cfbd95 100644 --- a/src/eu/siacs/conversations/services/ImageProvider.java +++ b/src/eu/siacs/conversations/services/ImageProvider.java @@ -2,13 +2,13 @@ package eu.siacs.conversations.services; import java.io.File; import java.io.FileNotFoundException; +import java.io.IOException; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.persistance.DatabaseBackend; import eu.siacs.conversations.persistance.FileBackend; - import android.content.ContentProvider; import android.content.ContentValues; import android.database.Cursor; @@ -21,46 +21,62 @@ public class ImageProvider extends ContentProvider { @Override public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { - DatabaseBackend databaseBackend = DatabaseBackend - .getInstance(getContext()); + ParcelFileDescriptor pfd; FileBackend fileBackend = new FileBackend(getContext()); - String uuids = uri.getPath(); - Log.d("xmppService", "uuids = " + uuids); - if (uuids == null) { - throw new FileNotFoundException(); - } - String[] uuidsSplited = uuids.split("/"); - if (uuidsSplited.length != 3) { + if ("r".equals(mode)) { + DatabaseBackend databaseBackend = DatabaseBackend + .getInstance(getContext()); + String uuids = uri.getPath(); + Log.d("xmppService", "uuids = " + uuids+" mode="+mode); + if (uuids == null) { + throw new FileNotFoundException(); + } + String[] uuidsSplited = uuids.split("/"); + if (uuidsSplited.length != 3) { + throw new FileNotFoundException(); + } + String conversationUuid = uuidsSplited[1]; + String messageUuid = uuidsSplited[2].split("\\.")[0]; + + Log.d("xmppService","messageUuid="+messageUuid); + + Conversation conversation = databaseBackend + .findConversationByUuid(conversationUuid); + if (conversation == null) { + throw new FileNotFoundException("conversation " + conversationUuid + + " could not be found"); + } + Message message = databaseBackend.findMessageByUuid(messageUuid); + if (message == null) { + throw new FileNotFoundException("message " + messageUuid + + " could not be found"); + } + + Account account = databaseBackend.findAccountByUuid(conversation + .getAccountUuid()); + if (account == null) { + throw new FileNotFoundException("account " + + conversation.getAccountUuid() + " cound not be found"); + } + message.setConversation(conversation); + conversation.setAccount(account); + + File file = fileBackend.getJingleFile(message); + pfd = ParcelFileDescriptor.open(file, + ParcelFileDescriptor.MODE_READ_ONLY); + return pfd; + } else if ("w".equals(mode)){ + File file = fileBackend.getIncomingFile(); + try { + file.createNewFile(); + } catch (IOException e) { + throw new FileNotFoundException(); + } + pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE); + return pfd; + } else { throw new FileNotFoundException(); } - String conversationUuid = uuidsSplited[1]; - String messageUuid = uuidsSplited[2]; - - Conversation conversation = databaseBackend - .findConversationByUuid(conversationUuid); - if (conversation == null) { - throw new FileNotFoundException("conversation " + conversationUuid - + " could not be found"); - } - Message message = databaseBackend.findMessageByUuid(messageUuid); - if (message == null) { - throw new FileNotFoundException("message " + messageUuid - + " could not be found"); - } - - Account account = databaseBackend.findAccountByUuid(conversation - .getAccountUuid()); - if (account == null) { - throw new FileNotFoundException("account " - + conversation.getAccountUuid() + " cound not be found"); - } - message.setConversation(conversation); - conversation.setAccount(account); - - File file = fileBackend.getJingleFile(message); - ParcelFileDescriptor pfd = ParcelFileDescriptor.open(file, - ParcelFileDescriptor.MODE_READ_ONLY); - return pfd; } @Override @@ -93,5 +109,17 @@ public class ImageProvider extends ContentProvider { public int update(Uri arg0, ContentValues arg1, String arg2, String[] arg3) { return 0; } + + public static Uri getContentUri(Message message) { + return Uri + .parse("content://eu.siacs.conversations.images/" + + message.getConversationUuid() + + "/" + + message.getUuid() + + ".webp"); + } -} + public static Uri getIncomingContentUri() { + return Uri.parse("content://eu.siacs.conversations.images/incoming"); + } +}
\ No newline at end of file diff --git a/src/eu/siacs/conversations/services/XmppConnectionService.java b/src/eu/siacs/conversations/services/XmppConnectionService.java index d2742997..505d09e5 100644 --- a/src/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/eu/siacs/conversations/services/XmppConnectionService.java @@ -1,23 +1,18 @@ package eu.siacs.conversations.services; -import java.text.ParseException; -import java.text.SimpleDateFormat; +import java.security.SecureRandom; import java.util.Collections; import java.util.Comparator; -import java.util.Date; import java.util.Hashtable; import java.util.List; import java.util.Locale; -import java.util.Random; -import org.openintents.openpgp.OpenPgpError; import org.openintents.openpgp.util.OpenPgpApi; import org.openintents.openpgp.util.OpenPgpServiceConnection; import net.java.otr4j.OtrException; import net.java.otr4j.session.Session; import net.java.otr4j.session.SessionStatus; -import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.PgpEngine; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; @@ -26,20 +21,22 @@ import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.MucOptions.OnRenameListener; import eu.siacs.conversations.entities.Presences; +import eu.siacs.conversations.parser.MessageParser; +import eu.siacs.conversations.parser.PresenceParser; import eu.siacs.conversations.persistance.DatabaseBackend; import eu.siacs.conversations.persistance.FileBackend; -import eu.siacs.conversations.persistance.OnPhoneContactsMerged; import eu.siacs.conversations.ui.OnAccountListChangedListener; import eu.siacs.conversations.ui.OnConversationListChangedListener; -import eu.siacs.conversations.ui.OnRosterFetchedListener; import eu.siacs.conversations.ui.UiCallback; +import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.ExceptionHelper; -import eu.siacs.conversations.utils.MessageParser; import eu.siacs.conversations.utils.OnPhoneContactsLoadedListener; +import eu.siacs.conversations.utils.PRNGFixes; import eu.siacs.conversations.utils.PhoneHelper; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.OnBindListener; +import eu.siacs.conversations.xmpp.OnContactStatusChanged; import eu.siacs.conversations.xmpp.OnIqPacketReceived; import eu.siacs.conversations.xmpp.OnMessagePacketReceived; import eu.siacs.conversations.xmpp.OnPresencePacketReceived; @@ -47,12 +44,12 @@ import eu.siacs.conversations.xmpp.OnStatusChanged; import eu.siacs.conversations.xmpp.OnTLSExceptionReceived; import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; -import eu.siacs.conversations.xmpp.jingle.JingleFile; import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived; import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; import eu.siacs.conversations.xmpp.stanzas.IqPacket; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; import eu.siacs.conversations.xmpp.stanzas.PresencePacket; +import android.annotation.SuppressLint; import android.app.AlarmManager; import android.app.PendingIntent; import android.app.Service; @@ -60,7 +57,6 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.database.ContentObserver; -import android.database.DatabaseUtils; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; @@ -88,6 +84,11 @@ public class XmppConnectionService extends Service { private static final int CONNECT_TIMEOUT = 60; private static final long CARBON_GRACE_PERIOD = 60000L; + private static String ACTION_MERGE_PHONE_CONTACTS = "merge_phone_contacts"; + + private MessageParser mMessageParser = new MessageParser(this); + private PresenceParser mPresenceParser = new PresenceParser(this); + private List<Account> accounts; private List<Conversation> conversations = null; private JingleConnectionManager mJingleConnectionManager = new JingleConnectionManager( @@ -97,13 +98,26 @@ public class XmppConnectionService extends Service { private int convChangedListenerCount = 0; private OnAccountListChangedListener accountChangedListener = null; private OnTLSExceptionReceived tlsException = null; + public OnContactStatusChanged onContactStatusChanged = new OnContactStatusChanged() { + + @Override + public void onContactStatusChanged(Contact contact, boolean online) { + Conversation conversation = findActiveConversation(contact); + if (conversation != null) { + conversation.endOtrIfNeeded(); + if (online && (contact.getPresences().size() == 1)) { + sendUnsendMessages(conversation); + } + } + } + }; public void setOnTLSExceptionReceivedListener( OnTLSExceptionReceived listener) { tlsException = listener; } - private Random mRandom = new Random(System.currentTimeMillis()); + private SecureRandom mRandom; private long lastCarbonMessageReceived = -CARBON_GRACE_PERIOD; @@ -111,13 +125,13 @@ public class XmppConnectionService extends Service { @Override public void onChange(boolean selfChange) { super.onChange(selfChange); - Log.d(LOGTAG, "contact list has changed"); - mergePhoneContactsWithRoster(null); + Intent intent = new Intent(getApplicationContext(), + XmppConnectionService.class); + intent.setAction(ACTION_MERGE_PHONE_CONTACTS); + startService(intent); } }; - private XmppConnectionService service = this; - private final IBinder mBinder = new XmppConnectionBinder(); private OnMessagePacketReceived messageListener = new OnMessagePacketReceived() { @@ -132,26 +146,19 @@ public class XmppConnectionService extends Service { } if ((packet.getType() == MessagePacket.TYPE_CHAT)) { - String pgpBody = MessageParser.getPgpBody(packet); - if (pgpBody != null) { - message = MessageParser.parsePgpChat(pgpBody, packet, - account, service); - message.markUnread(); - } else if ((packet.getBody() != null) + if ((packet.getBody() != null) && (packet.getBody().startsWith("?OTR"))) { - message = MessageParser.parseOtrChat(packet, account, - service); + message = mMessageParser.parseOtrChat(packet, account); if (message != null) { message.markUnread(); } } else if (packet.hasChild("body")) { - message = MessageParser.parsePlainTextChat(packet, account, - service); + message = mMessageParser.parseChat(packet, account); message.markUnread(); } else if (packet.hasChild("received") || (packet.hasChild("sent"))) { - message = MessageParser.parseCarbonMessage(packet, account, - service); + message = mMessageParser + .parseCarbonMessage(packet, account); if (message != null) { if (message.getStatus() == Message.STATUS_SEND) { lastCarbonMessageReceived = SystemClock @@ -165,8 +172,7 @@ public class XmppConnectionService extends Service { } } else if (packet.getType() == MessagePacket.TYPE_GROUPCHAT) { - message = MessageParser - .parseGroupchat(packet, account, service); + message = mMessageParser.parseGroupchat(packet, account); if (message != null) { if (message.getStatus() == Message.STATUS_RECIEVED) { message.markUnread(); @@ -176,38 +182,31 @@ public class XmppConnectionService extends Service { } } } else if (packet.getType() == MessagePacket.TYPE_ERROR) { - MessageParser.parseError(packet, account, service); + mMessageParser.parseError(packet, account); return; } else if (packet.getType() == MessagePacket.TYPE_NORMAL) { - if (packet.hasChild("x")) { - Element x = packet.findChild("x"); - if (x.hasChild("invite")) { - findOrCreateConversation(account, packet.getFrom(), - true); - if (convChangedListener != null) { - convChangedListener.onConversationListChanged(); - } - Log.d(LOGTAG, - "invitation received to " + packet.getFrom()); - } - - } else { - // Log.d(LOGTAG, "unparsed message " + packet.toString()); - } + mMessageParser.parseNormal(packet, account); } if ((message == null) || (message.getBody() == null)) { return; } - if (packet.hasChild("delay")) { - try { - String stamp = packet.findChild("delay").getAttribute( - "stamp"); - stamp = stamp.replace("Z", "+0000"); - Date date = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") - .parse(stamp); - message.setTime(date.getTime()); - } catch (ParseException e) { - Log.d(LOGTAG, "error trying to parse date" + e.getMessage()); + if ((confirmMessages()) && ((packet.getId() != null))) { + MessagePacket receivedPacket = new MessagePacket(); + receivedPacket.setType(MessagePacket.TYPE_NORMAL); + receivedPacket.setTo(message.getCounterpart()); + receivedPacket.setFrom(account.getFullJid()); + if (packet.hasChild("markable", "urn:xmpp:chat-markers:0")) { + Element received = receivedPacket.addChild("received", + "urn:xmpp:chat-markers:0"); + received.setAttribute("id", packet.getId()); + account.getXmppConnection().sendMessagePacket( + receivedPacket); + } else if (packet.hasChild("request", "urn:xmpp:receipts")) { + Element received = receivedPacket.addChild("received", + "urn:xmpp:receipts"); + received.setAttribute("id", packet.getId()); + account.getXmppConnection().sendMessagePacket( + receivedPacket); } } Conversation conversation = message.getConversation(); @@ -234,9 +233,11 @@ public class XmppConnectionService extends Service { List<Conversation> conversations = getConversations(); for (int i = 0; i < conversations.size(); ++i) { if (conversations.get(i).getAccount() == account) { + conversations.get(i).endOtrIfNeeded(); sendUnsendMessages(conversations.get(i)); } } + syncDirtyContacts(account); scheduleWakeupCall(PING_MAX_INTERVAL, true); } else if (account.getStatus() == Account.STATUS_OFFLINE) { if (!account.isOptionSet(Account.OPTION_DISABLED)) { @@ -247,10 +248,18 @@ public class XmppConnectionService extends Service { } else if (account.getStatus() == Account.STATUS_REGISTRATION_SUCCESSFULL) { databaseBackend.updateAccount(account); reconnectAccount(account, true); - } else { - UIHelper.showErrorNotification(getApplicationContext(), - getAccounts()); + } else if ((account.getStatus() != Account.STATUS_CONNECTING) + && (account.getStatus() != Account.STATUS_NO_INTERNET)) { + int next = account.getXmppConnection().getTimeToNextAttempt(); + Log.d(LOGTAG, account.getJid() + + ": error connecting account. try again in " + next + + "s for the " + + (account.getXmppConnection().getAttempt() + 1) + + " time"); + scheduleWakeupCall(next, false); } + UIHelper.showErrorNotification(getApplicationContext(), + getAccounts()); } }; @@ -260,116 +269,11 @@ public class XmppConnectionService extends Service { public void onPresencePacketReceived(final Account account, PresencePacket packet) { if (packet.hasChild("x", "http://jabber.org/protocol/muc#user")) { - Conversation muc = findMuc( - packet.getAttribute("from").split("/")[0], account); - if (muc != null) { - muc.getMucOptions().processPacket(packet); - } else { - Log.d(LOGTAG, account.getJid() - + ": could not find muc for received muc package " - + packet.toString()); - } + mPresenceParser.parseConferencePresence(packet, account); } else if (packet.hasChild("x", "http://jabber.org/protocol/muc")) { - Conversation muc = findMuc( - packet.getAttribute("from").split("/")[0], account); - if (muc != null) { - Log.d(LOGTAG, - account.getJid() + ": reading muc status packet " - + packet.toString()); - int error = muc.getMucOptions().getError(); - muc.getMucOptions().processPacket(packet); - if ((muc.getMucOptions().getError() != error) - && (convChangedListener != null)) { - Log.d(LOGTAG, "muc error status changed"); - convChangedListener.onConversationListChanged(); - } - } + mPresenceParser.parseConferencePresence(packet, account); } else { - String[] fromParts = packet.getAttribute("from").split("/"); - String type = packet.getAttribute("type"); - if (fromParts[0].equals(account.getJid())) { - if (fromParts.length == 2) { - if (type == null) { - account.updatePresence(fromParts[1], Presences - .parseShow(packet.findChild("show"))); - } else if (type.equals("unavailable")) { - account.removePresence(fromParts[1]); - } - } - - } else { - Contact contact = findContact(account, fromParts[0]); - if (contact == null) { - if ("subscribe".equals(type)) { - account.getXmppConnection().addPendingSubscription( - fromParts[0]); - } else { - // Log.d(LOGTAG,packet.getFrom()+ - // " could not be found"); - } - return; - } - if (type == null) { - if (fromParts.length == 2) { - contact.updatePresence(fromParts[1], Presences - .parseShow(packet.findChild("show"))); - PgpEngine pgp = getPgpEngine(); - if (pgp != null) { - Element x = packet.findChild("x", - "jabber:x:signed"); - if (x != null) { - Element status = packet.findChild("status"); - String msg; - if (status != null) { - msg = status.getContent(); - } else { - msg = ""; - } - contact.setPgpKeyId(pgp.fetchKeyId(account,msg,x.getContent())); - Log.d("xmppService",account.getJid()+": fetched key id for "+contact.getJid()+" was:"+contact.getPgpKeyId()); - } - } - replaceContactInConversation(account, - contact.getJid(), contact); - databaseBackend.updateContact(contact, true); - } else { - // Log.d(LOGTAG,"presence without resource "+packet.toString()); - } - } else if (type.equals("unavailable")) { - if (fromParts.length != 2) { - contact.clearPresences(); - } else { - contact.removePresence(fromParts[1]); - } - replaceContactInConversation(account, contact.getJid(), - contact); - databaseBackend.updateContact(contact, true); - } else if (type.equals("subscribe")) { - Log.d(LOGTAG, "received subscribe packet from " - + packet.getFrom()); - if (contact - .getSubscriptionOption(Contact.Subscription.PREEMPTIVE_GRANT)) { - Log.d(LOGTAG, "preemptive grant; granting"); - sendPresenceUpdatesTo(contact); - contact.setSubscriptionOption(Contact.Subscription.FROM); - contact.resetSubscriptionOption(Contact.Subscription.PREEMPTIVE_GRANT); - replaceContactInConversation(account, - contact.getJid(), contact); - databaseBackend.updateContact(contact, false); - if ((contact - .getSubscriptionOption(Contact.Subscription.ASKING)) - && (!contact - .getSubscriptionOption(Contact.Subscription.TO))) { - requestPresenceUpdatesFrom(contact); - } - } else { - account.getXmppConnection().addPendingSubscription( - fromParts[0]); - } - } else { - // Log.d(LOGTAG, packet.toString()); - } - } + mPresenceParser.parseContactPresence(packet, account); } } }; @@ -378,14 +282,13 @@ public class XmppConnectionService extends Service { @Override public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.hasChild("query","jabber:iq:roster")) { + if (packet.hasChild("query", "jabber:iq:roster")) { String from = packet.getFrom(); - if ((from==null)||(from.equals(account.getJid()))) { + if ((from == null) || (from.equals(account.getJid()))) { Element query = packet.findChild("query"); processRosterItems(account, query); - mergePhoneContactsWithRoster(null); } else { - Log.d(LOGTAG,"unauthorized roster push from: "+from); + Log.d(LOGTAG, "unauthorized roster push from: " + from); } } else if (packet .hasChild("open", "http://jabber.org/protocol/ibb") @@ -393,20 +296,34 @@ public class XmppConnectionService extends Service { .hasChild("data", "http://jabber.org/protocol/ibb")) { XmppConnectionService.this.mJingleConnectionManager .deliverIbbPacket(account, packet); - } else if (packet.hasChild("query","http://jabber.org/protocol/disco#info")) { - IqPacket iqResponse = packet.generateRespone(IqPacket.TYPE_RESULT); - Element query = iqResponse.addChild("query", "http://jabber.org/protocol/disco#info"); - query.addChild("feature").setAttribute("var", "urn:xmpp:jingle:1"); - query.addChild("feature").setAttribute("var", "urn:xmpp:jingle:apps:file-transfer:3"); - query.addChild("feature").setAttribute("var", "urn:xmpp:jingle:transports:s5b:1"); - query.addChild("feature").setAttribute("var", "urn:xmpp:jingle:transports:ibb:1"); + } else if (packet.hasChild("query", + "http://jabber.org/protocol/disco#info")) { + IqPacket iqResponse = packet + .generateRespone(IqPacket.TYPE_RESULT); + Element query = iqResponse.addChild("query", + "http://jabber.org/protocol/disco#info"); + query.addChild("feature").setAttribute("var", + "urn:xmpp:jingle:1"); + query.addChild("feature").setAttribute("var", + "urn:xmpp:jingle:apps:file-transfer:3"); + query.addChild("feature").setAttribute("var", + "urn:xmpp:jingle:transports:s5b:1"); + query.addChild("feature").setAttribute("var", + "urn:xmpp:jingle:transports:ibb:1"); + if (confirmMessages()) { + query.addChild("feature").setAttribute("var", + "urn:xmpp:receipts"); + } account.getXmppConnection().sendIqPacket(iqResponse, null); } else { - if ((packet.getType() == IqPacket.TYPE_GET)||(packet.getType() == IqPacket.TYPE_SET)) { - IqPacket response = packet.generateRespone(IqPacket.TYPE_ERROR); + if ((packet.getType() == IqPacket.TYPE_GET) + || (packet.getType() == IqPacket.TYPE_SET)) { + IqPacket response = packet + .generateRespone(IqPacket.TYPE_ERROR); Element error = response.addChild("error"); - error.setAttribute("type","cancel"); - error.addChild("feature-not-implemented","urn:ietf:params:xml:ns:xmpp-stanzas"); + error.setAttribute("type", "cancel"); + error.addChild("feature-not-implemented", + "urn:ietf:params:xml:ns:xmpp-stanzas"); account.getXmppConnection().sendIqPacket(response, null); } } @@ -433,7 +350,7 @@ public class XmppConnectionService extends Service { if (this.mPgpEngine == null) { this.mPgpEngine = new PgpEngine(new OpenPgpApi( getApplicationContext(), - pgpServiceConnection.getService()),this); + pgpServiceConnection.getService()), this); } return mPgpEngine; } else { @@ -446,12 +363,14 @@ public class XmppConnectionService extends Service { return this.fileBackend; } - public Message attachImageToConversation(final Conversation conversation, final Uri uri, final UiCallback callback) { + public Message attachImageToConversation(final Conversation conversation, + final Uri uri, final UiCallback<Message> callback) { final Message message; if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) { - message = new Message(conversation, "",Message.ENCRYPTION_DECRYPTED); + message = new Message(conversation, "", + Message.ENCRYPTION_DECRYPTED); } else { - message = new Message(conversation, "", Message.ENCRYPTION_NONE); + message = new Message(conversation, "", conversation.getNextEncryption()); } message.setPresence(conversation.getNextPresence()); message.setType(Message.TYPE_IMAGE); @@ -460,22 +379,22 @@ public class XmppConnectionService extends Service { @Override public void run() { - JingleFile file = getFileBackend().copyImageToPrivateStorage(message, uri); - if (file==null) { - callback.error(R.string.error_copying_image_file); - } else { + try { + getFileBackend().copyImageToPrivateStorage(message, uri); if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) { getPgpEngine().encrypt(message, callback); } else { - callback.success(); + callback.success(message); } + } catch (FileBackend.ImageCopyException e) { + callback.error(e.getResId(), message); } } }).start(); return message; } - - protected Conversation findMuc(String name, Account account) { + + public Conversation findMuc(String name, Account account) { for (Conversation conversation : this.conversations) { if (conversation.getContactJid().split("/")[0].equals(name) && (conversation.getAccount() == account)) { @@ -488,51 +407,28 @@ public class XmppConnectionService extends Service { private void processRosterItems(Account account, Element elements) { String version = elements.getAttribute("ver"); if (version != null) { - account.setRosterVersion(version); - databaseBackend.updateAccount(account); + account.getRoster().setVersion(version); } for (Element item : elements.getChildren()) { if (item.getName().equals("item")) { String jid = item.getAttribute("jid"); + String name = item.getAttribute("name"); String subscription = item.getAttribute("subscription"); - Contact contact = databaseBackend.findContact(account, jid); - if (contact == null) { - if (!subscription.equals("remove")) { - String name = item.getAttribute("name"); - if (name == null) { - name = jid.split("@")[0]; - } - contact = new Contact(account, name, jid, null); - contact.parseSubscriptionFromElement(item); - databaseBackend.createContact(contact); - } + Contact contact = account.getRoster().getContact(jid); + if (!contact.getOption(Contact.Options.DIRTY_PUSH)) { + contact.setServerName(name); + } + if (subscription.equals("remove")) { + contact.resetOption(Contact.Options.IN_ROSTER); + contact.resetOption(Contact.Options.DIRTY_DELETE); } else { - if (subscription.equals("remove")) { - databaseBackend.deleteContact(contact); - replaceContactInConversation(account, contact.getJid(), - null); - } else { - contact.parseSubscriptionFromElement(item); - databaseBackend.updateContact(contact, false); - replaceContactInConversation(account, contact.getJid(), - contact); - } + contact.setOption(Contact.Options.IN_ROSTER); + contact.parseSubscriptionFromElement(item); } } } } - private void replaceContactInConversation(Account account, String jid, - Contact contact) { - List<Conversation> conversations = getConversations(); - for (Conversation c : conversations) { - if (c.getContactJid().equals(jid) && (c.getAccount() == account)) { - c.setContact(contact); - break; - } - } - } - public class XmppConnectionBinder extends Binder { public XmppConnectionService getService() { return XmppConnectionService.this; @@ -541,8 +437,16 @@ public class XmppConnectionService extends Service { @Override public int onStartCommand(Intent intent, int flags, int startId) { + if ((intent != null) + && (ACTION_MERGE_PHONE_CONTACTS.equals(intent.getAction()))) { + mergePhoneContactsWithRoster(); + return START_STICKY; + } else if ((intent != null) + && (Intent.ACTION_SHUTDOWN.equals(intent.getAction()))) { + logoutAndSave(); + return START_NOT_STICKY; + } this.wakeLock.acquire(); - // Log.d(LOGTAG,"calling start service. caller was:"+intent.getAction()); ConnectivityManager cm = (ConnectivityManager) getApplicationContext() .getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); @@ -563,8 +467,6 @@ public class XmppConnectionService extends Service { statusListener.onStatusChanged(account); } } - - // TODO 3 remaining cases if (account.getStatus() == Account.STATUS_ONLINE) { long lastReceived = account.getXmppConnection().lastPaketReceived; long lastSent = account.getXmppConnection().lastPingSent; @@ -592,15 +494,9 @@ public class XmppConnectionService extends Service { + ": time out during connect reconnecting"); reconnectAccount(account, true); } else { - Log.d(LOGTAG, - "seconds since last connect:" - + ((SystemClock.elapsedRealtime() - account - .getXmppConnection().lastConnect) / 1000)); - Log.d(LOGTAG, - account.getJid() + ": status=" - + account.getStatus()); - // TODO notify user of ssl cert problem or auth problem - // or what ever + if (account.getXmppConnection().getTimeToNextAttempt() <= 0) { + reconnectAccount(account, true); + } } // in any case. reschedule wakup call this.scheduleWakeupCall(PING_MAX_INTERVAL, true); @@ -616,14 +512,21 @@ public class XmppConnectionService extends Service { return START_STICKY; } + @SuppressLint("TrulyRandom") @Override public void onCreate() { ExceptionHelper.init(getApplicationContext()); + PRNGFixes.apply(); + this.mRandom = new SecureRandom(); this.databaseBackend = DatabaseBackend .getInstance(getApplicationContext()); this.fileBackend = new FileBackend(getApplicationContext()); this.accounts = databaseBackend.getAccounts(); + for (Account account : this.accounts) { + this.databaseBackend.readRoster(account.getRoster()); + } + this.mergePhoneContactsWithRoster(); this.getConversations(); getContentResolver().registerContentObserver( @@ -639,13 +542,30 @@ public class XmppConnectionService extends Service { @Override public void onDestroy() { - Log.d(LOGTAG,"stopping service"); super.onDestroy(); + this.logoutAndSave(); + } + + @Override + public void onTaskRemoved(Intent rootIntent) { + super.onTaskRemoved(rootIntent); + this.logoutAndSave(); + } + + private void logoutAndSave() { for (Account account : accounts) { + databaseBackend.writeRoster(account.getRoster()); if (account.getXmppConnection() != null) { - disconnect(account, true); + disconnect(account, false); } } + Context context = getApplicationContext(); + AlarmManager alarmManager = (AlarmManager) context + .getSystemService(Context.ALARM_SERVICE); + Intent intent = new Intent(context, EventReceiver.class); + alarmManager.cancel(PendingIntent.getBroadcast(context, 0, intent, 0)); + Log.d(LOGTAG, "good bye"); + stopSelf(); } protected void scheduleWakeupCall(int seconds, boolean ping) { @@ -663,7 +583,6 @@ public class XmppConnectionService extends Service { this.pingIntent, 0); alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, pendingPingIntent); - // Log.d(LOGTAG,"schedule ping in "+seconds+" seconds"); } else { long scheduledTime = this.pingIntent.getLongExtra("time", 0); if (scheduledTime < SystemClock.elapsedRealtime() @@ -674,7 +593,6 @@ public class XmppConnectionService extends Service { context, 0, this.pingIntent, 0); alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, pendingPingIntent); - // Log.d(LOGTAG,"reschedule old ping to ping in "+seconds+" seconds"); } } } else { @@ -692,7 +610,7 @@ public class XmppConnectionService extends Service { SharedPreferences sharedPref = getPreferences(); account.setResource(sharedPref.getString("resource", "mobile") .toLowerCase(Locale.getDefault())); - XmppConnection connection = new XmppConnection(account, this.pm); + XmppConnection connection = new XmppConnection(account, this); connection.setOnMessagePacketReceivedListener(this.messageListener); connection.setOnStatusChangedListener(this.statusListener); connection.setOnPresencePacketReceivedListener(this.presenceListener); @@ -715,12 +633,11 @@ public class XmppConnectionService extends Service { connection.setOnBindListener(new OnBindListener() { @Override - public void onBind(Account account) { - databaseBackend.clearPresences(account); + public void onBind(final Account account) { + account.getRoster().clearPresences(); account.clearPresences(); // self presences - if (account.getXmppConnection().hasFeatureRosterManagment()) { - updateRoster(account, null); - } + fetchRosterFromServer(account); + sendPresence(account); connectMultiModeConversations(account); if (convChangedListener != null) { convChangedListener.onConversationListChanged(); @@ -730,73 +647,86 @@ public class XmppConnectionService extends Service { return connection; } - synchronized public void sendMessage(Message message, String presence) { + synchronized public void sendMessage(Message message) { Account account = message.getConversation().getAccount(); Conversation conv = message.getConversation(); MessagePacket packet = null; - boolean saveInDb = false; - boolean addToConversation = false; + boolean saveInDb = true; boolean send = false; if (account.getStatus() == Account.STATUS_ONLINE) { if (message.getType() == Message.TYPE_IMAGE) { - mJingleConnectionManager.createNewConnection(message); + if (message.getPresence() != null) { + mJingleConnectionManager.createNewConnection(message); + } else { + message.setStatus(Message.STATUS_WAITING); + } } else { if (message.getEncryption() == Message.ENCRYPTION_OTR) { - if (!conv.hasValidOtrSession()) { + if (!conv.hasValidOtrSession() + && (message.getPresence() != null)) { // starting otr session. messages will be send later - conv.startOtrSession(getApplicationContext(), presence, - true); - } else if (conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) { + conv.startOtrSession(getApplicationContext(), + message.getPresence(), true); + } else if (conv.hasValidOtrSession() + && conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) { // otr session aleary exists, creating message packet // accordingly packet = prepareMessagePacket(account, message, conv.getOtrSession()); send = true; message.setStatus(Message.STATUS_SEND); + } else if (message.getPresence() == null) { + message.setStatus(Message.STATUS_WAITING); } - saveInDb = true; - addToConversation = true; } else if (message.getEncryption() == Message.ENCRYPTION_PGP) { message.getConversation().endOtrIfNeeded(); - packet = new MessagePacket(); - packet.setType(MessagePacket.TYPE_CHAT); - packet.setFrom(message.getConversation().getAccount() - .getFullJid()); - packet.setTo(message.getCounterpart()); + packet = prepareMessagePacket(account, message, null); packet.setBody("This is an XEP-0027 encryted message"); packet.addChild("x", "jabber:x:encrypted").setContent( message.getEncryptedBody()); message.setStatus(Message.STATUS_SEND); message.setEncryption(Message.ENCRYPTION_DECRYPTED); - saveInDb = true; - addToConversation = true; send = true; } else { message.getConversation().endOtrIfNeeded(); // don't encrypt if (message.getConversation().getMode() == Conversation.MODE_SINGLE) { message.setStatus(Message.STATUS_SEND); - saveInDb = true; - addToConversation = true; } packet = prepareMessagePacket(account, message, null); send = true; } } } else { - // account is offline - saveInDb = true; - addToConversation = true; + message.setStatus(Message.STATUS_WAITING); + if (message.getType() == Message.TYPE_TEXT) { + if (message.getEncryption() == Message.ENCRYPTION_PGP) { + String pgpBody = message.getEncryptedBody(); + String decryptedBody = message.getBody(); + message.setBody(pgpBody); + databaseBackend.createMessage(message); + saveInDb = false; + message.setEncryption(Message.ENCRYPTION_DECRYPTED); + message.setBody(decryptedBody); + } 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(getApplicationContext(), + message.getPresence(), false); + } + } + } } if (saveInDb) { databaseBackend.createMessage(message); } - if (addToConversation) { - conv.getMessages().add(message); - if (convChangedListener != null) { - convChangedListener.onConversationListChanged(); - } + conv.getMessages().add(message); + if (convChangedListener != null) { + convChangedListener.onConversationListChanged(); } if ((send) && (packet != null)) { account.getXmppConnection().sendMessagePacket(packet); @@ -806,20 +736,68 @@ public class XmppConnectionService extends Service { private void sendUnsendMessages(Conversation conversation) { for (int i = 0; i < conversation.getMessages().size(); ++i) { - if ((conversation.getMessages().get(i).getStatus() == Message.STATUS_UNSEND) - && (conversation.getMessages().get(i).getEncryption() == Message.ENCRYPTION_NONE)) { - Message message = conversation.getMessages().get(i); - MessagePacket packet = prepareMessagePacket( - conversation.getAccount(), message, null); - conversation.getAccount().getXmppConnection() - .sendMessagePacket(packet); - message.setStatus(Message.STATUS_SEND); - if (conversation.getMode() == Conversation.MODE_SINGLE) { - databaseBackend.updateMessage(message); - } else { - databaseBackend.deleteMessage(message); - conversation.getMessages().remove(i); - i--; + int status = conversation.getMessages().get(i).getStatus(); + if (status == Message.STATUS_WAITING) { + resendMessage(conversation.getMessages().get(i)); + } + } + } + + private void resendMessage(Message message) { + Account account = message.getConversation().getAccount(); + if (message.getType() == Message.TYPE_TEXT) { + MessagePacket packet = null; + if (message.getEncryption() == Message.ENCRYPTION_NONE) { + packet = prepareMessagePacket(account, message, null); + } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { + packet = prepareMessagePacket(account, message, null); + packet.setBody("This is an XEP-0027 encryted message"); + if (message.getEncryptedBody() == null) { + markMessage(message, Message.STATUS_SEND_FAILED); + return; + } + packet.addChild("x", "jabber:x:encrypted").setContent( + message.getEncryptedBody()); + } else if (message.getEncryption() == Message.ENCRYPTION_PGP) { + packet = prepareMessagePacket(account, message, null); + packet.setBody("This is an XEP-0027 encryted message"); + packet.addChild("x", "jabber:x:encrypted").setContent( + message.getBody()); + } else if (message.getEncryption() == Message.ENCRYPTION_OTR) { + Presences presences = message.getConversation().getContact() + .getPresences(); + if (!message.getConversation().hasValidOtrSession()) { + if ((message.getPresence() != null) + && (presences.has(message.getPresence()))) { + message.getConversation().startOtrSession( + getApplicationContext(), message.getPresence(), + true); + } else { + if (presences.size() == 1) { + String presence = presences.asStringArray()[0]; + message.getConversation().startOtrSession( + getApplicationContext(), presence, true); + } + } + } + } + if (packet != null) { + account.getXmppConnection().sendMessagePacket(packet); + markMessage(message, Message.STATUS_SEND); + } + } else if (message.getType() == Message.TYPE_IMAGE) { + Presences presences = message.getConversation().getContact() + .getPresences(); + if ((message.getPresence() != null) + && (presences.has(message.getPresence()))) { + markMessage(message, Message.STATUS_OFFERED); + mJingleConnectionManager.createNewConnection(message); + } else { + if (presences.size() == 1) { + String presence = presences.asStringArray()[0]; + message.setPresence(presence); + markMessage(message, Message.STATUS_OFFERED); + mJingleConnectionManager.createNewConnection(message); } } } @@ -830,6 +808,7 @@ public class XmppConnectionService extends Service { MessagePacket packet = new MessagePacket(); if (message.getConversation().getMode() == Conversation.MODE_SINGLE) { packet.setType(MessagePacket.TYPE_CHAT); + packet.setFrom(account.getFullJid()); if (otrSession != null) { try { packet.setBody(otrSession.transformSending(message @@ -844,12 +823,11 @@ public class XmppConnectionService extends Service { packet.addChild("no-copy", "urn:xmpp:hints"); packet.setTo(otrSession.getSessionID().getAccountID() + "/" + otrSession.getSessionID().getUserID()); - packet.setFrom(account.getFullJid()); } else { packet.setBody(message.getBody()); packet.setTo(message.getCounterpart()); - packet.setFrom(account.getJid()); } + packet.addChild("markable", "urn:xmpp:chat-markers:0"); } else if (message.getConversation().getMode() == Conversation.MODE_MULTI) { packet.setType(MessagePacket.TYPE_GROUPCHAT); packet.setBody(message.getBody()); @@ -860,27 +838,7 @@ public class XmppConnectionService extends Service { return packet; } - private void getRoster(Account account, - final OnRosterFetchedListener listener) { - List<Contact> contacts = databaseBackend.getContactsByAccount(account); - for (int i = 0; i < contacts.size(); ++i) { - contacts.get(i).setAccount(account); - } - if (listener != null) { - listener.onRosterFetched(contacts); - } - } - - public List<Contact> getRoster(Account account) { - List<Contact> contacts = databaseBackend.getContactsByAccount(account); - for (int i = 0; i < contacts.size(); ++i) { - contacts.get(i).setAccount(account); - } - return contacts; - } - - public void updateRoster(final Account account, - final OnRosterFetchedListener listener) { + public void fetchRosterFromServer(Account account) { IqPacket iqPacket = new IqPacket(IqPacket.TYPE_GET); if (!"".equals(account.getRosterVersion())) { Log.d(LOGTAG, account.getJid() + ": fetching roster version " @@ -898,62 +856,26 @@ public class XmppConnectionService extends Service { IqPacket packet) { Element roster = packet.findChild("query"); if (roster != null) { - Log.d(LOGTAG, account.getJid() - + ": processing roster"); + account.getRoster().markAllAsNotInRoster(); processRosterItems(account, roster); - StringBuilder mWhere = new StringBuilder(); - mWhere.append("jid NOT IN("); - List<Element> items = roster.getChildren(); - for (int i = 0; i < items.size(); ++i) { - mWhere.append(DatabaseUtils - .sqlEscapeString(items.get(i) - .getAttribute("jid"))); - if (i != items.size() - 1) { - mWhere.append(","); - } - } - mWhere.append(") and accountUuid = \""); - mWhere.append(account.getUuid()); - mWhere.append("\""); - List<Contact> contactsToDelete = databaseBackend - .getContacts(mWhere.toString()); - for (Contact contact : contactsToDelete) { - databaseBackend.deleteContact(contact); - replaceContactInConversation(account, - contact.getJid(), null); - } - - } else { - Log.d(LOGTAG, account.getJid() - + ": empty roster returend"); } - mergePhoneContactsWithRoster(new OnPhoneContactsMerged() { - - @Override - public void phoneContactsMerged() { - if (listener != null) { - getRoster(account, listener); - } - } - }); } }); } - public void mergePhoneContactsWithRoster( - final OnPhoneContactsMerged listener) { + private void mergePhoneContactsWithRoster() { PhoneHelper.loadPhoneContacts(getApplicationContext(), new OnPhoneContactsLoadedListener() { @Override - public void onPhoneContactsLoaded( - Hashtable<String, Bundle> phoneContacts) { - List<Contact> contacts = databaseBackend - .getContactsByAccount(null); - for (int i = 0; i < contacts.size(); ++i) { - Contact contact = contacts.get(i); - if (phoneContacts.containsKey(contact.getJid())) { - Bundle phoneContact = phoneContacts.get(contact - .getJid()); + public void onPhoneContactsLoaded(List<Bundle> phoneContacts) { + for (Account account : accounts) { + account.getRoster().clearSystemAccounts(); + } + for (Bundle phoneContact : phoneContacts) { + for (Account account : accounts) { + String jid = phoneContact.getString("jid"); + Contact contact = account.getRoster() + .getContact(jid); String systemAccount = phoneContact .getInt("phoneid") + "#" @@ -961,28 +883,10 @@ public class XmppConnectionService extends Service { contact.setSystemAccount(systemAccount); contact.setPhotoUri(phoneContact .getString("photouri")); - contact.setDisplayName(phoneContact + contact.setSystemName(phoneContact .getString("displayname")); - databaseBackend.updateContact(contact, false); - replaceContactInConversation( - contact.getAccount(), contact.getJid(), - contact); - } else { - if ((contact.getSystemAccount() != null) - || (contact.getProfilePhoto() != null)) { - contact.setSystemAccount(null); - contact.setPhotoUri(null); - databaseBackend.updateContact(contact, - false); - replaceContactInConversation( - contact.getAccount(), - contact.getJid(), contact); - } } } - if (listener != null) { - listener.phoneContactsMerged(); - } } }); } @@ -998,30 +902,47 @@ public class XmppConnectionService extends Service { for (Conversation conv : this.conversations) { Account account = accountLookupTable.get(conv.getAccountUuid()); conv.setAccount(account); - conv.setContact(findContact(account, conv.getContactJid())); conv.setMessages(databaseBackend.getMessages(conv, 50)); } } Collections.sort(this.conversations, new Comparator<Conversation>() { @Override public int compare(Conversation lhs, Conversation rhs) { - return (int) (rhs.getLatestMessage().getTimeSent() - lhs - .getLatestMessage().getTimeSent()); + Message left = lhs.getLatestMessage(); + Message right = rhs.getLatestMessage(); + if (left.getTimeSent() > right.getTimeSent()) { + return -1; + } else if (left.getTimeSent() < right.getTimeSent()) { + return 1; + } else { + return 0; + } } }); return this.conversations; } + public List<Message> getMoreMessages(Conversation conversation, + long timestamp) { + List<Message> messages = databaseBackend.getMessages(conversation, 50, + timestamp); + for (Message message : messages) { + message.setConversation(conversation); + } + return messages; + } + public List<Account> getAccounts() { return this.accounts; } - public Contact findContact(Account account, String jid) { - Contact contact = databaseBackend.findContact(account, jid); - if (contact != null) { - contact.setAccount(account); + public Conversation findActiveConversation(Contact contact) { + for (Conversation conversation : this.getConversations()) { + if (conversation.getContact() == contact) { + return conversation; + } } - return contact; + return null; } public Conversation findOrCreateConversation(Account account, String jid, @@ -1045,11 +966,9 @@ public class XmppConnectionService extends Service { conversation.setMessages(databaseBackend.getMessages(conversation, 50)); this.databaseBackend.updateConversation(conversation); - conversation.setContact(findContact(account, - conversation.getContactJid())); } else { String conversationName; - Contact contact = findContact(account, jid); + Contact contact = account.getRoster().getContact(jid); if (contact != null) { conversationName = contact.getDisplayName(); } else { @@ -1062,7 +981,6 @@ public class XmppConnectionService extends Service { conversation = new Conversation(conversationName, account, jid, Conversation.MODE_SINGLE); } - conversation.setContact(contact); this.databaseBackend.createConversation(conversation); } this.conversations.add(conversation); @@ -1110,23 +1028,14 @@ public class XmppConnectionService extends Service { accountChangedListener.onAccountListChangedListener(); } - public void deleteContact(Contact contact) { - IqPacket iq = new IqPacket(IqPacket.TYPE_SET); - Element query = iq.query("jabber:iq:roster"); - query.addChild("item").setAttribute("jid", contact.getJid()) - .setAttribute("subscription", "remove"); - contact.getAccount().getXmppConnection().sendIqPacket(iq, null); - replaceContactInConversation(contact.getAccount(), contact.getJid(), - null); - databaseBackend.deleteContact(contact); - } - public void updateAccount(Account account) { this.statusListener.onStatusChanged(account); databaseBackend.updateAccount(account); reconnectAccount(account, false); - if (accountChangedListener != null) + if (accountChangedListener != null) { accountChangedListener.onAccountListChangedListener(); + } + UIHelper.showErrorNotification(getApplicationContext(), getAccounts()); } public void deleteAccount(Account account) { @@ -1135,8 +1044,10 @@ public class XmppConnectionService extends Service { } databaseBackend.deleteAccount(account); this.accounts.remove(account); - if (accountChangedListener != null) + if (accountChangedListener != null) { accountChangedListener.onAccountListChangedListener(); + } + UIHelper.showErrorNotification(getApplicationContext(), getAccounts()); } public void setOnConversationListChangedListener( @@ -1173,6 +1084,7 @@ public class XmppConnectionService extends Service { } public void joinMuc(Conversation conversation) { + Account account = conversation.getAccount(); String[] mucParts = conversation.getContactJid().split("/"); String muc; String nick; @@ -1181,20 +1093,24 @@ public class XmppConnectionService extends Service { nick = mucParts[1]; } else { muc = mucParts[0]; - nick = conversation.getAccount().getUsername(); + nick = account.getUsername(); } PresencePacket packet = new PresencePacket(); packet.setAttribute("to", muc + "/" + nick); Element x = new Element("x"); x.setAttribute("xmlns", "http://jabber.org/protocol/muc"); + String sig = account.getPgpSignature(); + if (sig != null) { + packet.addChild("status").setContent("online"); + packet.addChild("x", "jabber:x:signed").setContent(sig); + } if (conversation.getMessages().size() != 0) { long lastMsgTime = conversation.getLatestMessage().getTimeSent(); long diff = (System.currentTimeMillis() - lastMsgTime) / 1000 - 1; x.addChild("history").setAttribute("seconds", diff + ""); } packet.addChild(x); - conversation.getAccount().getXmppConnection() - .sendPresencePacket(packet); + account.getXmppConnection().sendPresencePacket(packet); } private OnRenameListener renameListener = null; @@ -1206,6 +1122,7 @@ public class XmppConnectionService extends Service { public void renameInMuc(final Conversation conversation, final String nick) { final MucOptions options = conversation.getMucOptions(); if (options.online()) { + Account account = conversation.getAccount(); options.setOnRenameListener(new OnRenameListener() { @Override @@ -1214,8 +1131,8 @@ public class XmppConnectionService extends Service { renameListener.onRename(success); } if (success) { - String jid = conversation.getContactJid().split("/")[0] + "/" - + nick; + String jid = conversation.getContactJid().split("/")[0] + + "/" + nick; conversation.setContactJid(jid); databaseBackend.updateConversation(conversation); } @@ -1227,8 +1144,13 @@ public class XmppConnectionService extends Service { conversation.getContactJid().split("/")[0] + "/" + nick); packet.setAttribute("from", conversation.getAccount().getFullJid()); - conversation.getAccount().getXmppConnection() - .sendPresencePacket(packet, null); + String sig = account.getPgpSignature(); + if (sig != null) { + packet.addChild("status").setContent("online"); + packet.addChild("x", "jabber:x:signed").setContent(sig); + } + + account.getXmppConnection().sendPresencePacket(packet, null); } else { String jid = conversation.getContactJid().split("/")[0] + "/" + nick; @@ -1277,96 +1199,153 @@ public class XmppConnectionService extends Service { return mBinder; } - public void updateContact(Contact contact) { - databaseBackend.updateContact(contact, false); - replaceContactInConversation(contact.getAccount(), contact.getJid(), - contact); - } - public void updateMessage(Message message) { databaseBackend.updateMessage(message); } + protected void syncDirtyContacts(Account account) { + for (Contact contact : account.getRoster().getContacts()) { + if (contact.getOption(Contact.Options.DIRTY_PUSH)) { + pushContactToServer(contact); + } + if (contact.getOption(Contact.Options.DIRTY_DELETE)) { + Log.d(LOGTAG, "dirty delete"); + deleteContactOnServer(contact); + } + } + } + public void createContact(Contact contact) { SharedPreferences sharedPref = getPreferences(); boolean autoGrant = sharedPref.getBoolean("grant_new_contacts", true); if (autoGrant) { - contact.setSubscriptionOption(Contact.Subscription.PREEMPTIVE_GRANT); - contact.setSubscriptionOption(Contact.Subscription.ASKING); + contact.setOption(Contact.Options.PREEMPTIVE_GRANT); + contact.setOption(Contact.Options.ASKING); + } + pushContactToServer(contact); + } + + public void onOtrSessionEstablished(Conversation conversation) { + Account account = conversation.getAccount(); + List<Message> messages = conversation.getMessages(); + Session otrSession = conversation.getOtrSession(); + Log.d(LOGTAG, account.getJid() + " otr session established with " + + conversation.getContactJid() + "/" + + otrSession.getSessionID().getUserID()); + for (int i = 0; i < messages.size(); ++i) { + Message msg = messages.get(i); + if ((msg.getStatus() == Message.STATUS_UNSEND || msg.getStatus() == Message.STATUS_WAITING) + && (msg.getEncryption() == Message.ENCRYPTION_OTR)) { + MessagePacket outPacket = prepareMessagePacket(account, msg, + otrSession); + msg.setStatus(Message.STATUS_SEND); + databaseBackend.updateMessage(msg); + account.getXmppConnection().sendMessagePacket(outPacket); + } } - databaseBackend.createContact(contact); - IqPacket iq = new IqPacket(IqPacket.TYPE_SET); - Element query = new Element("query"); - query.setAttribute("xmlns", "jabber:iq:roster"); - Element item = new Element("item"); - item.setAttribute("jid", contact.getJid()); - item.setAttribute("name", contact.getJid()); - query.addChild(item); - iq.addChild(query); + updateUi(conversation, false); + } + + public boolean renewSymmetricKey(Conversation conversation) { + Account account = conversation.getAccount(); + byte[] symmetricKey = new byte[32]; + this.mRandom.nextBytes(symmetricKey); + Session otrSession = conversation.getOtrSession(); + if (otrSession!=null) { + MessagePacket packet = new MessagePacket(); + packet.setType(MessagePacket.TYPE_CHAT); + packet.setFrom(account.getFullJid()); + packet.addChild("private", "urn:xmpp:carbons:2"); + packet.addChild("no-copy", "urn:xmpp:hints"); + packet.setTo(otrSession.getSessionID().getAccountID() + "/" + + otrSession.getSessionID().getUserID()); + try { + packet.setBody(otrSession.transformSending(CryptoHelper.FILETRANSFER+CryptoHelper.bytesToHex(symmetricKey))); + account.getXmppConnection().sendMessagePacket(packet); + conversation.setSymmetricKey(symmetricKey); + return true; + } catch (OtrException e) { + return false; + } + } + return false; + } + + public void pushContactToServer(Contact contact) { + contact.resetOption(Contact.Options.DIRTY_DELETE); Account account = contact.getAccount(); - account.getXmppConnection().sendIqPacket(iq, null); - if (autoGrant) { - requestPresenceUpdatesFrom(contact); - if (account.getXmppConnection().hasPendingSubscription( - contact.getJid())) { + if (account.getStatus() == Account.STATUS_ONLINE) { + IqPacket iq = new IqPacket(IqPacket.TYPE_SET); + iq.query("jabber:iq:roster").addChild(contact.asElement()); + account.getXmppConnection().sendIqPacket(iq, null); + if (contact.getOption(Contact.Options.ASKING)) { + requestPresenceUpdatesFrom(contact); + } + if (contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { Log.d("xmppService", "contact had pending subscription"); sendPresenceUpdatesTo(contact); } + contact.resetOption(Contact.Options.DIRTY_PUSH); + } else { + contact.setOption(Contact.Options.DIRTY_PUSH); + } + } + + public void deleteContactOnServer(Contact contact) { + contact.resetOption(Contact.Options.DIRTY_PUSH); + Account account = contact.getAccount(); + if (account.getStatus() == Account.STATUS_ONLINE) { + IqPacket iq = new IqPacket(IqPacket.TYPE_SET); + Element item = iq.query("jabber:iq:roster").addChild("item"); + item.setAttribute("jid", contact.getJid()); + item.setAttribute("subscription", "remove"); + account.getXmppConnection().sendIqPacket(iq, null); + contact.resetOption(Contact.Options.DIRTY_DELETE); + } else { + contact.setOption(Contact.Options.DIRTY_DELETE); } - replaceContactInConversation(contact.getAccount(), contact.getJid(), - contact); } public void requestPresenceUpdatesFrom(Contact contact) { - // Requesting a Subscription type=subscribe PresencePacket packet = new PresencePacket(); packet.setAttribute("type", "subscribe"); packet.setAttribute("to", contact.getJid()); packet.setAttribute("from", contact.getAccount().getJid()); - Log.d(LOGTAG, packet.toString()); contact.getAccount().getXmppConnection().sendPresencePacket(packet); } public void stopPresenceUpdatesFrom(Contact contact) { - // Unsubscribing type='unsubscribe' PresencePacket packet = new PresencePacket(); packet.setAttribute("type", "unsubscribe"); packet.setAttribute("to", contact.getJid()); packet.setAttribute("from", contact.getAccount().getJid()); - Log.d(LOGTAG, packet.toString()); contact.getAccount().getXmppConnection().sendPresencePacket(packet); } public void stopPresenceUpdatesTo(Contact contact) { - // Canceling a Subscription type=unsubscribed PresencePacket packet = new PresencePacket(); packet.setAttribute("type", "unsubscribed"); packet.setAttribute("to", contact.getJid()); packet.setAttribute("from", contact.getAccount().getJid()); - Log.d(LOGTAG, packet.toString()); contact.getAccount().getXmppConnection().sendPresencePacket(packet); } public void sendPresenceUpdatesTo(Contact contact) { - // type='subscribed' PresencePacket packet = new PresencePacket(); packet.setAttribute("type", "subscribed"); packet.setAttribute("to", contact.getJid()); packet.setAttribute("from", contact.getAccount().getJid()); - Log.d(LOGTAG, packet.toString()); contact.getAccount().getXmppConnection().sendPresencePacket(packet); } - public void sendPgpPresence(Account account, String signature) { + public void sendPresence(Account account) { PresencePacket packet = new PresencePacket(); packet.setAttribute("from", account.getFullJid()); - Element status = new Element("status"); - status.setContent("online"); - packet.addChild(status); - Element x = new Element("x"); - x.setAttribute("xmlns", "jabber:x:signed"); - x.setContent(signature); - packet.addChild(x); + String sig = account.getPgpSignature(); + if (sig != null) { + packet.addChild("status").setContent("online"); + packet.addChild("x", "jabber:x:signed").setContent(sig); + } account.getXmppConnection().sendPresencePacket(packet); } @@ -1374,23 +1353,10 @@ public class XmppConnectionService extends Service { this.databaseBackend.updateConversation(conversation); } - public Contact findContact(String uuid) { - Contact contact = this.databaseBackend.getContact(uuid); - if (contact != null) { - for (Account account : getAccounts()) { - if (contact.getAccountUuid().equals(account.getUuid())) { - contact.setAccount(account); - } - } - } - return contact; - } - public void removeOnTLSExceptionReceivedListener() { this.tlsException = null; } - // TODO dont let thread sleep but schedule wake up public void reconnectAccount(final Account account, final boolean force) { new Thread(new Runnable() { @@ -1447,21 +1413,24 @@ public class XmppConnectionService extends Service { public boolean markMessage(Account account, String recipient, String uuid, int status) { - boolean marked = false; for (Conversation conversation : getConversations()) { if (conversation.getContactJid().equals(recipient) && conversation.getAccount().equals(account)) { - for (Message message : conversation.getMessages()) { - if (message.getUuid().equals(uuid)) { - markMessage(message, status); - marked = true; - break; - } - } - break; + return markMessage(conversation, uuid, status); + } + } + return false; + } + + public boolean markMessage(Conversation conversation, String uuid, + int status) { + for (Message message : conversation.getMessages()) { + if (message.getUuid().equals(uuid)) { + markMessage(message, status); + return true; } } - return marked; + return false; } public void markMessage(Message message, int status) { @@ -1476,12 +1445,49 @@ public class XmppConnectionService extends Service { return PreferenceManager .getDefaultSharedPreferences(getApplicationContext()); } - + + public boolean confirmMessages() { + return getPreferences().getBoolean("confirm_messages", true); + } + public void updateUi(Conversation conversation, boolean notify) { if (convChangedListener != null) { convChangedListener.onConversationListChanged(); } else { - UIHelper.updateNotification(getApplicationContext(), getConversations(), conversation, notify); + UIHelper.updateNotification(getApplicationContext(), + getConversations(), conversation, notify); + } + } + + public Account findAccountByJid(String accountJid) { + for (Account account : this.accounts) { + if (account.getJid().equals(accountJid)) { + return account; + } } + return null; + } + + public void markRead(Conversation conversation) { + conversation.markRead(this); + } + + public void sendConfirmMessage(Account account, String to, String id) { + MessagePacket receivedPacket = new MessagePacket(); + receivedPacket.setType(MessagePacket.TYPE_NORMAL); + receivedPacket.setTo(to); + receivedPacket.setFrom(account.getFullJid()); + Element received = receivedPacket.addChild("displayed", + "urn:xmpp:chat-markers:0"); + received.setAttribute("id", id); + account.getXmppConnection().sendMessagePacket(receivedPacket); + } + + public SecureRandom getRNG() { + return this.mRandom; + } + + public PowerManager getPowerManager() { + return this.pm; } } diff --git a/src/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/eu/siacs/conversations/ui/ContactDetailsActivity.java index 06179bc6..d89c35f1 100644 --- a/src/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -1,8 +1,6 @@ package eu.siacs.conversations.ui; -import java.math.BigInteger; import java.util.Iterator; -import java.util.Locale; import org.openintents.openpgp.util.OpenPgpUtils; @@ -17,7 +15,6 @@ import android.os.Bundle; import android.provider.ContactsContract.CommonDataKinds; import android.provider.ContactsContract.Contacts; import android.provider.ContactsContract.Intents; -import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; @@ -31,6 +28,7 @@ import android.widget.TextView; import android.widget.Toast; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.PgpEngine; +import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Presences; import eu.siacs.conversations.utils.UIHelper; @@ -40,14 +38,17 @@ public class ContactDetailsActivity extends XmppActivity { protected ContactDetailsActivity activity = this; - private String uuid; private Contact contact; - + + private String accountJid; + private String contactJid; + private EditText name; - private TextView contactJid; - private TextView accountJid; + private TextView contactJidTv; + private TextView accountJidTv; private TextView status; private TextView askAgain; + private TextView lastseen; private CheckBox send; private CheckBox receive; private QuickContactBadge badge; @@ -56,7 +57,7 @@ public class ContactDetailsActivity extends XmppActivity { @Override public void onClick(DialogInterface dialog, int which) { - activity.xmppConnectionService.deleteContact(contact); + activity.xmppConnectionService.deleteContactOnServer(contact); activity.finish(); } }; @@ -65,8 +66,8 @@ public class ContactDetailsActivity extends XmppActivity { @Override public void onClick(DialogInterface dialog, int which) { - contact.setDisplayName(name.getText().toString()); - activity.xmppConnectionService.updateContact(contact); + contact.setServerName(name.getText().toString()); + activity.xmppConnectionService.pushContactToServer(contact); populateView(); } }; @@ -89,11 +90,10 @@ public class ContactDetailsActivity extends XmppActivity { @Override public void onClick(View v) { AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setTitle("Add to phone book"); - builder.setMessage("Do you want to add " + contact.getJid() - + " to your phones contact list?"); - builder.setNegativeButton("Cancel", null); - builder.setPositiveButton("Add", addToPhonebook); + 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(); } }; @@ -104,13 +104,15 @@ public class ContactDetailsActivity extends XmppActivity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getIntent().getAction().equals(ACTION_VIEW_CONTACT)) { - this.uuid = getIntent().getExtras().getString("uuid"); + this.accountJid = getIntent().getExtras().getString("account"); + this.contactJid = getIntent().getExtras().getString("contact"); } setContentView(R.layout.activity_contact_details); - contactJid = (TextView) findViewById(R.id.details_contactjid); - accountJid = (TextView) findViewById(R.id.details_account); + contactJidTv = (TextView) findViewById(R.id.details_contactjid); + accountJidTv = (TextView) findViewById(R.id.details_account); status = (TextView) findViewById(R.id.details_contactstatus); + lastseen = (TextView) findViewById(R.id.details_lastseen); send = (CheckBox) findViewById(R.id.details_send_presence); receive = (CheckBox) findViewById(R.id.details_receive_presence); askAgain = (TextView) findViewById(R.id.ask_again); @@ -124,17 +126,17 @@ public class ContactDetailsActivity extends XmppActivity { @Override public boolean onOptionsItemSelected(MenuItem menuItem) { AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setNegativeButton("Cancel", null); + 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("Delete from roster") + builder.setTitle(getString(R.string.action_delete_contact)) .setMessage( getString(R.string.remove_contact_text, contact.getJid())) - .setPositiveButton("Delete", removeFromRoster).create() + .setPositiveButton(getString(R.string.delete), removeFromRoster).create() .show(); break; case R.id.action_edit_contact: @@ -145,7 +147,7 @@ public class ContactDetailsActivity extends XmppActivity { name = (EditText) view.findViewById(R.id.editText1); name.setText(contact.getDisplayName()); builder.setView(view).setTitle(contact.getJid()) - .setPositiveButton("Edit", editContactNameListener) + .setPositiveButton(getString(R.string.edit), editContactNameListener) .create().show(); } else { @@ -170,18 +172,18 @@ public class ContactDetailsActivity extends XmppActivity { private void populateView() { setTitle(contact.getDisplayName()); - if (contact.getSubscriptionOption(Contact.Subscription.FROM)) { + if (contact.getOption(Contact.Options.FROM)) { send.setChecked(true); } else { send.setText(R.string.preemptively_grant); if (contact - .getSubscriptionOption(Contact.Subscription.PREEMPTIVE_GRANT)) { + .getOption(Contact.Options.PREEMPTIVE_GRANT)) { send.setChecked(true); } else { send.setChecked(false); } } - if (contact.getSubscriptionOption(Contact.Subscription.TO)) { + if (contact.getOption(Contact.Options.TO)) { receive.setChecked(true); } else { receive.setText(R.string.ask_for_presence_updates); @@ -190,54 +192,57 @@ public class ContactDetailsActivity extends XmppActivity { @Override public void onClick(View v) { - Toast.makeText(getApplicationContext(), "Asked for presence updates",Toast.LENGTH_SHORT).show(); + Toast.makeText(getApplicationContext(), getString(R.string.asked_for_presence_updates), + Toast.LENGTH_SHORT).show(); xmppConnectionService.requestPresenceUpdatesFrom(contact); } }); - if (contact.getSubscriptionOption(Contact.Subscription.ASKING)) { + if (contact.getOption(Contact.Options.ASKING)) { receive.setChecked(true); } else { receive.setChecked(false); } } + + lastseen.setText(UIHelper.lastseen(getApplicationContext(),contact.lastseen.time)); switch (contact.getMostAvailableStatus()) { case Presences.CHAT: - status.setText("free to chat"); + status.setText(R.string.contact_status_free_to_chat); status.setTextColor(0xFF83b600); break; case Presences.ONLINE: - status.setText("online"); + status.setText(R.string.contact_status_online); status.setTextColor(0xFF83b600); break; case Presences.AWAY: - status.setText("away"); + status.setText(R.string.contact_status_away); status.setTextColor(0xFFffa713); break; case Presences.XA: - status.setText("extended away"); + status.setText(R.string.contact_status_extended_away); status.setTextColor(0xFFffa713); break; case Presences.DND: - status.setText("do not disturb"); + status.setText(R.string.contact_status_do_not_disturb); status.setTextColor(0xFFe92727); break; case Presences.OFFLINE: - status.setText("offline"); + status.setText(R.string.contact_status_offline); status.setTextColor(0xFFe92727); break; default: - status.setText("offline"); + status.setText(R.string.contact_status_offline); status.setTextColor(0xFFe92727); break; } if (contact.getPresences().size() > 1) { - contactJid.setText(contact.getJid()+" ("+contact.getPresences().size()+")"); + contactJidTv.setText(contact.getJid()+" ("+contact.getPresences().size()+")"); } else { - contactJid.setText(contact.getJid()); + contactJidTv.setText(contact.getJid()); } - accountJid.setText(contact.getAccount().getJid()); + accountJidTv.setText(contact.getAccount().getJid()); UIHelper.prepareContactBadge(this, badge, contact, getApplicationContext()); @@ -286,65 +291,83 @@ public class ContactDetailsActivity extends XmppActivity { @Override public void onBackendConnected() { - if (uuid != null) { - this.contact = xmppConnectionService.findContact(uuid); - if (this.contact != null) { - populateView(); + if ((accountJid != null)&&(contactJid != null)) { + Account account = xmppConnectionService.findAccountByJid(accountJid); + if (account==null) { + return; } + this.contact = account.getRoster().getContact(contactJid); + populateView(); } } @Override protected void onStop() { super.onStop(); - boolean needsUpdating = false; - if (contact.getSubscriptionOption(Contact.Subscription.FROM)) { + boolean updated = false; + boolean online = contact.getAccount().getStatus() == Account.STATUS_ONLINE; + if (contact.getOption(Contact.Options.FROM)) { if (!send.isChecked()) { - contact.resetSubscriptionOption(Contact.Subscription.FROM); - contact.resetSubscriptionOption(Contact.Subscription.PREEMPTIVE_GRANT); - activity.xmppConnectionService.stopPresenceUpdatesTo(contact); - needsUpdating = true; + if (online) { + contact.resetOption(Contact.Options.FROM); + contact.resetOption(Contact.Options.PREEMPTIVE_GRANT); + activity.xmppConnectionService.stopPresenceUpdatesTo(contact); + } + updated = true; } } else { if (contact - .getSubscriptionOption(Contact.Subscription.PREEMPTIVE_GRANT)) { + .getOption(Contact.Options.PREEMPTIVE_GRANT)) { if (!send.isChecked()) { - contact.resetSubscriptionOption(Contact.Subscription.PREEMPTIVE_GRANT); - needsUpdating = true; + if (online) { + contact.resetOption(Contact.Options.PREEMPTIVE_GRANT); + } + updated = true; } } else { if (send.isChecked()) { - contact.setSubscriptionOption(Contact.Subscription.PREEMPTIVE_GRANT); - needsUpdating = true; + if (online) { + contact.setOption(Contact.Options.PREEMPTIVE_GRANT); + } + updated = true; } } } - if (contact.getSubscriptionOption(Contact.Subscription.TO)) { + if (contact.getOption(Contact.Options.TO)) { if (!receive.isChecked()) { - contact.resetSubscriptionOption(Contact.Subscription.TO); - activity.xmppConnectionService.stopPresenceUpdatesFrom(contact); - needsUpdating = true; + if (online) { + contact.resetOption(Contact.Options.TO); + activity.xmppConnectionService.stopPresenceUpdatesFrom(contact); + } + updated = true; } } else { - if (contact.getSubscriptionOption(Contact.Subscription.ASKING)) { + if (contact.getOption(Contact.Options.ASKING)) { if (!receive.isChecked()) { - contact.resetSubscriptionOption(Contact.Subscription.ASKING); - activity.xmppConnectionService + if (online) { + contact.resetOption(Contact.Options.ASKING); + activity.xmppConnectionService .stopPresenceUpdatesFrom(contact); - needsUpdating = true; + } + updated = true; } } else { if (receive.isChecked()) { - contact.setSubscriptionOption(Contact.Subscription.ASKING); - activity.xmppConnectionService + if (online) { + contact.setOption(Contact.Options.ASKING); + activity.xmppConnectionService .requestPresenceUpdatesFrom(contact); - needsUpdating = true; + } + updated = true; } } } - if (needsUpdating) { - Toast.makeText(getApplicationContext(), "Subscription updated", Toast.LENGTH_SHORT).show(); - activity.xmppConnectionService.updateContact(contact); + if (updated) { + if (online) { + Toast.makeText(getApplicationContext(), getString(R.string.subscription_updated), Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(getApplicationContext(), getString(R.string.subscription_not_updated_offline), Toast.LENGTH_SHORT).show(); + } } } diff --git a/src/eu/siacs/conversations/ui/ContactsActivity.java b/src/eu/siacs/conversations/ui/ContactsActivity.java index e403450a..4e9c8af6 100644 --- a/src/eu/siacs/conversations/ui/ContactsActivity.java +++ b/src/eu/siacs/conversations/ui/ContactsActivity.java @@ -18,7 +18,6 @@ import android.os.Bundle; import android.preference.PreferenceManager; import android.text.Editable; import android.text.TextWatcher; -import android.util.Log; import android.util.SparseBooleanArray; import android.view.ActionMode; import android.view.LayoutInflater; @@ -34,13 +33,11 @@ import android.widget.AdapterView.OnItemLongClickListener; import android.widget.ArrayAdapter; import android.widget.EditText; import android.widget.ListView; -import android.widget.ProgressBar; import android.widget.TextView; import android.widget.ImageView; import android.widget.Toast; import android.annotation.SuppressLint; import android.app.AlertDialog; -import android.app.AlertDialog.Builder; import android.content.Context; import android.content.DialogInterface; import android.content.SharedPreferences; @@ -130,8 +127,10 @@ public class ContactsActivity extends XmppActivity { Intent intent = new Intent(getApplicationContext(), ContactDetailsActivity.class); intent.setAction(ContactDetailsActivity.ACTION_VIEW_CONTACT); - intent.putExtra("uuid", selectedContacts.get(0).getUuid()); + intent.putExtra("account", selectedContacts.get(0).getAccount().getJid()); + intent.putExtra("contact",selectedContacts.get(0).getJid()); startActivity(intent); + finish(); break; case R.id.action_invite: invite(); @@ -187,7 +186,7 @@ public class ContactsActivity extends XmppActivity { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(getString(R.string.account_offline)); builder.setMessage(getString(R.string.cant_invite_while_offline)); - builder.setNegativeButton("OK", null); + builder.setNegativeButton(getString(R.string.ok), null); builder.setIconAttribute(android.R.attr.alertDialogIcon); builder.create().show(); return false; @@ -238,7 +237,7 @@ public class ContactsActivity extends XmppActivity { @Override public void onClick(DialogInterface dialog, int which) { - String mucName = CryptoHelper.randomMucName(); + String mucName = CryptoHelper.randomMucName(xmppConnectionService.getRNG()); String serverName = account.getXmppConnection() .getMucServer(); String jid = mucName + "@" + serverName; @@ -259,7 +258,7 @@ public class ContactsActivity extends XmppActivity { conversation, subject.toString()); xmppConnectionService.inviteToConference(conversation, selectedContacts); - switchToConversation(conversation, null); + switchToConversation(conversation, null,false); } }); builder.create().show(); @@ -270,7 +269,7 @@ public class ContactsActivity extends XmppActivity { aggregatedContacts.clear(); for (Contact contact : rosterContacts) { - if (contact.match(searchString)) + if (contact.match(searchString)&&(contact.showInRoster())) aggregatedContacts.add(contact); } @@ -287,16 +286,15 @@ public class ContactsActivity extends XmppActivity { if (aggregatedContacts.size() == 0) { if (Validator.isValidJid(searchString)) { - String name = searchString.split("@")[0]; - Contact newContact = new Contact(null, name, searchString, null); - newContact.flagAsNotInRoster(); + Contact newContact = new Contact(searchString); + newContact.resetOption(Contact.Options.IN_ROSTER); aggregatedContacts.add(newContact); - contactsHeader.setText("Create new contact"); + contactsHeader.setText(getString(R.string.new_contact)); } else { - contactsHeader.setText("Contacts"); + contactsHeader.setText(getString(R.string.contacts)); } } else { - contactsHeader.setText("Contacts"); + contactsHeader.setText(getString(R.string.contacts)); } contactsAdapter.notifyDataSetChanged(); @@ -429,17 +427,19 @@ public class ContactsActivity extends XmppActivity { } AlertDialog.Builder accountChooser = new AlertDialog.Builder(this); - accountChooser.setTitle("Choose account"); + accountChooser.setTitle(getString(R.string.choose_account)); accountChooser.setItems(accountList, listener); return accountChooser.create(); } public void showIsMucDialogIfNeeded(final Contact clickedContact) { - if (clickedContact.couldBeMuc()) { + if (isMuc(clickedContact)) { + startConversation(clickedContact,clickedContact.getAccount(), true); + } else if (clickedContact.couldBeMuc()) { AlertDialog.Builder dialog = new AlertDialog.Builder(this); - dialog.setTitle("Multi User Conference"); - dialog.setMessage("Are you trying to join a conference?"); - dialog.setPositiveButton("Yes", new OnClickListener() { + dialog.setTitle(getString(R.string.multi_user_conference)); + dialog.setMessage(getString(R.string.trying_join_conference)); + dialog.setPositiveButton(getString(R.string.yes), new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { @@ -447,7 +447,7 @@ public class ContactsActivity extends XmppActivity { clickedContact.getAccount(), true); } }); - dialog.setNegativeButton("No", new OnClickListener() { + dialog.setNegativeButton(getString(R.string.no), new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { @@ -461,15 +461,26 @@ public class ContactsActivity extends XmppActivity { false); } } + + private boolean isMuc(Contact contact) { + ArrayList<String> mucServers = new ArrayList<String>(); + for(Account account : accounts) { + if (account.getXmppConnection()!=null) { + mucServers.add(account.getXmppConnection().getMucServer()); + } + } + String server = contact.getJid().split("@")[1]; + return mucServers.contains(server); + } public void startConversation(Contact contact, Account account, boolean muc) { - if (!contact.isInRoster()&&(!muc)) { + if (!contact.getOption(Contact.Options.IN_ROSTER)&&(!muc)) { xmppConnectionService.createContact(contact); } Conversation conversation = xmppConnectionService .findOrCreateConversation(account, contact.getJid(), muc); - switchToConversation(conversation, null); + switchToConversation(conversation, null,false); } @Override @@ -496,7 +507,7 @@ public class ContactsActivity extends XmppActivity { .findOrCreateConversation( accounts.get(which), finalJid, false); - switchToConversation(conversation, null); + switchToConversation(conversation, null,false); finish(); } }).show(); @@ -504,7 +515,7 @@ public class ContactsActivity extends XmppActivity { Conversation conversation = xmppConnectionService .findOrCreateConversation(this.accounts.get(0), jid, false); - switchToConversation(conversation, null); + switchToConversation(conversation, null,false); finish(); } } @@ -515,9 +526,10 @@ public class ContactsActivity extends XmppActivity { getActionBar().setHomeButtonEnabled(false); } this.rosterContacts.clear(); - for (int i = 0; i < accounts.size(); ++i) { - rosterContacts.addAll(xmppConnectionService.getRoster(accounts - .get(i))); + for(Account account : accounts) { + if (account.getStatus() != Account.STATUS_DISABLED) { + rosterContacts.addAll(account.getRoster().getContacts()); + } } updateAggregatedContacts(); } @@ -532,52 +544,12 @@ public class ContactsActivity extends XmppActivity { @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { - case R.id.action_refresh_contacts: - refreshContacts(); - break; default: break; } return super.onOptionsItemSelected(item); } - private void refreshContacts() { - final ProgressBar progress = (ProgressBar) findViewById(R.id.progressBar1); - final EditText searchBar = (EditText) findViewById(R.id.new_conversation_search); - final TextView contactsHeader = (TextView) findViewById(R.id.contacts_header); - final ListView contactList = (ListView) findViewById(R.id.contactList); - searchBar.setVisibility(View.GONE); - contactsHeader.setVisibility(View.GONE); - contactList.setVisibility(View.GONE); - progress.setVisibility(View.VISIBLE); - this.accounts = xmppConnectionService.getAccounts(); - this.rosterContacts.clear(); - for (int i = 0; i < accounts.size(); ++i) { - if (accounts.get(i).getStatus() == Account.STATUS_ONLINE) { - xmppConnectionService.updateRoster(accounts.get(i), - new OnRosterFetchedListener() { - - @Override - public void onRosterFetched( - final List<Contact> roster) { - runOnUiThread(new Runnable() { - - @Override - public void run() { - rosterContacts.addAll(roster); - progress.setVisibility(View.GONE); - searchBar.setVisibility(View.VISIBLE); - contactList.setVisibility(View.VISIBLE); - contactList.setVisibility(View.VISIBLE); - updateAggregatedContacts(); - } - }); - } - }); - } - } - } - @Override public void onActionModeStarted(ActionMode mode) { super.onActionModeStarted(mode); diff --git a/src/eu/siacs/conversations/ui/ConversationActivity.java b/src/eu/siacs/conversations/ui/ConversationActivity.java index 88728245..4e264df7 100644 --- a/src/eu/siacs/conversations/ui/ConversationActivity.java +++ b/src/eu/siacs/conversations/ui/ConversationActivity.java @@ -3,22 +3,20 @@ package eu.siacs.conversations.ui; import java.io.FileNotFoundException; import java.lang.ref.WeakReference; import java.util.ArrayList; -import java.util.Hashtable; import java.util.List; -import org.openintents.openpgp.OpenPgpError; - import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.ImageProvider; import eu.siacs.conversations.utils.ExceptionHelper; import eu.siacs.conversations.utils.UIHelper; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.preference.PreferenceManager; +import android.provider.MediaStore; import android.app.AlertDialog; import android.app.FragmentTransaction; import android.app.PendingIntent; @@ -65,21 +63,25 @@ public class ConversationActivity extends XmppActivity { public static final int REQUEST_SEND_MESSAGE = 0x75441; public static final int REQUEST_DECRYPT_PGP = 0x76783; private static final int REQUEST_ATTACH_FILE_DIALOG = 0x48502; + private static final int REQUEST_IMAGE_CAPTURE = 0x33788; + private static final int REQUEST_RECORD_AUDIO = 0x46189; private static final int REQUEST_SEND_PGP_IMAGE = 0x53883; - private static final int REQUEST_ATTACH_FILE = 0x73824; public static final int REQUEST_ENCRYPT_MESSAGE = 0x378018; + private static final int ATTACHMENT_CHOICE_CHOOSE_IMAGE = 0x92734; + private static final int ATTACHMENT_CHOICE_TAKE_PHOTO = 0x84123; + private static final int ATTACHMENT_CHOICE_RECORD_VOICE = 0x75291; + protected SlidingPaneLayout spl; private List<Conversation> conversationList = new ArrayList<Conversation>(); private Conversation selectedConversation = null; private ListView listView; - + private boolean paneShouldBeOpen = true; private boolean useSubject = true; + private boolean showLastseen = false; private ArrayAdapter<Conversation> listAdapter; - - public Message pendingMessage = null; private OnConversationListChangedListener onConvChanged = new OnConversationListChangedListener() { @@ -108,7 +110,7 @@ public class ConversationActivity extends XmppActivity { }); } }; - + protected ConversationActivity activity = this; private DisplayMetrics metrics; private Toast prepareImageToast; @@ -121,6 +123,10 @@ public class ConversationActivity extends XmppActivity { return this.selectedConversation; } + public void setSelectedConversation(Conversation conversation) { + this.selectedConversation = conversation; + } + public ListView getConversationListView() { return this.listView; } @@ -137,7 +143,7 @@ public class ConversationActivity extends XmppActivity { protected void onCreate(Bundle savedInstanceState) { metrics = getResources().getDisplayMetrics(); - + super.onCreate(savedInstanceState); setContentView(R.layout.fragment_conversations_overview); @@ -173,15 +179,18 @@ public class ConversationActivity extends XmppActivity { convName.setText(conv.getName(useSubject)); TextView convLastMsg = (TextView) view .findViewById(R.id.conversation_lastmsg); - ImageView imagePreview = (ImageView) view.findViewById(R.id.conversation_lastimage); - + ImageView imagePreview = (ImageView) view + .findViewById(R.id.conversation_lastimage); + Message latestMessage = conv.getLatestMessage(); - + if (latestMessage.getType() == Message.TYPE_TEXT) { - if ((latestMessage.getEncryption() != Message.ENCRYPTION_PGP)&&(latestMessage.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED)) { + if ((latestMessage.getEncryption() != Message.ENCRYPTION_PGP) + && (latestMessage.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED)) { convLastMsg.setText(conv.getLatestMessage().getBody()); } else { - convLastMsg.setText(getText(R.string.encrypted_message_received)); + convLastMsg + .setText(getText(R.string.encrypted_message_received)); } convLastMsg.setVisibility(View.VISIBLE); imagePreview.setVisibility(View.GONE); @@ -194,16 +203,16 @@ public class ConversationActivity extends XmppActivity { convLastMsg.setVisibility(View.VISIBLE); imagePreview.setVisibility(View.GONE); if (latestMessage.getStatus() == Message.STATUS_RECEIVED_OFFER) { - convLastMsg.setText(getText(R.string.image_offered_for_download)); + convLastMsg + .setText(getText(R.string.image_offered_for_download)); } else if (latestMessage.getStatus() == Message.STATUS_RECIEVING) { - convLastMsg.setText(getText(R.string.receiving_image)); + convLastMsg + .setText(getText(R.string.receiving_image)); } else { convLastMsg.setText(""); } } } - - if (!conv.isRead()) { convName.setTypeface(null, Typeface.BOLD); @@ -214,14 +223,14 @@ public class ConversationActivity extends XmppActivity { } ((TextView) view.findViewById(R.id.conversation_lastupdate)) - .setText(UIHelper.readableTimeDifference(conv - .getLatestMessage().getTimeSent())); + .setText(UIHelper.readableTimeDifference(getContext(), + conv.getLatestMessage().getTimeSent())); ImageView profilePicture = (ImageView) view .findViewById(R.id.conversation_image); - profilePicture.setImageBitmap(UIHelper.getContactPicture( - conv, 56, activity.getApplicationContext(), false)); - + profilePicture.setImageBitmap(UIHelper.getContactPicture(conv, + 56, activity.getApplicationContext(), false)); + return view; } @@ -235,8 +244,8 @@ public class ConversationActivity extends XmppActivity { public void onItemClick(AdapterView<?> arg0, View clickedView, int position, long arg3) { paneShouldBeOpen = false; - if (selectedConversation != conversationList.get(position)) { - selectedConversation = conversationList.get(position); + if (getSelectedConversation() != conversationList.get(position)) { + setSelectedConversation(conversationList.get(position)); swapConversationFragment(); // .onBackendConnected(conversationList.get(position)); } else { spl.closePane(); @@ -253,6 +262,7 @@ public class ConversationActivity extends XmppActivity { public void onPanelOpened(View arg0) { paneShouldBeOpen = true; getActionBar().setDisplayHomeAsUpEnabled(false); + getActionBar().setHomeButtonEnabled(false); getActionBar().setTitle(R.string.app_name); invalidateOptionsMenu(); hideKeyboard(); @@ -264,11 +274,13 @@ public class ConversationActivity extends XmppActivity { if ((conversationList.size() > 0) && (getSelectedConversation() != null)) { getActionBar().setDisplayHomeAsUpEnabled(true); + getActionBar().setHomeButtonEnabled(true); getActionBar().setTitle( getSelectedConversation().getName(useSubject)); invalidateOptionsMenu(); if (!getSelectedConversation().isRead()) { - getSelectedConversation().markRead(); + xmppConnectionService + .markRead(getSelectedConversation()); UIHelper.updateNotification(getApplicationContext(), getConversationList(), null, false); listView.invalidateViews(); @@ -296,7 +308,8 @@ public class ConversationActivity extends XmppActivity { MenuItem menuInviteContacts = (MenuItem) menu .findItem(R.id.action_invite); MenuItem menuAttach = (MenuItem) menu.findItem(R.id.action_attach_file); - MenuItem menuClearHistory = (MenuItem) menu.findItem(R.id.action_clear_history); + MenuItem menuClearHistory = (MenuItem) menu + .findItem(R.id.action_clear_history); if ((spl.isOpen() && (spl.isSlideable()))) { menuArchive.setVisible(false); @@ -310,112 +323,136 @@ public class ConversationActivity extends XmppActivity { ((MenuItem) menu.findItem(R.id.action_add)).setVisible(!spl .isSlideable()); if (this.getSelectedConversation() != null) { + if (this.getSelectedConversation().getLatestMessage() + .getEncryption() != Message.ENCRYPTION_NONE) { + menuSecure.setIcon(R.drawable.ic_action_secure); + } if (this.getSelectedConversation().getMode() == Conversation.MODE_MULTI) { menuContactDetails.setVisible(false); - menuSecure.setVisible(false); menuAttach.setVisible(false); } else { menuMucDetails.setVisible(false); menuInviteContacts.setVisible(false); - if (this.getSelectedConversation().getLatestMessage() - .getEncryption() != Message.ENCRYPTION_NONE) { - menuSecure.setIcon(R.drawable.ic_action_secure); - } } } } return true; } - - private void attachFileDialog() { + + private void selectPresenceToAttachFile(final int attachmentChoice) { selectPresence(getSelectedConversation(), new OnPresenceSelected() { - + @Override - public void onPresenceSelected(boolean success, String presence) { - if (success) { + public void onPresenceSelected() { + if (attachmentChoice == ATTACHMENT_CHOICE_TAKE_PHOTO) { + Intent takePictureIntent = new Intent( + MediaStore.ACTION_IMAGE_CAPTURE); + takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, + ImageProvider.getIncomingContentUri()); + if (takePictureIntent.resolveActivity(getPackageManager()) != null) { + startActivityForResult(takePictureIntent, + REQUEST_IMAGE_CAPTURE); + } + } else if (attachmentChoice == ATTACHMENT_CHOICE_CHOOSE_IMAGE) { Intent attachFileIntent = new Intent(); attachFileIntent.setType("image/*"); attachFileIntent.setAction(Intent.ACTION_GET_CONTENT); - Intent chooser = Intent.createChooser(attachFileIntent, getString(R.string.attach_file)); - startActivityForResult(chooser, REQUEST_ATTACH_FILE_DIALOG); + 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); } } - - @Override - public void onSendPlainTextInstead() { - - } - },"file"); + }); } - private void attachFile() { + private void attachFile(final int attachmentChoice) { final Conversation conversation = getSelectedConversation(); if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) { if (hasPgp()) { - if (conversation.getContact().getPgpKeyId()!=0) { - xmppConnectionService.getPgpEngine().hasKey(conversation.getContact(), new UiCallback() { - - @Override - public void userInputRequried(PendingIntent pi) { - ConversationActivity.this.runIntent(pi, REQUEST_ATTACH_FILE); - } - - @Override - public void success() { - attachFileDialog(); - } - - @Override - public void error(int error) { - // TODO Auto-generated method stub - - } - }); + if (conversation.getContact().getPgpKeyId() != 0) { + xmppConnectionService.getPgpEngine().hasKey( + conversation.getContact(), + new UiCallback<Contact>() { + + @Override + public void userInputRequried(PendingIntent pi, + Contact contact) { + ConversationActivity.this.runIntent(pi, + attachmentChoice); + } + + @Override + public void success(Contact contact) { + selectPresenceToAttachFile(attachmentChoice); + } + + @Override + public void error(int error, Contact contact) { + displayErrorDialog(error); + } + }); } else { final ConversationFragment fragment = (ConversationFragment) getFragmentManager() .findFragmentByTag("conversation"); if (fragment != null) { - fragment.showNoPGPKeyDialog(new OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - conversation.setNextEncryption(Message.ENCRYPTION_NONE); - attachFileDialog(); - } - }); + fragment.showNoPGPKeyDialog(false, + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, + int which) { + conversation + .setNextEncryption(Message.ENCRYPTION_NONE); + selectPresenceToAttachFile(attachmentChoice); + } + }); } } + } else { + showInstallPgpDialog(); } } else if (getSelectedConversation().getNextEncryption() == Message.ENCRYPTION_NONE) { - attachFileDialog(); + selectPresenceToAttachFile(attachmentChoice); } else { - AlertDialog.Builder builder = new AlertDialog.Builder(this); + selectPresenceToAttachFile(attachmentChoice); + /*AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(getString(R.string.otr_file_transfer)); builder.setMessage(getString(R.string.otr_file_transfer_msg)); builder.setNegativeButton(getString(R.string.cancel), null); - if (conversation.getContact().getPgpKeyId()==0) { - builder.setPositiveButton(getString(R.string.send_unencrypted), new OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - conversation.setNextEncryption(Message.ENCRYPTION_NONE); - attachFile(); - } - }); + if (conversation.getContact().getPgpKeyId() == 0) { + builder.setPositiveButton(getString(R.string.send_unencrypted), + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, + int which) { + conversation + .setNextEncryption(Message.ENCRYPTION_NONE); + attachFile(attachmentChoice); + } + }); } else { - builder.setPositiveButton(getString(R.string.use_pgp_encryption), new OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - conversation.setNextEncryption(Message.ENCRYPTION_PGP); - attachFile(); - } - }); + builder.setPositiveButton( + getString(R.string.use_pgp_encryption), + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, + int which) { + conversation + .setNextEncryption(Message.ENCRYPTION_PGP); + attachFile(attachmentChoice); + } + }); } - builder.create().show(); + builder.create().show();*/ } } - + @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { @@ -423,7 +460,29 @@ public class ConversationActivity extends XmppActivity { spl.openPane(); break; case R.id.action_attach_file: - attachFile(); + View menuAttachFile = findViewById(R.id.action_attach_file); + PopupMenu attachFilePopup = new PopupMenu(this, menuAttachFile); + attachFilePopup.inflate(R.menu.attachment_choices); + attachFilePopup + .setOnMenuItemClickListener(new OnMenuItemClickListener() { + + @Override + public boolean onMenuItemClick(MenuItem item) { + switch (item.getItemId()) { + case R.id.attach_choose_picture: + attachFile(ATTACHMENT_CHOICE_CHOOSE_IMAGE); + break; + case R.id.attach_take_picture: + attachFile(ATTACHMENT_CHOICE_TAKE_PHOTO); + break; + case R.id.attach_record_voice: + attachFile(ATTACHMENT_CHOICE_RECORD_VOICE); + break; + } + return false; + } + }); + attachFilePopup.show(); break; case R.id.action_add: startActivity(new Intent(this, ContactsActivity.class)); @@ -433,10 +492,12 @@ public class ConversationActivity extends XmppActivity { break; case R.id.action_contact_details: Contact contact = this.getSelectedConversation().getContact(); - if (contact != null) { + if (contact.showInRoster()) { Intent intent = new Intent(this, ContactDetailsActivity.class); intent.setAction(ContactDetailsActivity.ACTION_VIEW_CONTACT); - intent.putExtra("uuid", contact.getUuid()); + intent.putExtra("account", this.getSelectedConversation() + .getAccount().getJid()); + intent.putExtra("contact", contact.getJid()); startActivity(intent); } else { showAddToRosterDialog(getSelectedConversation()); @@ -452,7 +513,7 @@ public class ConversationActivity extends XmppActivity { Intent inviteIntent = new Intent(getApplicationContext(), ContactsActivity.class); inviteIntent.setAction("invite"); - inviteIntent.putExtra("uuid", selectedConversation.getUuid()); + inviteIntent.putExtra("uuid", getSelectedConversation().getUuid()); startActivity(inviteIntent); break; case R.id.action_security: @@ -468,25 +529,33 @@ public class ConversationActivity extends XmppActivity { public boolean onMenuItemClick(MenuItem item) { switch (item.getItemId()) { case R.id.encryption_choice_none: - conversation.setNextEncryption(Message.ENCRYPTION_NONE); + conversation + .setNextEncryption(Message.ENCRYPTION_NONE); item.setChecked(true); break; case R.id.encryption_choice_otr: - conversation.setNextEncryption(Message.ENCRYPTION_OTR); + conversation + .setNextEncryption(Message.ENCRYPTION_OTR); item.setChecked(true); break; case R.id.encryption_choice_pgp: if (hasPgp()) { - if (conversation.getAccount().getKeys().has("pgp_signature")) { - conversation.setNextEncryption(Message.ENCRYPTION_PGP); + if (conversation.getAccount().getKeys() + .has("pgp_signature")) { + conversation + .setNextEncryption(Message.ENCRYPTION_PGP); item.setChecked(true); } else { - announcePgp(conversation.getAccount(),conversation); + announcePgp(conversation.getAccount(), + conversation); } + } else { + showInstallPgpDialog(); } break; default: - conversation.setNextEncryption(Message.ENCRYPTION_NONE); + conversation + .setNextEncryption(Message.ENCRYPTION_NONE); break; } fragment.updateChatMsgHint(); @@ -494,14 +563,18 @@ public class ConversationActivity extends XmppActivity { } }); popup.inflate(R.menu.encryption_choices); + MenuItem otr = popup.getMenu().findItem( + R.id.encryption_choice_otr); + if (conversation.getMode() == Conversation.MODE_MULTI) { + otr.setEnabled(false); + } switch (conversation.getNextEncryption()) { case Message.ENCRYPTION_NONE: popup.getMenu().findItem(R.id.encryption_choice_none) .setChecked(true); break; case Message.ENCRYPTION_OTR: - popup.getMenu().findItem(R.id.encryption_choice_otr) - .setChecked(true); + otr.setChecked(true); break; case Message.ENCRYPTION_PGP: popup.getMenu().findItem(R.id.encryption_choice_pgp) @@ -524,36 +597,40 @@ public class ConversationActivity extends XmppActivity { } return super.onOptionsItemSelected(item); } - + private void endConversation(Conversation conversation) { conversation.setStatus(Conversation.STATUS_ARCHIVED); paneShouldBeOpen = true; spl.openPane(); xmppConnectionService.archiveConversation(conversation); if (conversationList.size() > 0) { - selectedConversation = conversationList.get(0); + setSelectedConversation(conversationList.get(0)); } else { - selectedConversation = null; + setSelectedConversation(null); } } protected void clearHistoryDialog(final Conversation conversation) { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(getString(R.string.clear_conversation_history)); - View dialogView = getLayoutInflater().inflate(R.layout.dialog_clear_history, null); - final CheckBox endConversationCheckBox = (CheckBox) dialogView.findViewById(R.id.end_conversation_checkbox); + View dialogView = getLayoutInflater().inflate( + R.layout.dialog_clear_history, null); + final CheckBox endConversationCheckBox = (CheckBox) dialogView + .findViewById(R.id.end_conversation_checkbox); builder.setView(dialogView); builder.setNegativeButton(getString(R.string.cancel), null); - builder.setPositiveButton(getString(R.string.delete_messages), new OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - activity.xmppConnectionService.clearConversationHistory(conversation); - if (endConversationCheckBox.isChecked()) { - endConversation(conversation); - } - } - }); + builder.setPositiveButton(getString(R.string.delete_messages), + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + activity.xmppConnectionService + .clearConversationHistory(conversation); + if (endConversationCheckBox.isChecked()) { + endConversation(conversation); + } + } + }); builder.create().show(); } @@ -580,11 +657,30 @@ public class ConversationActivity extends XmppActivity { } @Override + protected void onNewIntent(Intent intent) { + if ((Intent.ACTION_VIEW.equals(intent.getAction()) && (VIEW_CONVERSATION + .equals(intent.getType())))) { + String convToView = (String) intent.getExtras().get(CONVERSATION); + updateConversationList(); + for (int i = 0; i < conversationList.size(); ++i) { + if (conversationList.get(i).getUuid().equals(convToView)) { + setSelectedConversation(conversationList.get(i)); + break; + } + } + paneShouldBeOpen = false; + String text = intent.getExtras().getString(TEXT, null); + swapConversationFragment().setText(text); + } + } + + @Override public void onStart() { super.onStart(); SharedPreferences preferences = PreferenceManager .getDefaultSharedPreferences(this); this.useSubject = preferences.getBoolean("use_subject_in_muc", true); + this.showLastseen = preferences.getBoolean("show_last_seen", false); if (this.xmppConnectionServiceBound) { this.onBackendConnected(); } @@ -619,7 +715,7 @@ public class ConversationActivity extends XmppActivity { for (int i = 0; i < conversationList.size(); ++i) { if (conversationList.get(i).getUuid().equals(convToView)) { - selectedConversation = conversationList.get(i); + setSelectedConversation(conversationList.get(i)); } } paneShouldBeOpen = false; @@ -642,7 +738,7 @@ public class ConversationActivity extends XmppActivity { if (selectedFragment != null) { selectedFragment.onBackendConnected(); } else { - selectedConversation = conversationList.get(0); + setSelectedConversation(conversationList.get(0)); swapConversationFragment(); } ExceptionHelper.checkForCrash(this, this.xmppConnectionService); @@ -658,7 +754,8 @@ public class ConversationActivity extends XmppActivity { } @Override - protected void onActivityResult(int requestCode, int resultCode, final Intent data) { + protected void onActivityResult(int requestCode, int resultCode, + final Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == RESULT_OK) { if (requestCode == REQUEST_DECRYPT_PGP) { @@ -668,305 +765,213 @@ public class ConversationActivity extends XmppActivity { selectedFragment.hidePgpPassphraseBox(); } } else if (requestCode == REQUEST_ATTACH_FILE_DIALOG) { - prepareImageToast = Toast.makeText(getApplicationContext(), getText(R.string.preparing_image), Toast.LENGTH_LONG); - final Conversation conversation = getSelectedConversation(); - if (conversation.getNextEncryption() == Message.ENCRYPTION_NONE) { - prepareImageToast.show(); - this.pendingMessage = xmppConnectionService.attachImageToConversation(conversation, data.getData(),new UiCallback() { - - @Override - public void userInputRequried(PendingIntent pi) { - - } - - @Override - public void success() { - sendPendingImageMessage(); - } - - @Override - public void error(int error) { - pendingMessage = null; - displayErrorDialog(error); - } - }); - } else if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) { - prepareImageToast.show(); - attachPgpFile(conversation,data.getData()); - } else { - Log.d(LOGTAG,"unknown next message encryption: "+conversation.getNextEncryption()); - } + attachImageToConversation(getSelectedConversation(), + data.getData()); } else if (requestCode == REQUEST_SEND_PGP_IMAGE) { - - } else if (requestCode == REQUEST_ATTACH_FILE) { - attachFile(); + + } else if (requestCode == ATTACHMENT_CHOICE_CHOOSE_IMAGE) { + attachFile(ATTACHMENT_CHOICE_CHOOSE_IMAGE); + } else if (requestCode == ATTACHMENT_CHOICE_TAKE_PHOTO) { + attachFile(ATTACHMENT_CHOICE_TAKE_PHOTO); } else if (requestCode == REQUEST_ANNOUNCE_PGP) { - announcePgp(getSelectedConversation().getAccount(),getSelectedConversation()); + announcePgp(getSelectedConversation().getAccount(), + getSelectedConversation()); } else if (requestCode == REQUEST_ENCRYPT_MESSAGE) { - encryptTextMessage(); + // encryptTextMessage(); + } else if (requestCode == REQUEST_IMAGE_CAPTURE) { + attachImageToConversation(getSelectedConversation(), null); + } else if (requestCode == REQUEST_RECORD_AUDIO) { + Log.d("xmppService", data.getData().toString()); + attachAudioToConversation(getSelectedConversation(), + data.getData()); } else { - Log.d(LOGTAG,"unknown result code:"+requestCode); + Log.d(LOGTAG, "unknown result code:" + requestCode); } } } - - private void attachPgpFile(Conversation conversation, Uri uri) { - pendingMessage = xmppConnectionService.attachImageToConversation(conversation, uri, new UiCallback() { - - @Override - public void userInputRequried(PendingIntent pi) { - ConversationActivity.this.runIntent(pi, ConversationActivity.REQUEST_SEND_PGP_IMAGE); - } - - @Override - public void success() { - sendPendingImageMessage(); - } - + + private void attachAudioToConversation(Conversation conversation, Uri uri) { + + } + + private void attachImageToConversation(Conversation conversation, Uri uri) { + prepareImageToast = Toast.makeText(getApplicationContext(), + getText(R.string.preparing_image), Toast.LENGTH_LONG); + prepareImageToast.show(); + xmppConnectionService.attachImageToConversation(conversation, uri, + new UiCallback<Message>() { + + @Override + public void userInputRequried(PendingIntent pi, + Message object) { + hidePrepareImageToast(); + ConversationActivity.this.runIntent(pi, + ConversationActivity.REQUEST_SEND_PGP_IMAGE); + } + + @Override + public void success(Message message) { + xmppConnectionService.sendMessage(message); + } + + @Override + public void error(int error, Message message) { + hidePrepareImageToast(); + displayErrorDialog(error); + } + }); + } + + private void hidePrepareImageToast() { + if (prepareImageToast != null) { + runOnUiThread(new Runnable() { + @Override - public void error(int error) { - pendingMessage = null; - displayErrorDialog(error); + public void run() { + prepareImageToast.cancel(); } }); + } } - private void sendPendingImageMessage() { - pendingMessage.getConversation().getMessages().add(pendingMessage); - xmppConnectionService.databaseBackend.createMessage(pendingMessage); - xmppConnectionService.sendMessage(pendingMessage, null); - xmppConnectionService.updateUi(pendingMessage.getConversation(), false); - pendingMessage = null; - } - public void updateConversationList() { conversationList.clear(); conversationList.addAll(xmppConnectionService.getConversations()); listView.invalidateViews(); } - - public void selectPresence(final Conversation conversation, final OnPresenceSelected listener, String reason) { - Account account = conversation.getAccount(); - if (account.getStatus() != Account.STATUS_ONLINE) { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(getString(R.string.not_connected)); - builder.setIconAttribute(android.R.attr.alertDialogIcon); - if ("otr".equals(reason)) { - builder.setMessage(getString(R.string.you_are_offline,getString(R.string.otr_messages))); - } else if ("file".equals(reason)) { - builder.setMessage(getString(R.string.you_are_offline,getString(R.string.files))); - } else { - builder.setMessage(getString(R.string.you_are_offline_blank)); - } - builder.setNegativeButton(getString(R.string.cancel), null); - builder.setPositiveButton(getString(R.string.manage_account), new OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - startActivity(new Intent(activity, ManageAccountActivity.class)); - } - }); - builder.create().show(); - listener.onPresenceSelected(false, null); + + public boolean showLastseen() { + if (getSelectedConversation() == null) { + return false; } else { - Contact contact = conversation.getContact(); - if (contact==null) { - showAddToRosterDialog(conversation); - listener.onPresenceSelected(false,null); - } else { - Hashtable<String, Integer> presences = contact.getPresences(); - if (presences.size() == 0) { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(getString(R.string.contact_offline)); - if ("otr".equals(reason)) { - builder.setMessage(getString(R.string.contact_offline_otr)); - builder.setPositiveButton(getString(R.string.send_unencrypted), new OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - listener.onSendPlainTextInstead(); - } - }); - } else if ("file".equals(reason)) { - builder.setMessage(getString(R.string.contact_offline_file)); - } - builder.setIconAttribute(android.R.attr.alertDialogIcon); - builder.setNegativeButton(getString(R.string.cancel), null); - builder.create().show(); - listener.onPresenceSelected(false, null); - } else if (presences.size() == 1) { - String presence = (String) presences.keySet().toArray()[0]; - conversation.setNextPresence(presence); - listener.onPresenceSelected(true, presence); - } else { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(getString(R.string.choose_presence)); - final String[] presencesArray = new String[presences.size()]; - presences.keySet().toArray(presencesArray); - builder.setItems(presencesArray, - new DialogInterface.OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, - int which) { - String presence = presencesArray[which]; - conversation.setNextPresence(presence); - listener.onPresenceSelected(true,presence); - } - }); - builder.create().show(); - } - } + return this.showLastseen + && getSelectedConversation().getMode() == Conversation.MODE_SINGLE; } } - - private void showAddToRosterDialog(final Conversation conversation) { - String jid = conversation.getContactJid(); - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(jid); - builder.setMessage(getString(R.string.not_in_roster)); - builder.setNegativeButton(getString(R.string.cancel), null); - builder.setPositiveButton(getString(R.string.add_contact), new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - String jid = conversation.getContactJid(); - Account account = getSelectedConversation().getAccount(); - String name = jid.split("@")[0]; - Contact contact = new Contact(account, name, jid, null); - xmppConnectionService.createContact(contact); - } - }); - builder.create().show(); - } - public void runIntent(PendingIntent pi, int requestCode) { try { - this.startIntentSenderForResult(pi.getIntentSender(),requestCode, null, 0, - 0, 0); + this.startIntentSenderForResult(pi.getIntentSender(), requestCode, + null, 0, 0, 0); } catch (SendIntentException e1) { - Log.d("xmppService","failed to start intent to send message"); + Log.d("xmppService", "failed to start intent to send message"); } } - - + class BitmapWorkerTask extends AsyncTask<Message, Void, Bitmap> { - private final WeakReference<ImageView> imageViewReference; - private Message message = null; - - public BitmapWorkerTask(ImageView imageView) { - imageViewReference = new WeakReference<ImageView>(imageView); - } - - @Override - protected Bitmap doInBackground(Message... params) { - message = params[0]; - try { - return xmppConnectionService.getFileBackend().getThumbnail(message, (int) (metrics.density * 288),false); + private final WeakReference<ImageView> imageViewReference; + private Message message = null; + + public BitmapWorkerTask(ImageView imageView) { + imageViewReference = new WeakReference<ImageView>(imageView); + } + + @Override + protected Bitmap doInBackground(Message... params) { + message = params[0]; + try { + return xmppConnectionService.getFileBackend().getThumbnail( + message, (int) (metrics.density * 288), false); } catch (FileNotFoundException e) { - Log.d("xmppService","file not found!"); + Log.d("xmppService", "file not found!"); return null; } - } - - @Override - protected void onPostExecute(Bitmap bitmap) { - if (imageViewReference != null && bitmap != null) { - final ImageView imageView = imageViewReference.get(); - if (imageView != null) { - imageView.setImageBitmap(bitmap); - imageView.setBackgroundColor(0x00000000); - } - } - } - } - + } + + @Override + protected void onPostExecute(Bitmap bitmap) { + if (imageViewReference != null && bitmap != null) { + final ImageView imageView = imageViewReference.get(); + if (imageView != null) { + imageView.setImageBitmap(bitmap); + imageView.setBackgroundColor(0x00000000); + } + } + } + } + public void loadBitmap(Message message, ImageView imageView) { Bitmap bm; try { - bm = xmppConnectionService.getFileBackend().getThumbnail(message, (int) (metrics.density * 288), true); + bm = xmppConnectionService.getFileBackend().getThumbnail(message, + (int) (metrics.density * 288), true); } catch (FileNotFoundException e) { bm = null; } - if (bm!=null) { + if (bm != null) { imageView.setImageBitmap(bm); imageView.setBackgroundColor(0x00000000); } else { - if (cancelPotentialWork(message, imageView)) { - imageView.setBackgroundColor(0xff333333); - final BitmapWorkerTask task = new BitmapWorkerTask(imageView); - final AsyncDrawable asyncDrawable = - new AsyncDrawable(getResources(), null, task); - imageView.setImageDrawable(asyncDrawable); - task.execute(message); - } + if (cancelPotentialWork(message, imageView)) { + imageView.setBackgroundColor(0xff333333); + final BitmapWorkerTask task = new BitmapWorkerTask(imageView); + final AsyncDrawable asyncDrawable = new AsyncDrawable( + getResources(), null, task); + imageView.setImageDrawable(asyncDrawable); + task.execute(message); + } } } - - public static boolean cancelPotentialWork(Message message, ImageView imageView) { - final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); - - if (bitmapWorkerTask != null) { - final Message oldMessage = bitmapWorkerTask.message; - if (oldMessage == null || message != oldMessage) { - bitmapWorkerTask.cancel(true); - } else { - return false; - } - } - return true; - } - + + public static boolean cancelPotentialWork(Message message, + ImageView imageView) { + final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); + + if (bitmapWorkerTask != null) { + final Message oldMessage = bitmapWorkerTask.message; + if (oldMessage == null || message != oldMessage) { + bitmapWorkerTask.cancel(true); + } else { + return false; + } + } + return true; + } + private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { - if (imageView != null) { - final Drawable drawable = imageView.getDrawable(); - if (drawable instanceof AsyncDrawable) { - final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; - return asyncDrawable.getBitmapWorkerTask(); - } - } - return null; - } - + if (imageView != null) { + final Drawable drawable = imageView.getDrawable(); + if (drawable instanceof AsyncDrawable) { + final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; + return asyncDrawable.getBitmapWorkerTask(); + } + } + return null; + } + static class AsyncDrawable extends BitmapDrawable { - private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference; + private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference; - public AsyncDrawable(Resources res, Bitmap bitmap, - BitmapWorkerTask bitmapWorkerTask) { - super(res, bitmap); - bitmapWorkerTaskReference = - new WeakReference<BitmapWorkerTask>(bitmapWorkerTask); - } + public AsyncDrawable(Resources res, Bitmap bitmap, + BitmapWorkerTask bitmapWorkerTask) { + super(res, bitmap); + bitmapWorkerTaskReference = new WeakReference<BitmapWorkerTask>( + bitmapWorkerTask); + } - public BitmapWorkerTask getBitmapWorkerTask() { - return bitmapWorkerTaskReference.get(); - } + public BitmapWorkerTask getBitmapWorkerTask() { + return bitmapWorkerTaskReference.get(); + } } - public void encryptTextMessage() { - xmppConnectionService.getPgpEngine().encrypt(this.pendingMessage, new UiCallback() { + public void encryptTextMessage(Message message) { + xmppConnectionService.getPgpEngine().encrypt(message, + new UiCallback<Message>() { @Override - public void userInputRequried( - PendingIntent pi) { - activity.runIntent( - pi, + public void userInputRequried(PendingIntent pi, + Message message) { + activity.runIntent(pi, ConversationActivity.REQUEST_SEND_MESSAGE); } @Override - public void success() { - xmppConnectionService.sendMessage(pendingMessage, null); - pendingMessage = null; - ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager() - .findFragmentByTag("conversation"); - if (selectedFragment != null) { - selectedFragment.clearInputField(); - } + public void success(Message message) { + xmppConnectionService.sendMessage(message); } @Override - public void error(int error) { + public void error(int error, Message message) { } }); diff --git a/src/eu/siacs/conversations/ui/ConversationFragment.java b/src/eu/siacs/conversations/ui/ConversationFragment.java index 91a39ecc..2eac7ad0 100644 --- a/src/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/eu/siacs/conversations/ui/ConversationFragment.java @@ -5,17 +5,15 @@ import java.util.HashMap; import java.util.List; import java.util.Set; -import org.openintents.openpgp.OpenPgpError; - import net.java.otr4j.session.SessionStatus; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.PgpEngine; -import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.MucOptions.OnRenameListener; +import eu.siacs.conversations.services.ImageProvider; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.jingle.JingleConnection; @@ -30,17 +28,20 @@ import android.content.SharedPreferences; import android.content.IntentSender.SendIntentException; import android.graphics.Bitmap; import android.graphics.Typeface; -import android.net.Uri; import android.os.Bundle; import android.preference.PreferenceManager; import android.text.Editable; import android.text.Selection; import android.util.DisplayMetrics; import android.util.Log; +import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; +import android.view.View.OnLongClickListener; import android.view.ViewGroup; +import android.widget.AbsListView.OnScrollListener; +import android.widget.AbsListView; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.EditText; @@ -69,6 +70,7 @@ public class ConversationFragment extends Fragment { protected Bitmap selfBitmap; private boolean useSubject = true; + private boolean messagesLoaded = false; private IntentSender askForPassphraseIntent = null; @@ -93,7 +95,7 @@ public class ConversationFragment extends Fragment { @Override public void onClick(View v) { - if (askForPassphraseIntent != null) { + if (activity.hasPgp() && askForPassphraseIntent != null) { try { getActivity().startIntentSenderForResult( askForPassphraseIntent, @@ -119,6 +121,33 @@ public class ConversationFragment extends Fragment { startActivity(intent); } }; + + private OnScrollListener mOnScrollListener = new OnScrollListener() { + + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + // TODO Auto-generated method stub + + } + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, + int visibleItemCount, int totalItemCount) { + if (firstVisibleItem == 0 && messagesLoaded) { + long timestamp = messageList.get(0).getTimeSent(); + messagesLoaded = false; + List<Message> messages = activity.xmppConnectionService + .getMoreMessages(conversation, timestamp); + messageList.addAll(0, messages); + messageListAdapter.notifyDataSetChanged(); + if (messages.size() != 0) { + messagesLoaded = true; + } + messagesView.setSelectionFromTop(messages.size() + 1, 0); + } + } + }; + private ConversationActivity activity; public void hidePgpPassphraseBox() { @@ -126,22 +155,18 @@ public class ConversationFragment extends Fragment { } public void updateChatMsgHint() { - if (conversation.getMode() == Conversation.MODE_MULTI) { - chatMsg.setHint(getString(R.string.send_message_to_conference)); - } else { - switch (conversation.getNextEncryption()) { - case Message.ENCRYPTION_NONE: - chatMsg.setHint(getString(R.string.send_plain_text_message)); - break; - case Message.ENCRYPTION_OTR: - chatMsg.setHint(getString(R.string.send_otr_message)); - break; - case Message.ENCRYPTION_PGP: - chatMsg.setHint(getString(R.string.send_pgp_message)); - break; - default: - break; - } + switch (conversation.getNextEncryption()) { + case Message.ENCRYPTION_NONE: + chatMsg.setHint(getString(R.string.send_plain_text_message)); + break; + case Message.ENCRYPTION_OTR: + chatMsg.setHint(getString(R.string.send_otr_message)); + break; + case Message.ENCRYPTION_PGP: + chatMsg.setHint(getString(R.string.send_pgp_message)); + break; + default: + break; } } @@ -168,6 +193,8 @@ public class ConversationFragment extends Fragment { mucErrorText = (TextView) view.findViewById(R.id.muc_error_msg); messagesView = (ListView) view.findViewById(R.id.messages_view); + messagesView.setOnScrollListener(mOnScrollListener); + messagesView.setTranscriptMode(ListView.TRANSCRIPT_MODE_NORMAL); messageListAdapter = new ArrayAdapter<Message>(this.getActivity() .getApplicationContext(), R.layout.message_sent, @@ -175,15 +202,18 @@ public class ConversationFragment extends Fragment { private static final int SENT = 0; private static final int RECIEVED = 1; + private static final int STATUS = 2; @Override public int getViewTypeCount() { - return 2; + return 3; } @Override public int getItemViewType(int position) { - if (getItem(position).getStatus() <= Message.STATUS_RECIEVED) { + if (getItem(position).getType() == Message.TYPE_STATUS) { + return STATUS; + } else if (getItem(position).getStatus() <= Message.STATUS_RECIEVED) { return RECIEVED; } else { return SENT; @@ -194,6 +224,8 @@ public class ConversationFragment extends Fragment { String filesize = null; String info = null; boolean error = false; + boolean multiReceived = message.getConversation().getMode() == Conversation.MODE_MULTI + && message.getStatus() <= Message.STATUS_RECIEVED; if (message.getType() == Message.TYPE_IMAGE) { String[] fileParams = message.getBody().split(","); try { @@ -204,6 +236,9 @@ public class ConversationFragment extends Fragment { } } switch (message.getStatus()) { + case Message.STATUS_WAITING: + info = getString(R.string.waiting); + break; case Message.STATUS_UNSEND: info = getString(R.string.sending); break; @@ -219,8 +254,7 @@ public class ConversationFragment extends Fragment { error = true; break; default: - if ((message.getConversation().getMode() == Conversation.MODE_MULTI) - && (message.getStatus() <= Message.STATUS_RECIEVED)) { + if (multiReceived) { info = message.getCounterpart(); } break; @@ -236,8 +270,8 @@ public class ConversationFragment extends Fragment { viewHolder.indicator.setVisibility(View.VISIBLE); } - String formatedTime = UIHelper.readableTimeDifference(message - .getTimeSent()); + String formatedTime = UIHelper.readableTimeDifference( + getContext(), message.getTimeSent()); if (message.getStatus() <= Message.STATUS_RECIEVED) { if ((filesize != null) && (info != null)) { viewHolder.time.setText(filesize + " \u00B7 " + info); @@ -254,8 +288,12 @@ public class ConversationFragment extends Fragment { if ((filesize != null) && (info != null)) { viewHolder.time.setText(filesize + " \u00B7 " + info); } else if ((filesize == null) && (info != null)) { - viewHolder.time.setText(info + " \u00B7 " - + formatedTime); + if (error) { + viewHolder.time.setText(info + " \u00B7 " + + formatedTime); + } else { + viewHolder.time.setText(info); + } } else if ((filesize != null) && (info == null)) { viewHolder.time.setText(filesize + " \u00B7 " + formatedTime); @@ -266,22 +304,28 @@ public class ConversationFragment extends Fragment { } private void displayInfoMessage(ViewHolder viewHolder, int r) { - viewHolder.download_button.setVisibility(View.GONE); + if (viewHolder.download_button != null) { + viewHolder.download_button.setVisibility(View.GONE); + } viewHolder.image.setVisibility(View.GONE); viewHolder.messageBody.setVisibility(View.VISIBLE); viewHolder.messageBody.setText(getString(r)); viewHolder.messageBody.setTextColor(0xff33B5E5); viewHolder.messageBody.setTypeface(null, Typeface.ITALIC); + viewHolder.messageBody.setTextIsSelectable(false); } private void displayDecryptionFailed(ViewHolder viewHolder) { - viewHolder.download_button.setVisibility(View.GONE); + if (viewHolder.download_button != null) { + viewHolder.download_button.setVisibility(View.GONE); + } viewHolder.image.setVisibility(View.GONE); viewHolder.messageBody.setVisibility(View.VISIBLE); viewHolder.messageBody .setText(getString(R.string.decryption_failed)); viewHolder.messageBody.setTextColor(0xFFe92727); viewHolder.messageBody.setTypeface(null, Typeface.NORMAL); + viewHolder.messageBody.setTextIsSelectable(false); } private void displayTextMessage(ViewHolder viewHolder, String text) { @@ -297,6 +341,7 @@ public class ConversationFragment extends Fragment { } viewHolder.messageBody.setTextColor(0xff333333); viewHolder.messageBody.setTypeface(null, Typeface.NORMAL); + viewHolder.messageBody.setTextIsSelectable(true); } private void displayImageMessage(ViewHolder viewHolder, @@ -329,18 +374,27 @@ public class ConversationFragment extends Fragment { @Override public void onClick(View v) { - Uri uri = Uri - .parse("content://eu.siacs.conversations.images/" - + message.getConversationUuid() - + "/" - + message.getUuid()); - Log.d("xmppService", - "staring intent with uri:" + uri.toString()); Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setDataAndType(uri, "image/*"); + intent.setDataAndType( + ImageProvider.getContentUri(message), "image/*"); startActivity(intent); } }); + viewHolder.image + .setOnLongClickListener(new OnLongClickListener() { + + @Override + public boolean onLongClick(View v) { + Intent shareIntent = new Intent(); + shareIntent.setAction(Intent.ACTION_SEND); + shareIntent.putExtra(Intent.EXTRA_STREAM, + ImageProvider.getContentUri(message)); + shareIntent.setType("image/webp"); + startActivity(Intent.createChooser(shareIntent, + getText(R.string.share_with))); + return true; + } + }); } @Override @@ -354,13 +408,26 @@ public class ConversationFragment extends Fragment { case SENT: view = (View) inflater.inflate(R.layout.message_sent, null); + viewHolder.message_box = (LinearLayout) view + .findViewById(R.id.message_box); viewHolder.contact_picture = (ImageView) view .findViewById(R.id.message_photo); viewHolder.contact_picture.setImageBitmap(selfBitmap); + viewHolder.indicator = (ImageView) view + .findViewById(R.id.security_indicator); + viewHolder.image = (ImageView) view + .findViewById(R.id.message_image); + viewHolder.messageBody = (TextView) view + .findViewById(R.id.message_body); + viewHolder.time = (TextView) view + .findViewById(R.id.message_time); + view.setTag(viewHolder); break; case RECIEVED: view = (View) inflater.inflate( R.layout.message_recieved, null); + viewHolder.message_box = (LinearLayout) view + .findViewById(R.id.message_box); viewHolder.contact_picture = (ImageView) view .findViewById(R.id.message_photo); @@ -379,40 +446,61 @@ public class ConversationFragment extends Fragment { .getApplicationContext())); } + viewHolder.indicator = (ImageView) view + .findViewById(R.id.security_indicator); + viewHolder.image = (ImageView) view + .findViewById(R.id.message_image); + viewHolder.messageBody = (TextView) view + .findViewById(R.id.message_body); + viewHolder.time = (TextView) view + .findViewById(R.id.message_time); + view.setTag(viewHolder); + break; + case STATUS: + view = (View) inflater.inflate(R.layout.message_status, + null); + viewHolder.contact_picture = (ImageView) view + .findViewById(R.id.message_photo); + if (item.getConversation().getMode() == Conversation.MODE_SINGLE) { + + viewHolder.contact_picture + .setImageBitmap(mBitmapCache.get( + item.getConversation().getName( + useSubject), item + .getConversation() + .getContact(), + getActivity() + .getApplicationContext())); + viewHolder.contact_picture.setAlpha(128); + + } break; default: viewHolder = null; break; } - viewHolder.indicator = (ImageView) view - .findViewById(R.id.security_indicator); - viewHolder.image = (ImageView) view - .findViewById(R.id.message_image); - viewHolder.messageBody = (TextView) view - .findViewById(R.id.message_body); - viewHolder.time = (TextView) view - .findViewById(R.id.message_time); - view.setTag(viewHolder); } else { viewHolder = (ViewHolder) view.getTag(); } + if (type == STATUS) { + return view; + } + if (type == RECIEVED) { if (item.getConversation().getMode() == Conversation.MODE_MULTI) { - if (item.getCounterpart() != null) { - viewHolder.contact_picture - .setImageBitmap(mBitmapCache.get(item - .getCounterpart(), null, - getActivity() - .getApplicationContext())); - } else { - viewHolder.contact_picture - .setImageBitmap(mBitmapCache.get( - item.getConversation().getName( - useSubject), null, - getActivity() - .getApplicationContext())); - } + viewHolder.contact_picture.setImageBitmap(mBitmapCache + .get(item.getCounterpart(), null, getActivity() + .getApplicationContext())); + viewHolder.contact_picture + .setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + highlightInConference(item + .getCounterpart()); + } + }); } } @@ -439,7 +527,8 @@ public class ConversationFragment extends Fragment { } }); } else if ((item.getEncryption() == Message.ENCRYPTION_DECRYPTED) - || (item.getEncryption() == Message.ENCRYPTION_NONE)) { + || (item.getEncryption() == Message.ENCRYPTION_NONE) + || (item.getEncryption() == Message.ENCRYPTION_OTR)) { displayImageMessage(viewHolder, item); } else if (item.getEncryption() == Message.ENCRYPTION_PGP) { displayInfoMessage(viewHolder, @@ -449,8 +538,21 @@ public class ConversationFragment extends Fragment { } } else { if (item.getEncryption() == Message.ENCRYPTION_PGP) { - displayInfoMessage(viewHolder, - R.string.encrypted_message); + if (activity.hasPgp()) { + displayInfoMessage(viewHolder, + R.string.encrypted_message); + } else { + displayInfoMessage(viewHolder, + R.string.install_openkeychain); + viewHolder.message_box + .setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + activity.showInstallPgpDialog(); + } + }); + } } else if (item.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) { displayDecryptionFailed(viewHolder); } else { @@ -468,6 +570,18 @@ public class ConversationFragment extends Fragment { return view; } + protected void highlightInConference(String nick) { + String oldString = chatMsg.getText().toString().trim(); + if (oldString.isEmpty()) { + chatMsg.setText(nick + ": "); + } else { + chatMsg.setText(oldString + " " + nick + " "); + } + int position = chatMsg.length(); + Editable etext = chatMsg.getText(); + Selection.setSelection(etext, position); + } + protected Bitmap findSelfPicture() { SharedPreferences sharedPref = PreferenceManager .getDefaultSharedPreferences(getActivity() @@ -494,7 +608,7 @@ public class ConversationFragment extends Fragment { @Override public void onStop() { super.onStop(); - if (this.conversation!=null) { + if (this.conversation != null) { this.conversation.setNextMessage(chatMsg.getText().toString()); } } @@ -504,10 +618,16 @@ public class ConversationFragment extends Fragment { if (this.conversation == null) { return; } + String oldString = conversation.getNextMessage().trim(); if (this.pastedText == null) { - this.chatMsg.setText(conversation.getNextMessage()); + this.chatMsg.setText(oldString); } else { - chatMsg.setText(conversation.getNextMessage() + " " + pastedText); + + if (oldString.isEmpty()) { + chatMsg.setText(pastedText); + } else { + chatMsg.setText(oldString + " " + pastedText); + } pastedText = null; } int position = chatMsg.length(); @@ -519,10 +639,10 @@ public class ConversationFragment extends Fragment { if (!activity.shouldPaneBeOpen()) { activity.getSlidingPaneLayout().closePane(); activity.getActionBar().setDisplayHomeAsUpEnabled(true); + activity.getActionBar().setHomeButtonEnabled(true); activity.getActionBar().setTitle( conversation.getName(useSubject)); activity.invalidateOptionsMenu(); - } } if (conversation.getMode() == Conversation.MODE_MULTI) { @@ -555,50 +675,61 @@ public class ConversationFragment extends Fragment { } } - private void decryptMessage(final Message message) { + private void decryptMessage(Message message) { PgpEngine engine = activity.xmppConnectionService.getPgpEngine(); if (engine != null) { - engine.decrypt(message, new UiCallback() { + engine.decrypt(message, new UiCallback<Message>() { @Override - public void userInputRequried(PendingIntent pi) { + public void userInputRequried(PendingIntent pi, Message message) { askForPassphraseIntent = pi.getIntentSender(); pgpInfo.setVisibility(View.VISIBLE); } @Override - public void success() { + public void success(Message message) { activity.xmppConnectionService.databaseBackend .updateMessage(message); updateMessages(); } @Override - public void error(int error) { + public void error(int error, Message message) { message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED); // updateMessages(); } }); } else { - pgpInfo.setVisibility(View.VISIBLE); + pgpInfo.setVisibility(View.GONE); } } public void updateMessages() { - if (getView()==null) { + if (getView() == null) { return; } ConversationActivity activity = (ConversationActivity) getActivity(); if (this.conversation != null) { for (Message message : this.conversation.getMessages()) { if ((message.getEncryption() == Message.ENCRYPTION_PGP) - && (message.getStatus() == Message.STATUS_RECIEVED)) { + && ((message.getStatus() == Message.STATUS_RECIEVED) || (message + .getStatus() == Message.STATUS_SEND))) { decryptMessage(message); break; } } - this.messageList.clear(); - this.messageList.addAll(this.conversation.getMessages()); + if (this.conversation.getMessages().size() == 0) { + this.messageList.clear(); + messagesLoaded = false; + } else { + for (Message message : this.conversation.getMessages()) { + if (!this.messageList.contains(message)) { + this.messageList.add(message); + } + } + messagesLoaded = true; + updateStatusMessages(); + } this.messageListAdapter.notifyDataSetChanged(); if (conversation.getMode() == Conversation.MODE_SINGLE) { if (messageList.size() >= 1) { @@ -616,11 +747,8 @@ public class ConversationFragment extends Fragment { } getActivity().invalidateOptionsMenu(); updateChatMsgHint(); - int size = this.messageList.size(); - if (size >= 1) - messagesView.setSelection(size - 1); if (!activity.shouldPaneBeOpen()) { - conversation.markRead(); + activity.xmppConnectionService.markRead(conversation); // TODO update notifications UIHelper.updateNotification(getActivity(), activity.getConversationList(), null, false); @@ -629,34 +757,61 @@ public class ConversationFragment extends Fragment { } } + private void messageSent() { + int size = this.messageList.size(); + if (size >= 1) { + messagesView.setSelection(size - 1); + } + chatMsg.setText(""); + } + + protected void updateStatusMessages() { + boolean addedStatusMsg = false; + if (conversation.getMode() == Conversation.MODE_SINGLE) { + for (int i = this.messageList.size() - 1; i >= 0; --i) { + if (addedStatusMsg) { + if (this.messageList.get(i).getType() == Message.TYPE_STATUS) { + this.messageList.remove(i); + --i; + } + } else { + if (this.messageList.get(i).getStatus() == Message.STATUS_RECIEVED) { + addedStatusMsg = true; + } else { + if (this.messageList.get(i).getStatus() == Message.STATUS_SEND_DISPLAYED) { + this.messageList.add(i + 1, + Message.createStatusMessage(conversation)); + addedStatusMsg = true; + } + } + } + } + } + } + protected void makeFingerprintWarning(int latestEncryption) { final LinearLayout fingerprintWarning = (LinearLayout) getView() .findViewById(R.id.new_fingerprint); - if (conversation.getContact() != null) { - Set<String> knownFingerprints = conversation.getContact() - .getOtrFingerprints(); - if ((latestEncryption == Message.ENCRYPTION_OTR) - && (conversation.hasValidOtrSession() - && (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) && (!knownFingerprints - .contains(conversation.getOtrFingerprint())))) { - fingerprintWarning.setVisibility(View.VISIBLE); - TextView fingerprint = (TextView) getView().findViewById( - R.id.otr_fingerprint); - fingerprint.setText(conversation.getOtrFingerprint()); - fingerprintWarning.setOnClickListener(new OnClickListener() { + Set<String> knownFingerprints = conversation.getContact() + .getOtrFingerprints(); + if ((latestEncryption == Message.ENCRYPTION_OTR) + && (conversation.hasValidOtrSession() + && (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) && (!knownFingerprints + .contains(conversation.getOtrFingerprint())))) { + fingerprintWarning.setVisibility(View.VISIBLE); + TextView fingerprint = (TextView) getView().findViewById( + R.id.otr_fingerprint); + fingerprint.setText(conversation.getOtrFingerprint()); + fingerprintWarning.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - AlertDialog dialog = UIHelper - .getVerifyFingerprintDialog( - (ConversationActivity) getActivity(), - conversation, fingerprintWarning); - dialog.show(); - } - }); - } else { - fingerprintWarning.setVisibility(View.GONE); - } + @Override + public void onClick(View v) { + AlertDialog dialog = UIHelper.getVerifyFingerprintDialog( + (ConversationActivity) getActivity(), conversation, + fingerprintWarning); + dialog.show(); + } + }); } else { fingerprintWarning.setVisibility(View.GONE); } @@ -664,97 +819,128 @@ public class ConversationFragment extends Fragment { protected void sendPlainTextMessage(Message message) { ConversationActivity activity = (ConversationActivity) getActivity(); - activity.xmppConnectionService.sendMessage(message, null); - chatMsg.setText(""); + activity.xmppConnectionService.sendMessage(message); + messageSent(); } protected void sendPgpMessage(final Message message) { - activity.pendingMessage = message; final ConversationActivity activity = (ConversationActivity) getActivity(); final XmppConnectionService xmppService = activity.xmppConnectionService; final Contact contact = message.getConversation().getContact(); if (activity.hasPgp()) { - if (contact.getPgpKeyId() != 0) { - xmppService.getPgpEngine().hasKey(contact, - new UiCallback() { + if (conversation.getMode() == Conversation.MODE_SINGLE) { + if (contact.getPgpKeyId() != 0) { + xmppService.getPgpEngine().hasKey(contact, + new UiCallback<Contact>() { - @Override - public void userInputRequried(PendingIntent pi) { - activity.runIntent( - pi, - ConversationActivity.REQUEST_ENCRYPT_MESSAGE); - } + @Override + public void userInputRequried(PendingIntent pi, + Contact contact) { + activity.runIntent( + pi, + ConversationActivity.REQUEST_ENCRYPT_MESSAGE); + } - @Override - public void success() { - activity.encryptTextMessage(); - } + @Override + public void success(Contact contact) { + messageSent(); + activity.encryptTextMessage(message); + } - @Override - public void error(int error) { - - } - }); + @Override + public void error(int error, Contact contact) { - } else { - showNoPGPKeyDialog(new DialogInterface.OnClickListener() { + } + }); - @Override - public void onClick(DialogInterface dialog, - int which) { - conversation - .setNextEncryption(Message.ENCRYPTION_NONE); - message.setEncryption(Message.ENCRYPTION_NONE); - xmppService.sendMessage(message, null); - chatMsg.setText(""); + } else { + showNoPGPKeyDialog(false, + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, + int which) { + conversation + .setNextEncryption(Message.ENCRYPTION_NONE); + message.setEncryption(Message.ENCRYPTION_NONE); + xmppService.sendMessage(message); + messageSent(); + } + }); + } + } else { + if (conversation.getMucOptions().pgpKeysInUse()) { + if (!conversation.getMucOptions().everybodyHasKeys()) { + Toast warning = Toast + .makeText(getActivity(), + R.string.missing_public_keys, + Toast.LENGTH_LONG); + warning.setGravity(Gravity.CENTER_VERTICAL, 0, 0); + warning.show(); } - }); + activity.encryptTextMessage(message); + messageSent(); + } else { + showNoPGPKeyDialog(true, + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, + int which) { + conversation + .setNextEncryption(Message.ENCRYPTION_NONE); + message.setEncryption(Message.ENCRYPTION_NONE); + xmppService.sendMessage(message); + messageSent(); + } + }); + } } + } else { + activity.showInstallPgpDialog(); } } - - public void showNoPGPKeyDialog(DialogInterface.OnClickListener listener) { - AlertDialog.Builder builder = new AlertDialog.Builder( - getActivity()); - builder.setTitle(getString(R.string.no_pgp_key)); + + public void showNoPGPKeyDialog(boolean plural, + DialogInterface.OnClickListener listener) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder.setIconAttribute(android.R.attr.alertDialogIcon); - builder.setMessage(getText(R.string.contact_has_no_pgp_key)); + if (plural) { + builder.setTitle(getString(R.string.no_pgp_keys)); + builder.setMessage(getText(R.string.contacts_have_no_pgp_keys)); + } else { + builder.setTitle(getString(R.string.no_pgp_key)); + builder.setMessage(getText(R.string.contact_has_no_pgp_key)); + } builder.setNegativeButton(getString(R.string.cancel), null); - builder.setPositiveButton(getString(R.string.send_unencrypted),listener); + builder.setPositiveButton(getString(R.string.send_unencrypted), + listener); builder.create().show(); } protected void sendOtrMessage(final Message message) { - ConversationActivity activity = (ConversationActivity) getActivity(); + final ConversationActivity activity = (ConversationActivity) getActivity(); final XmppConnectionService xmppService = activity.xmppConnectionService; if (conversation.hasValidOtrSession()) { - activity.xmppConnectionService.sendMessage(message, null); - chatMsg.setText(""); + activity.xmppConnectionService.sendMessage(message); + messageSent(); } else { activity.selectPresence(message.getConversation(), new OnPresenceSelected() { @Override - public void onPresenceSelected(boolean success, - String presence) { - if (success) { - xmppService.sendMessage(message, presence); - chatMsg.setText(""); - } + public void onPresenceSelected() { + message.setPresence(conversation.getNextPresence()); + xmppService.sendMessage(message); + messageSent(); } - - @Override - public void onSendPlainTextInstead() { - message.setEncryption(Message.ENCRYPTION_NONE); - xmppService.sendMessage(message, null); - chatMsg.setText(""); - } - }, "otr"); + }); } } private static class ViewHolder { + protected LinearLayout message_box; protected Button download_button; protected ImageView image; protected ImageView indicator; @@ -766,7 +952,6 @@ public class ConversationFragment extends Fragment { private class BitmapCache { private HashMap<String, Bitmap> bitmaps = new HashMap<String, Bitmap>(); - private Bitmap error = null; public Bitmap get(String name, Contact contact, Context context) { if (bitmaps.containsKey(name)) { diff --git a/src/eu/siacs/conversations/ui/EditAccount.java b/src/eu/siacs/conversations/ui/EditAccount.java index 8b1c0fa6..e1bcaeb5 100644 --- a/src/eu/siacs/conversations/ui/EditAccount.java +++ b/src/eu/siacs/conversations/ui/EditAccount.java @@ -7,7 +7,6 @@ import android.app.AlertDialog; import android.app.Dialog; import android.app.DialogFragment; import android.os.Bundle; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.widget.Button; @@ -51,27 +50,19 @@ public class EditAccount extends DialogFragment { final CheckBox registerAccount = (CheckBox) view .findViewById(R.id.edit_account_register_new); - final String okButtonDesc; - if (account != null) { jidText.setText(account.getJid()); password.setText(account.getPassword()); - Log.d("xmppService","mein debugger. account != null"); if (account.isOptionSet(Account.OPTION_REGISTER)) { registerAccount.setChecked(true); - builder.setTitle(getString(R.string.register_account)); - okButtonDesc = "Register"; passwordConfirm.setVisibility(View.VISIBLE); passwordConfirm.setText(account.getPassword()); } else { registerAccount.setVisibility(View.GONE); - builder.setTitle("Edit account"); - okButtonDesc = "Edit"; } - } else { - builder.setTitle("Add account"); - okButtonDesc = "Add"; } + builder.setTitle(R.string.account_settings); + registerAccount .setOnCheckedChangeListener(new OnCheckedChangeListener() { @@ -79,26 +70,19 @@ public class EditAccount extends DialogFragment { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - AlertDialog d = (AlertDialog) getDialog(); - Button positiveButton = (Button) d - .getButton(Dialog.BUTTON_POSITIVE); if (isChecked) { - d.setTitle(getString(R.string.register_account)); - positiveButton.setText("Register"); passwordConfirm.setVisibility(View.VISIBLE); confirmPwDesc.setVisibility(View.VISIBLE); } else { - d.setTitle("Add account"); passwordConfirm.setVisibility(View.GONE); - positiveButton.setText("Add"); confirmPwDesc.setVisibility(View.GONE); } } }); builder.setView(view); - builder.setNeutralButton("Cancel", null); - builder.setPositiveButton(okButtonDesc, null); + builder.setNeutralButton(getString(R.string.cancel), null); + builder.setPositiveButton(getString(R.string.save), null); return builder.create(); } @@ -114,7 +98,9 @@ public class EditAccount extends DialogFragment { String jid = jidEdit.getText().toString(); EditText passwordEdit = (EditText) d .findViewById(R.id.account_password); + EditText passwordConfirmEdit = (EditText) d.findViewById(R.id.account_password_confirm2); String password = passwordEdit.getText().toString(); + String passwordConfirm = passwordConfirmEdit.getText().toString(); CheckBox register = (CheckBox) d.findViewById(R.id.edit_account_register_new); String username; String server; @@ -123,9 +109,15 @@ public class EditAccount extends DialogFragment { username = parts[0]; server = parts[1]; } else { - jidEdit.setError("Invalid Jabber ID"); + jidEdit.setError(getString(R.string.invalid_jid)); return; } + if (register.isChecked()) { + if (!passwordConfirm.equals(password)) { + passwordConfirmEdit.setError(getString(R.string.passwords_do_not_match)); + return; + } + } if (account != null) { account.setPassword(password); account.setUsername(username); diff --git a/src/eu/siacs/conversations/ui/ManageAccountActivity.java b/src/eu/siacs/conversations/ui/ManageAccountActivity.java index 0b7dac58..c52916a2 100644 --- a/src/eu/siacs/conversations/ui/ManageAccountActivity.java +++ b/src/eu/siacs/conversations/ui/ManageAccountActivity.java @@ -3,25 +3,19 @@ package eu.siacs.conversations.ui; import java.util.ArrayList; import java.util.List; -import org.openintents.openpgp.OpenPgpError; - import eu.siacs.conversations.R; -import eu.siacs.conversations.crypto.PgpEngine; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.ui.EditAccount.EditAccountListener; import eu.siacs.conversations.xmpp.OnTLSExceptionReceived; import eu.siacs.conversations.xmpp.XmppConnection; import android.app.Activity; import android.app.AlertDialog; -import android.app.PendingIntent; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.content.Intent; -import android.content.IntentSender.SendIntentException; import android.os.Bundle; import android.os.SystemClock; -import android.util.Log; import android.view.ActionMode; import android.view.LayoutInflater; import android.view.Menu; @@ -73,7 +67,7 @@ public class ManageAccountActivity extends XmppActivity { @Override public void run() { AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setTitle("Untrusted Certificate"); + builder.setTitle(getString(R.string.account_status_error)); builder.setIconAttribute(android.R.attr.alertDialogIcon); View view = (View) getLayoutInflater().inflate(R.layout.cert_warning, null); TextView sha = (TextView) view.findViewById(R.id.sha); @@ -91,8 +85,8 @@ public class ManageAccountActivity extends XmppActivity { hint.setText(getString(R.string.untrusted_cert_hint,account.getServer())); sha.setText(humanReadableSha.toString()); builder.setView(view); - builder.setNegativeButton("Don't connect", null); - builder.setPositiveButton("Trust certificate", new OnClickListener() { + builder.setNegativeButton(getString(R.string.certif_no_trust), null); + builder.setPositiveButton(getString(R.string.certif_trust), new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { @@ -130,55 +124,55 @@ public class ManageAccountActivity extends XmppActivity { .findViewById(R.id.account_status); switch (account.getStatus()) { case Account.STATUS_DISABLED: - statusView.setText("temporarily disabled"); + statusView.setText(getString(R.string.account_status_disabled)); statusView.setTextColor(0xFF1da9da); break; case Account.STATUS_ONLINE: - statusView.setText("online"); + statusView.setText(getString(R.string.account_status_online)); statusView.setTextColor(0xFF83b600); break; case Account.STATUS_CONNECTING: - statusView.setText("connecting\u2026"); + statusView.setText(getString(R.string.account_status_connecting)); statusView.setTextColor(0xFF1da9da); break; case Account.STATUS_OFFLINE: - statusView.setText("offline"); + statusView.setText(getString(R.string.account_status_offline)); statusView.setTextColor(0xFFe92727); break; case Account.STATUS_UNAUTHORIZED: - statusView.setText("unauthorized"); + statusView.setText(getString(R.string.account_status_unauthorized)); statusView.setTextColor(0xFFe92727); break; case Account.STATUS_SERVER_NOT_FOUND: - statusView.setText("server not found"); + statusView.setText(getString(R.string.account_status_not_found)); statusView.setTextColor(0xFFe92727); break; case Account.STATUS_NO_INTERNET: - statusView.setText("no internet"); + statusView.setText(getString(R.string.account_status_no_internet)); statusView.setTextColor(0xFFe92727); break; case Account.STATUS_SERVER_REQUIRES_TLS: - statusView.setText("server requires TLS"); + statusView.setText(getString(R.string.account_status_requires_tls)); statusView.setTextColor(0xFFe92727); break; case Account.STATUS_TLS_ERROR: - statusView.setText("untrusted cerficate"); + statusView.setText(getString(R.string.account_status_error)); statusView.setTextColor(0xFFe92727); break; case Account.STATUS_REGISTRATION_FAILED: - statusView.setText("registration failed"); + statusView.setText(getString(R.string.account_status_regis_fail)); statusView.setTextColor(0xFFe92727); break; case Account.STATUS_REGISTRATION_CONFLICT: - statusView.setText("username already in use"); + statusView.setText(getString(R.string.account_status_regis_conflict)); statusView.setTextColor(0xFFe92727); break; case Account.STATUS_REGISTRATION_SUCCESSFULL: - statusView.setText("registration completed"); + statusView.setText(getString(R.string.account_status_regis_success)); statusView.setTextColor(0xFF83b600); break; case Account.STATUS_REGISTRATION_NOT_SUPPORTED: - statusView.setText("server does not support registration"); + statusView.setText(getString(R.string.account_status_regis_not_sup)); statusView.setTextColor(0xFFe92727); break; default: @@ -261,10 +255,10 @@ public class ManageAccountActivity extends XmppActivity { mode.finish(); } else if (item.getItemId()==R.id.mgmt_account_delete) { AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setTitle("Are you sure?"); + builder.setTitle(getString(R.string.mgmt_account_are_you_sure)); builder.setIconAttribute(android.R.attr.alertDialogIcon); - builder.setMessage("If you delete your account your entire conversation history will be lost"); - builder.setPositiveButton("Delete", new OnClickListener() { + builder.setMessage(getString(R.string.mgmt_account_delete_confirm_text)); + builder.setPositiveButton(getString(R.string.delete), new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { @@ -273,12 +267,14 @@ public class ManageAccountActivity extends XmppActivity { mode.finish(); } }); - builder.setNegativeButton("Cancel",null); + builder.setNegativeButton(getString(R.string.cancel),null); builder.create().show(); } else if (item.getItemId()==R.id.mgmt_account_announce_pgp) { if (activity.hasPgp()) { mode.finish(); announcePgp(selectedAccountForActionMode,null); + } else { + activity.showInstallPgpDialog(); } } else if (item.getItemId() == R.id.mgmt_otr_key) { AlertDialog.Builder builder = new AlertDialog.Builder(activity); @@ -293,7 +289,7 @@ public class ManageAccountActivity extends XmppActivity { noFingerprintView.setVisibility(View.GONE); } builder.setView(view); - builder.setPositiveButton("Done", null); + builder.setPositiveButton(getString(R.string.done), null); builder.create().show(); } else if (item.getItemId() == R.id.mgmt_account_info) { AlertDialog.Builder builder = new AlertDialog.Builder(activity); @@ -317,36 +313,36 @@ public class ManageAccountActivity extends XmppActivity { pcks_received.setText(""+xmpp.getReceivedStanzas()); pcks_sent.setText(""+xmpp.getSentStanzas()); if (connectionAgeHours >= 2) { - connection.setText(connectionAgeHours+" hours"); + connection.setText(connectionAgeHours+" " + getString(R.string.hours)); } else { - connection.setText(connectionAge+" mins"); + connection.setText(connectionAge+" " + getString(R.string.mins)); } if (xmpp.hasFeatureStreamManagment()) { if (sessionAgeHours >= 2) { - session.setText(sessionAgeHours+" hours"); + session.setText(sessionAgeHours+" " + getString(R.string.hours)); } else { - session.setText(sessionAge+" mins"); + session.setText(sessionAge+" " + getString(R.string.mins)); } - stream.setText("Yes"); + stream.setText(getString(R.string.yes)); } else { - stream.setText("No"); + stream.setText(getString(R.string.no)); session.setText(connection.getText()); } if (xmpp.hasFeaturesCarbon()) { - carbon.setText("Yes"); + carbon.setText(getString(R.string.yes)); } else { - carbon.setText("No"); + carbon.setText(getString(R.string.no)); } if (xmpp.hasFeatureRosterManagment()) { - roster.setText("Yes"); + roster.setText(getString(R.string.yes)); } else { - roster.setText("No"); + roster.setText(getString(R.string.no)); } builder.setView(view); } else { - builder.setMessage("Account is offline"); + builder.setMessage(getString(R.string.mgmt_account_account_offline)); } - builder.setPositiveButton("Hide", null); + builder.setPositiveButton(getString(R.string.hide), null); builder.create().show(); } return true; @@ -380,6 +376,7 @@ public class ManageAccountActivity extends XmppActivity { accountListViewAdapter.notifyDataSetChanged(); if ((this.accountList.size() == 0)&&(this.firstrun)) { getActionBar().setDisplayHomeAsUpEnabled(false); + getActionBar().setHomeButtonEnabled(false); addAccount(); this.firstrun = false; } @@ -403,6 +400,25 @@ public class ManageAccountActivity extends XmppActivity { return super.onOptionsItemSelected(item); } + @Override + public boolean onNavigateUp() { + if (xmppConnectionService.getConversations().size() == 0) { + Intent contactsIntent = new Intent(this, ContactsActivity.class); + contactsIntent.setFlags( + // if activity exists in stack, pop the stack and go back to it + Intent.FLAG_ACTIVITY_CLEAR_TOP | + // otherwise, make a new task for it + Intent.FLAG_ACTIVITY_NEW_TASK | + // don't use the new activity animation; finish animation runs instead + Intent.FLAG_ACTIVITY_NO_ANIMATION); + startActivity(contactsIntent); + finish(); + return true; + } else { + return super.onNavigateUp(); + } + } + private void editAccount(Account account) { EditAccount dialog = new EditAccount(); dialog.setAccount(account); @@ -429,6 +445,7 @@ public class ManageAccountActivity extends XmppActivity { public void onAccountEdited(Account account) { xmppConnectionService.createAccount(account); activity.getActionBar().setDisplayHomeAsUpEnabled(true); + activity.getActionBar().setHomeButtonEnabled(true); } }); dialog.show(getFragmentManager(), "add_account"); diff --git a/src/eu/siacs/conversations/ui/MucDetailsActivity.java b/src/eu/siacs/conversations/ui/MucDetailsActivity.java index c6807c61..ee6709b7 100644 --- a/src/eu/siacs/conversations/ui/MucDetailsActivity.java +++ b/src/eu/siacs/conversations/ui/MucDetailsActivity.java @@ -3,14 +3,19 @@ package eu.siacs.conversations.ui; import java.util.ArrayList; import java.util.List; +import org.openintents.openpgp.util.OpenPgpUtils; + import eu.siacs.conversations.R; +import eu.siacs.conversations.crypto.PgpEngine; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.MucOptions.User; import eu.siacs.conversations.utils.UIHelper; +import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; +import android.content.IntentSender.SendIntentException; import android.os.Bundle; import android.preference.PreferenceManager; import android.util.Log; @@ -19,8 +24,6 @@ import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.EditText; import android.widget.ImageButton; @@ -153,11 +156,11 @@ public class MucDetailsActivity extends XmppActivity { switch (self.getAffiliation()) { case User.AFFILIATION_ADMIN: mRoleAffiliaton.setText(getReadableRole(self.getRole()) - + " (Admin)"); + + " (" + getString(R.string.admin) + ")"); break; case User.AFFILIATION_OWNER: mRoleAffiliaton.setText(getReadableRole(self.getRole()) - + " (Owner)"); + + " (" + getString(R.string.owner) + ")"); break; default: mRoleAffiliaton @@ -167,16 +170,36 @@ public class MucDetailsActivity extends XmppActivity { } this.users.clear(); this.users.addAll(conversation.getMucOptions().getUsers()); - //contactsAdapter.notifyDataSetChanged(); LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); membersView.removeAllViews(); - for(User contact : conversation.getMucOptions().getUsers()) { + for(final User contact : conversation.getMucOptions().getUsers()) { View view = (View) inflater.inflate(R.layout.contact, null); - - ((TextView) view.findViewById(R.id.contact_display_name)) - .setText(contact.getName()); + TextView displayName = (TextView) view.findViewById(R.id.contact_display_name); + TextView key = (TextView) view.findViewById(R.id.key); + displayName.setText(contact.getName()); TextView role = (TextView) view.findViewById(R.id.contact_jid); role.setText(getReadableRole(contact.getRole())); + if (contact.getPgpKeyId()!=0) { + key.setVisibility(View.VISIBLE); + key.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + PgpEngine pgp = xmppConnectionService.getPgpEngine(); + if (pgp!=null) { + PendingIntent intent = pgp.getIntentForKey(conversation.getAccount(), contact.getPgpKeyId()); + if (intent!=null) { + try { + startIntentSenderForResult(intent.getIntentSender(), 0, null, 0, 0, 0); + } catch (SendIntentException e) { + + } + } + } + } + }); + key.setText(OpenPgpUtils.convertKeyIdToHex(contact.getPgpKeyId())); + } ImageView imageView = (ImageView) view .findViewById(R.id.contact_photo); imageView.setImageBitmap(UIHelper.getContactPicture(contact.getName(), 48,this.getApplicationContext(), false)); diff --git a/src/eu/siacs/conversations/ui/OnPresenceSelected.java b/src/eu/siacs/conversations/ui/OnPresenceSelected.java index b3a995dc..1c967224 100644 --- a/src/eu/siacs/conversations/ui/OnPresenceSelected.java +++ b/src/eu/siacs/conversations/ui/OnPresenceSelected.java @@ -1,6 +1,5 @@ package eu.siacs.conversations.ui; public interface OnPresenceSelected { - public void onPresenceSelected(boolean success, String presence); - public void onSendPlainTextInstead(); + public void onPresenceSelected(); } diff --git a/src/eu/siacs/conversations/ui/OnRosterFetchedListener.java b/src/eu/siacs/conversations/ui/OnRosterFetchedListener.java deleted file mode 100644 index d69ce35b..00000000 --- a/src/eu/siacs/conversations/ui/OnRosterFetchedListener.java +++ /dev/null @@ -1,9 +0,0 @@ -package eu.siacs.conversations.ui; - -import java.util.List; - -import eu.siacs.conversations.entities.Contact; - -public interface OnRosterFetchedListener { - public void onRosterFetched(List<Contact> roster); -} diff --git a/src/eu/siacs/conversations/ui/ShareWithActivity.java b/src/eu/siacs/conversations/ui/ShareWithActivity.java index 1bc9fc46..d4d23ddf 100644 --- a/src/eu/siacs/conversations/ui/ShareWithActivity.java +++ b/src/eu/siacs/conversations/ui/ShareWithActivity.java @@ -11,10 +11,13 @@ import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.utils.UIHelper; +import android.app.PendingIntent; import android.content.Intent; import android.content.SharedPreferences; import android.graphics.Bitmap; +import android.net.Uri; import android.os.Bundle; import android.preference.PreferenceManager; import android.util.Log; @@ -25,37 +28,49 @@ import android.widget.LinearLayout; import android.widget.TextView; public class ShareWithActivity extends XmppActivity { - + private LinearLayout conversations; private LinearLayout contacts; + private boolean isImage = false; - private OnClickListener click = new OnClickListener() { + private UiCallback<Message> attachImageCallback = new UiCallback<Message>() { @Override - public void onClick(View v) { + public void userInputRequried(PendingIntent pi, Message object) { + // TODO Auto-generated method stub + + } + + @Override + public void success(Message message) { + xmppConnectionService.sendMessage(message); + } + + @Override + public void error(int errorCode, Message object) { // TODO Auto-generated method stub } }; - + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.share_with); - setTitle("Share with Conversation"); - + setTitle(getString(R.string.title_activity_sharewith)); + contacts = (LinearLayout) findViewById(R.id.contacts); conversations = (LinearLayout) findViewById(R.id.conversations); - + } - - + public View createContactView(String name, String msgTxt, Bitmap bm) { View view = (View) getLayoutInflater().inflate(R.layout.contact, null); view.setBackgroundResource(R.drawable.greybackground); - TextView contactName =(TextView) view.findViewById(R.id.contact_display_name); + TextView contactName = (TextView) view + .findViewById(R.id.contact_display_name); contactName.setText(name); TextView msg = (TextView) view.findViewById(R.id.contact_jid); msg.setText(msgTxt); @@ -63,75 +78,100 @@ public class ShareWithActivity extends XmppActivity { imageView.setImageBitmap(bm); return view; } - - - + @Override void onBackendConnected() { - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); + this.isImage = (getIntent().getType() != null && getIntent() + .getType().startsWith("image/")); + SharedPreferences preferences = PreferenceManager + .getDefaultSharedPreferences(this); boolean useSubject = preferences.getBoolean("use_subject_in_muc", true); - - Set<String> displayedContacts = new HashSet<String>(); + + Set<Contact> displayedContacts = new HashSet<Contact>(); conversations.removeAllViews(); List<Conversation> convList = xmppConnectionService.getConversations(); Collections.sort(convList, new Comparator<Conversation>() { @Override public int compare(Conversation lhs, Conversation rhs) { - return (int) (rhs.getLatestMessage().getTimeSent() - lhs.getLatestMessage().getTimeSent()); + return (int) (rhs.getLatestMessage().getTimeSent() - lhs + .getLatestMessage().getTimeSent()); } }); - for(final Conversation conversation : convList) { - View view = createContactView(conversation.getName(useSubject), - conversation.getLatestMessage().getBody().trim(), - UIHelper.getContactPicture(conversation, 48, - this.getApplicationContext(), false)); - view.setOnClickListener(new OnClickListener() { - - @Override - public void onClick(View v) { - String sharedText = getIntent().getStringExtra(Intent.EXTRA_TEXT); - switchToConversation(conversation, sharedText); - finish(); - } - }); - conversations.addView(view); - if (conversation.getContact() != null) { - displayedContacts.add(conversation.getContact().getUuid()); + for (final Conversation conversation : convList) { + if (!isImage || conversation.getMode() == Conversation.MODE_SINGLE) { + View view = createContactView( + conversation.getName(useSubject), + conversation.getLatestMessage().getBody().trim(), + UIHelper.getContactPicture(conversation, 48, + this.getApplicationContext(), false)); + view.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + share(conversation); + } + }); + conversations.addView(view); + displayedContacts.add(conversation.getContact()); } } contacts.removeAllViews(); - final List<Contact> contactsList = new ArrayList<Contact>(); - for(Account account : xmppConnectionService.getAccounts()) { - for(final Contact contact : xmppConnectionService.getRoster(account)) { - if (!displayedContacts.contains(contact.getUuid())) { + List<Contact> contactsList = new ArrayList<Contact>(); + for (Account account : xmppConnectionService.getAccounts()) { + for (Contact contact : account.getRoster().getContacts()) { + if (!displayedContacts.contains(contact) + && (contact.showInRoster())) { contactsList.add(contact); } } } - + Collections.sort(contactsList, new Comparator<Contact>() { @Override public int compare(Contact lhs, Contact rhs) { - return lhs.getDisplayName().compareToIgnoreCase(rhs.getDisplayName()); + return lhs.getDisplayName().compareToIgnoreCase( + rhs.getDisplayName()); } }); - - for(int i = 0; i < contactsList.size(); ++i) { + + for (int i = 0; i < contactsList.size(); ++i) { final Contact con = contactsList.get(i); - View view = createContactView(con.getDisplayName(), con.getJid(), - UIHelper.getContactPicture(con, 48, this.getApplicationContext(), false)); + View view = createContactView( + con.getDisplayName(), + con.getJid(), + UIHelper.getContactPicture(con, 48, + this.getApplicationContext(), false)); view.setOnClickListener(new OnClickListener() { - + @Override public void onClick(View v) { - String sharedText = getIntent().getStringExtra(Intent.EXTRA_TEXT); - Conversation conversation = xmppConnectionService.findOrCreateConversation(con.getAccount(), con.getJid(), false); - switchToConversation(conversation, sharedText); - finish(); + Conversation conversation = xmppConnectionService + .findOrCreateConversation(con.getAccount(), + con.getJid(), false); + share(conversation); } }); contacts.addView(view); } } + + private void share(final Conversation conversation) { + String sharedText = null; + if (isImage) { + final Uri uri = (Uri) getIntent().getParcelableExtra(Intent.EXTRA_STREAM); + selectPresence(conversation, new OnPresenceSelected() { + @Override + public void onPresenceSelected() { + ShareWithActivity.this.xmppConnectionService.attachImageToConversation(conversation, uri,attachImageCallback); + } + }); + + } else { + sharedText = getIntent().getStringExtra( + Intent.EXTRA_TEXT); + } + switchToConversation(conversation, sharedText, true); + finish(); + } } diff --git a/src/eu/siacs/conversations/ui/UiCallback.java b/src/eu/siacs/conversations/ui/UiCallback.java index f9273b96..05a869f7 100644 --- a/src/eu/siacs/conversations/ui/UiCallback.java +++ b/src/eu/siacs/conversations/ui/UiCallback.java @@ -2,8 +2,8 @@ package eu.siacs.conversations.ui; import android.app.PendingIntent; -public interface UiCallback { - public void success(); - public void error(int errorCode); - public void userInputRequried(PendingIntent pi); +public interface UiCallback<T> { + public void success(T object); + public void error(int errorCode, T object); + public void userInputRequried(PendingIntent pi, T object); } diff --git a/src/eu/siacs/conversations/ui/XmppActivity.java b/src/eu/siacs/conversations/ui/XmppActivity.java index dc894ad5..c95cbfec 100644 --- a/src/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/eu/siacs/conversations/ui/XmppActivity.java @@ -1,11 +1,11 @@ package eu.siacs.conversations.ui; -import java.nio.channels.AlreadyConnectedException; - import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.entities.Presences; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder; import eu.siacs.conversations.utils.ExceptionHelper; @@ -29,15 +29,15 @@ import android.view.View; import android.view.inputmethod.InputMethodManager; public abstract class XmppActivity extends Activity { - + public static final int REQUEST_ANNOUNCE_PGP = 0x73731; - + protected final static String LOGTAG = "xmppService"; - + public XmppConnectionService xmppConnectionService; public boolean xmppConnectionServiceBound = false; protected boolean handledViewIntent = false; - + protected ServiceConnection mConnection = new ServiceConnection() { @Override @@ -53,7 +53,7 @@ public abstract class XmppActivity extends Activity { xmppConnectionServiceBound = false; } }; - + @Override protected void onStart() { super.onStart(); @@ -61,14 +61,14 @@ public abstract class XmppActivity extends Activity { connectToBackend(); } } - + public void connectToBackend() { Intent intent = new Intent(this, XmppConnectionService.class); intent.setAction("ui"); startService(intent); bindService(intent, mConnection, Context.BIND_AUTO_CREATE); } - + @Override protected void onStop() { super.onStop(); @@ -77,7 +77,7 @@ public abstract class XmppActivity extends Activity { xmppConnectionServiceBound = false; } } - + protected void hideKeyboard() { InputMethodManager inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); @@ -85,50 +85,52 @@ public abstract class XmppActivity extends Activity { if (focus != null) { - inputManager.hideSoftInputFromWindow( - focus.getWindowToken(), + inputManager.hideSoftInputFromWindow(focus.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); } } - + public boolean hasPgp() { - if (xmppConnectionService.getPgpEngine()!=null) { - return true; - } else { - Builder builder = new AlertDialog.Builder(this); - builder.setTitle(getString(R.string.openkeychain_required)); - builder.setIconAttribute(android.R.attr.alertDialogIcon); - builder.setMessage(getText(R.string.openkeychain_required_long)); - builder.setNegativeButton(getString(R.string.cancel), null); - builder.setNeutralButton(getString(R.string.restart), new OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - if (xmppConnectionServiceBound) { - unbindService(mConnection); - xmppConnectionServiceBound = false; + return xmppConnectionService.getPgpEngine() != null; + } + + public void showInstallPgpDialog() { + Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.openkeychain_required)); + builder.setIconAttribute(android.R.attr.alertDialogIcon); + builder.setMessage(getText(R.string.openkeychain_required_long)); + builder.setNegativeButton(getString(R.string.cancel), null); + builder.setNeutralButton(getString(R.string.restart), + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + if (xmppConnectionServiceBound) { + unbindService(mConnection); + xmppConnectionServiceBound = false; + } + stopService(new Intent(XmppActivity.this, + XmppConnectionService.class)); + finish(); } - stopService(new Intent(XmppActivity.this, XmppConnectionService.class)); - finish(); - } - }); - builder.setPositiveButton(getString(R.string.install), new OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - Uri uri = Uri.parse("market://details?id=org.sufficientlysecure.keychain"); - Intent intent = new Intent(Intent.ACTION_VIEW, uri); - startActivity(intent); - finish(); - } - }); - builder.create().show(); - return false; - } + }); + builder.setPositiveButton(getString(R.string.install), + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + Uri uri = Uri + .parse("market://details?id=org.sufficientlysecure.keychain"); + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + startActivity(intent); + finish(); + } + }); + builder.create().show(); } - + abstract void onBackendConnected(); - + public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.action_settings: @@ -140,62 +142,76 @@ public abstract class XmppActivity extends Activity { } return super.onOptionsItemSelected(item); } - + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ExceptionHelper.init(getApplicationContext()); } - - public void switchToConversation(Conversation conversation, String text) { + + public void switchToConversation(Conversation conversation, String text, + boolean newTask) { Intent viewConversationIntent = new Intent(this, ConversationActivity.class); viewConversationIntent.setAction(Intent.ACTION_VIEW); viewConversationIntent.putExtra(ConversationActivity.CONVERSATION, conversation.getUuid()); - if (text!=null) { + if (text != null) { viewConversationIntent.putExtra(ConversationActivity.TEXT, text); } viewConversationIntent.setType(ConversationActivity.VIEW_CONVERSATION); - viewConversationIntent.setFlags(viewConversationIntent.getFlags() - | Intent.FLAG_ACTIVITY_CLEAR_TOP); + if (newTask) { + viewConversationIntent.setFlags(viewConversationIntent.getFlags() + | Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_SINGLE_TOP); + } else { + viewConversationIntent.setFlags(viewConversationIntent.getFlags() + | Intent.FLAG_ACTIVITY_CLEAR_TOP); + } startActivity(viewConversationIntent); } - - protected void announcePgp(final Account account, final Conversation conversation) { - xmppConnectionService.getPgpEngine().generateSignature(account, "online", new UiCallback() { - - @Override - public void userInputRequried(PendingIntent pi) { - try { - startIntentSenderForResult(pi.getIntentSender(), REQUEST_ANNOUNCE_PGP, null, 0, 0, 0); - } catch (SendIntentException e) { - Log.d("xmppService","coulnd start intent for pgp anncouncment"); - } - } - - @Override - public void success() { - xmppConnectionService.databaseBackend.updateAccount(account); - xmppConnectionService.sendPgpPresence(account, account.getPgpSignature()); - if (conversation!=null) { - conversation.setNextEncryption(Message.ENCRYPTION_PGP); - } - } - - @Override - public void error(int error) { - displayErrorDialog(error); - } - }); + + protected void announcePgp(Account account, final Conversation conversation) { + xmppConnectionService.getPgpEngine().generateSignature(account, + "online", new UiCallback<Account>() { + + @Override + public void userInputRequried(PendingIntent pi, + Account account) { + try { + startIntentSenderForResult(pi.getIntentSender(), + REQUEST_ANNOUNCE_PGP, null, 0, 0, 0); + } catch (SendIntentException e) { + Log.d("xmppService", + "coulnd start intent for pgp anncouncment"); + } + } + + @Override + public void success(Account account) { + xmppConnectionService.databaseBackend + .updateAccount(account); + xmppConnectionService.sendPresence(account); + if (conversation != null) { + conversation + .setNextEncryption(Message.ENCRYPTION_PGP); + } + } + + @Override + public void error(int error, Account account) { + displayErrorDialog(error); + } + }); } - + protected void displayErrorDialog(final int errorCode) { runOnUiThread(new Runnable() { - + @Override public void run() { - AlertDialog.Builder builder = new AlertDialog.Builder(XmppActivity.this); + AlertDialog.Builder builder = new AlertDialog.Builder( + XmppActivity.this); builder.setIconAttribute(android.R.attr.alertDialogIcon); builder.setTitle(getString(R.string.error)); builder.setMessage(errorCode); @@ -203,6 +219,78 @@ public abstract class XmppActivity extends Activity { builder.create().show(); } }); - + + } + + protected void showAddToRosterDialog(final Conversation conversation) { + String jid = conversation.getContactJid(); + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(jid); + builder.setMessage(getString(R.string.not_in_roster)); + builder.setNegativeButton(getString(R.string.cancel), null); + builder.setPositiveButton(getString(R.string.add_contact), + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + String jid = conversation.getContactJid(); + Account account = conversation.getAccount(); + Contact contact = account.getRoster().getContact(jid); + xmppConnectionService.createContact(contact); + } + }); + builder.create().show(); + } + + public void selectPresence(final Conversation conversation, + final OnPresenceSelected listener) { + Contact contact = conversation.getContact(); + if (contact == null) { + showAddToRosterDialog(conversation); + } else { + Presences presences = contact.getPresences(); + if (presences.size() == 0) { + conversation.setNextPresence(null); + listener.onPresenceSelected(); + } else if (presences.size() == 1) { + String presence = (String) presences.asStringArray()[0]; + conversation.setNextPresence(presence); + listener.onPresenceSelected(); + } else { + final StringBuilder presence = new StringBuilder(); + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.choose_presence)); + final String[] presencesArray = presences.asStringArray(); + int preselectedPresence = 0; + for (int i = 0; i < presencesArray.length; ++i) { + if (presencesArray[i].equals(contact.lastseen.presence)) { + preselectedPresence = i; + break; + } + } + presence.append(presencesArray[preselectedPresence]); + builder.setSingleChoiceItems(presencesArray, + preselectedPresence, + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, + int which) { + presence.delete(0, presence.length()); + presence.append(presencesArray[which]); + } + }); + builder.setNegativeButton(R.string.cancel, null); + builder.setPositiveButton(R.string.ok, new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + conversation.setNextPresence(presence.toString()); + listener.onPresenceSelected(); + } + }); + builder.create().show(); + } + } } } diff --git a/src/eu/siacs/conversations/utils/CryptoHelper.java b/src/eu/siacs/conversations/utils/CryptoHelper.java index 98dbea4d..a70b419e 100644 --- a/src/eu/siacs/conversations/utils/CryptoHelper.java +++ b/src/eu/siacs/conversations/utils/CryptoHelper.java @@ -5,15 +5,14 @@ import java.nio.charset.Charset; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; -import java.util.Random; +import java.util.Arrays; import eu.siacs.conversations.entities.Account; - import android.util.Base64; -import android.util.Log; public class CryptoHelper { - final protected static char[] hexArray = "0123456789ABCDEF".toCharArray(); + 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(); @@ -25,7 +24,15 @@ public class CryptoHelper { hexChars[j * 2] = hexArray[v >>> 4]; hexChars[j * 2 + 1] = hexArray[v & 0x0F]; } - return new String(hexChars).toLowerCase(); + return new String(hexChars); + } + + public static byte[] hexToBytes(String hexString) { + byte[] array = new BigInteger(hexString, 16).toByteArray(); + if (array[0] == 0) { + array = Arrays.copyOfRange(array, 1, array.length); + } + return array; } public static String saslPlain(String username, String password) { @@ -41,9 +48,8 @@ public class CryptoHelper { return result; } - public static String saslDigestMd5(Account account, String challenge) { + public static String saslDigestMd5(Account account, String challenge, SecureRandom random) { try { - Random random = new SecureRandom(); String[] challengeParts = new String(Base64.decode(challenge, Base64.DEFAULT)).split(","); String nonce = ""; @@ -85,12 +91,11 @@ public class CryptoHelper { } } - public static String randomMucName() { - Random random = new SecureRandom(); + public static String randomMucName(SecureRandom random) { return randomWord(3, random) + "." + randomWord(7, random); } - protected static String randomWord(int lenght, Random random) { + protected static String randomWord(int lenght, SecureRandom random) { StringBuilder builder = new StringBuilder(lenght); for (int i = 0; i < lenght; ++i) { if (i % 2 == 0) { diff --git a/src/eu/siacs/conversations/utils/DNSHelper.java b/src/eu/siacs/conversations/utils/DNSHelper.java index 94dd3b51..002e124f 100644 --- a/src/eu/siacs/conversations/utils/DNSHelper.java +++ b/src/eu/siacs/conversations/utils/DNSHelper.java @@ -52,7 +52,7 @@ public class DNSHelper { DNSMessage message = client.query( qname, - TYPE.ANY, + TYPE.SRV, CLASS.IN, dnsServer.getHostAddress()); diff --git a/src/eu/siacs/conversations/utils/ExceptionHelper.java b/src/eu/siacs/conversations/utils/ExceptionHelper.java index f9cd375e..05b16240 100644 --- a/src/eu/siacs/conversations/utils/ExceptionHelper.java +++ b/src/eu/siacs/conversations/utils/ExceptionHelper.java @@ -70,7 +70,7 @@ public class ExceptionHelper { Log.d("xmppService","using account="+finalAccount.getJid()+" to send in stack trace"); Conversation conversation = service.findOrCreateConversation(finalAccount, "bugs@siacs.eu", false); Message message = new Message(conversation, stacktrace.toString(), Message.ENCRYPTION_NONE); - service.sendMessage(message, null); + service.sendMessage(message); } }); builder.setNegativeButton(context.getText(R.string.send_never),new OnClickListener() { diff --git a/src/eu/siacs/conversations/utils/MessageParser.java b/src/eu/siacs/conversations/utils/MessageParser.java deleted file mode 100644 index 52b18f66..00000000 --- a/src/eu/siacs/conversations/utils/MessageParser.java +++ /dev/null @@ -1,162 +0,0 @@ -package eu.siacs.conversations.utils; - -import java.util.List; - -import net.java.otr4j.session.Session; -import net.java.otr4j.session.SessionStatus; -import android.util.Log; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xmpp.stanzas.MessagePacket; - -public class MessageParser { - - protected static final String LOGTAG = "xmppService"; - - public static Message parsePlainTextChat(MessagePacket packet, Account account, XmppConnectionService service) { - String[] fromParts = packet.getFrom().split("/"); - Conversation conversation = service.findOrCreateConversation(account, fromParts[0],false); - String body = packet.getBody(); - return new Message(conversation, packet.getFrom(), body, Message.ENCRYPTION_NONE, Message.STATUS_RECIEVED); - } - - public static Message parsePgpChat(String pgpBody, MessagePacket packet, Account account, XmppConnectionService service) { - String[] fromParts = packet.getFrom().split("/"); - Conversation conversation = service.findOrCreateConversation(account, fromParts[0],false); - return new Message(conversation, packet.getFrom(), pgpBody, Message.ENCRYPTION_PGP, Message.STATUS_RECIEVED); - } - - public static Message parseOtrChat(MessagePacket packet, Account account, XmppConnectionService service) { - boolean properlyAddressed = (packet.getTo().split("/").length == 2) || (account.countPresences() == 1); - String[] fromParts = packet.getFrom().split("/"); - Conversation conversation = service.findOrCreateConversation(account, fromParts[0],false); - String body = packet.getBody(); - if (!conversation.hasValidOtrSession()) { - if (properlyAddressed) { - Log.d("xmppService","starting new otr session with "+packet.getFrom()+" because no valid otr session has been found"); - conversation.startOtrSession(service.getApplicationContext(), fromParts[1],false); - } else { - Log.d("xmppService",account.getJid()+": ignoring otr session with "+fromParts[0]); - return null; - } - } else { - String foreignPresence = conversation.getOtrSession().getSessionID().getUserID(); - if (!foreignPresence.equals(fromParts[1])) { - conversation.resetOtrSession(); - if (properlyAddressed) { - Log.d("xmppService","replacing otr session with "+packet.getFrom()); - conversation.startOtrSession(service.getApplicationContext(), fromParts[1],false); - } else { - return null; - } - } - } - try { - Session otrSession = conversation.getOtrSession(); - SessionStatus before = otrSession - .getSessionStatus(); - body = otrSession.transformReceiving(body); - SessionStatus after = otrSession.getSessionStatus(); - if ((before != after) - && (after == SessionStatus.ENCRYPTED)) { - Log.d(LOGTAG, "otr session etablished"); - List<Message> messages = conversation - .getMessages(); - for (int i = 0; i < messages.size(); ++i) { - Message msg = messages.get(i); - if ((msg.getStatus() == Message.STATUS_UNSEND) - && (msg.getEncryption() == Message.ENCRYPTION_OTR)) { - MessagePacket outPacket = service.prepareMessagePacket( - account, msg, otrSession); - msg.setStatus(Message.STATUS_SEND); - service.databaseBackend.updateMessage(msg); - account.getXmppConnection() - .sendMessagePacket(outPacket); - } - } - service.updateUi(conversation, false); - } else if ((before != after) && (after == SessionStatus.FINISHED)) { - conversation.resetOtrSession(); - Log.d(LOGTAG,"otr session stoped"); - } - //isEmpty is a work around for some weird clients which send emtpty strings over otr - if ((body == null)||(body.isEmpty())) { - return null; - } - return new Message(conversation, packet.getFrom(), body, Message.ENCRYPTION_OTR,Message.STATUS_RECIEVED); - } catch (Exception e) { - conversation.resetOtrSession(); - return null; - } - } - - public static Message parseGroupchat(MessagePacket packet, Account account, XmppConnectionService service) { - int status; - String[] fromParts = packet.getFrom().split("/"); - Conversation conversation = service.findOrCreateConversation(account, fromParts[0],true); - if (packet.hasChild("subject")) { - conversation.getMucOptions().setSubject(packet.findChild("subject").getContent()); - service.updateUi(conversation, false); - return null; - } - if ((fromParts.length == 1)) { - return null; - } - String counterPart = fromParts[1]; - if (counterPart.equals(conversation.getMucOptions().getNick())) { - status = Message.STATUS_SEND; - } else { - status = Message.STATUS_RECIEVED; - } - return new Message(conversation, counterPart, packet.getBody(), Message.ENCRYPTION_NONE, status); - } - - public static Message parseCarbonMessage(MessagePacket packet, - Account account, XmppConnectionService service) { - int status; - String fullJid; - Element forwarded; - if (packet.hasChild("received")) { - forwarded = packet.findChild("received").findChild( - "forwarded"); - status = Message.STATUS_RECIEVED; - } else if (packet.hasChild("sent")) { - forwarded = packet.findChild("sent").findChild( - "forwarded"); - status = Message.STATUS_SEND; - } else { - return null; - } - if (forwarded==null) { - return null; - } - Element message = forwarded.findChild("message"); - if ((message == null) || (!message.hasChild("body"))) - return null; // either malformed or boring - if (status == Message.STATUS_RECIEVED) { - fullJid = message.getAttribute("from"); - } else { - fullJid = message.getAttribute("to"); - } - String[] parts = fullJid.split("/"); - Conversation conversation = service.findOrCreateConversation(account, parts[0],false); - return new Message(conversation,fullJid, message.findChild("body").getContent(), Message.ENCRYPTION_NONE,status); - } - - public static void parseError(MessagePacket packet, Account account, XmppConnectionService service) { - String[] fromParts = packet.getFrom().split("/"); - service.markMessage(account, fromParts[0], packet.getId(), Message.STATUS_SEND_FAILED); - } - - public static String getPgpBody(MessagePacket packet) { - for(Element child : packet.getChildren()) { - if (child.getName().equals("x")&&child.getAttribute("xmlns").equals("jabber:x:encrypted")) { - return child.getContent(); - } - } - return null; - } -} diff --git a/src/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java b/src/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java index fa8cea04..9a689768 100644 --- a/src/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java +++ b/src/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java @@ -1,9 +1,9 @@ package eu.siacs.conversations.utils; -import java.util.Hashtable; +import java.util.List; import android.os.Bundle; public interface OnPhoneContactsLoadedListener { - public void onPhoneContactsLoaded(Hashtable<String, Bundle> phoneContacts); + public void onPhoneContactsLoaded(List<Bundle> phoneContacts); } diff --git a/src/eu/siacs/conversations/utils/PRNGFixes.java b/src/eu/siacs/conversations/utils/PRNGFixes.java new file mode 100644 index 00000000..cf28ff30 --- /dev/null +++ b/src/eu/siacs/conversations/utils/PRNGFixes.java @@ -0,0 +1,326 @@ +package eu.siacs.conversations.utils; + +import android.os.Build; +import android.os.Process; +import android.util.Log; + +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.security.NoSuchAlgorithmException; +import java.security.Provider; +import java.security.SecureRandom; +import java.security.SecureRandomSpi; +import java.security.Security; + +/** + * Fixes for the output of the default PRNG having low entropy. + * + * The fixes need to be applied via {@link #apply()} before any use of Java + * Cryptography Architecture primitives. A good place to invoke them is in the + * application's {@code onCreate}. + */ +public final class PRNGFixes { + + private static final int VERSION_CODE_JELLY_BEAN = 16; + private static final int VERSION_CODE_JELLY_BEAN_MR2 = 18; + private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL = + getBuildFingerprintAndDeviceSerial(); + + /** Hidden constructor to prevent instantiation. */ + private PRNGFixes() {} + + /** + * Applies all fixes. + * + * @throws SecurityException if a fix is needed but could not be applied. + */ + public static void apply() { + applyOpenSSLFix(); + installLinuxPRNGSecureRandom(); + } + + /** + * Applies the fix for OpenSSL PRNG having low entropy. Does nothing if the + * fix is not needed. + * + * @throws SecurityException if the fix is needed but could not be applied. + */ + private static void applyOpenSSLFix() throws SecurityException { + if ((Build.VERSION.SDK_INT < VERSION_CODE_JELLY_BEAN) + || (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2)) { + // No need to apply the fix + return; + } + + try { + // Mix in the device- and invocation-specific seed. + Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto") + .getMethod("RAND_seed", byte[].class) + .invoke(null, generateSeed()); + + // Mix output of Linux PRNG into OpenSSL's PRNG + int bytesRead = (Integer) Class.forName( + "org.apache.harmony.xnet.provider.jsse.NativeCrypto") + .getMethod("RAND_load_file", String.class, long.class) + .invoke(null, "/dev/urandom", 1024); + if (bytesRead != 1024) { + throw new IOException( + "Unexpected number of bytes read from Linux PRNG: " + + bytesRead); + } + } catch (Exception e) { + throw new SecurityException("Failed to seed OpenSSL PRNG", e); + } + } + + /** + * Installs a Linux PRNG-backed {@code SecureRandom} implementation as the + * default. Does nothing if the implementation is already the default or if + * there is not need to install the implementation. + * + * @throws SecurityException if the fix is needed but could not be applied. + */ + private static void installLinuxPRNGSecureRandom() + throws SecurityException { + if (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2) { + // No need to apply the fix + return; + } + + // Install a Linux PRNG-based SecureRandom implementation as the + // default, if not yet installed. + Provider[] secureRandomProviders = + Security.getProviders("SecureRandom.SHA1PRNG"); + if ((secureRandomProviders == null) + || (secureRandomProviders.length < 1) + || (!LinuxPRNGSecureRandomProvider.class.equals( + secureRandomProviders[0].getClass()))) { + Security.insertProviderAt(new LinuxPRNGSecureRandomProvider(), 1); + } + + // Assert that new SecureRandom() and + // SecureRandom.getInstance("SHA1PRNG") return a SecureRandom backed + // by the Linux PRNG-based SecureRandom implementation. + SecureRandom rng1 = new SecureRandom(); + if (!LinuxPRNGSecureRandomProvider.class.equals( + rng1.getProvider().getClass())) { + throw new SecurityException( + "new SecureRandom() backed by wrong Provider: " + + rng1.getProvider().getClass()); + } + + SecureRandom rng2; + try { + rng2 = SecureRandom.getInstance("SHA1PRNG"); + } catch (NoSuchAlgorithmException e) { + throw new SecurityException("SHA1PRNG not available", e); + } + if (!LinuxPRNGSecureRandomProvider.class.equals( + rng2.getProvider().getClass())) { + throw new SecurityException( + "SecureRandom.getInstance(\"SHA1PRNG\") backed by wrong" + + " Provider: " + rng2.getProvider().getClass()); + } + } + + /** + * {@code Provider} of {@code SecureRandom} engines which pass through + * all requests to the Linux PRNG. + */ + private static class LinuxPRNGSecureRandomProvider extends Provider { + + public LinuxPRNGSecureRandomProvider() { + super("LinuxPRNG", + 1.0, + "A Linux-specific random number provider that uses" + + " /dev/urandom"); + // Although /dev/urandom is not a SHA-1 PRNG, some apps + // explicitly request a SHA1PRNG SecureRandom and we thus need to + // prevent them from getting the default implementation whose output + // may have low entropy. + put("SecureRandom.SHA1PRNG", LinuxPRNGSecureRandom.class.getName()); + put("SecureRandom.SHA1PRNG ImplementedIn", "Software"); + } + } + + /** + * {@link SecureRandomSpi} which passes all requests to the Linux PRNG + * ({@code /dev/urandom}). + */ + public static class LinuxPRNGSecureRandom extends SecureRandomSpi { + + /* + * IMPLEMENTATION NOTE: Requests to generate bytes and to mix in a seed + * are passed through to the Linux PRNG (/dev/urandom). Instances of + * this class seed themselves by mixing in the current time, PID, UID, + * build fingerprint, and hardware serial number (where available) into + * Linux PRNG. + * + * Concurrency: Read requests to the underlying Linux PRNG are + * serialized (on sLock) to ensure that multiple threads do not get + * duplicated PRNG output. + */ + + private static final File URANDOM_FILE = new File("/dev/urandom"); + + private static final Object sLock = new Object(); + + /** + * Input stream for reading from Linux PRNG or {@code null} if not yet + * opened. + * + * @GuardedBy("sLock") + */ + private static DataInputStream sUrandomIn; + + /** + * Output stream for writing to Linux PRNG or {@code null} if not yet + * opened. + * + * @GuardedBy("sLock") + */ + private static OutputStream sUrandomOut; + + /** + * Whether this engine instance has been seeded. This is needed because + * each instance needs to seed itself if the client does not explicitly + * seed it. + */ + private boolean mSeeded; + + @Override + protected void engineSetSeed(byte[] bytes) { + try { + OutputStream out; + synchronized (sLock) { + out = getUrandomOutputStream(); + } + out.write(bytes); + out.flush(); + } catch (IOException e) { + // On a small fraction of devices /dev/urandom is not writable. + // Log and ignore. + Log.w(PRNGFixes.class.getSimpleName(), + "Failed to mix seed into " + URANDOM_FILE); + } finally { + mSeeded = true; + } + } + + @Override + protected void engineNextBytes(byte[] bytes) { + if (!mSeeded) { + // Mix in the device- and invocation-specific seed. + engineSetSeed(generateSeed()); + } + + try { + DataInputStream in; + synchronized (sLock) { + in = getUrandomInputStream(); + } + synchronized (in) { + in.readFully(bytes); + } + } catch (IOException e) { + throw new SecurityException( + "Failed to read from " + URANDOM_FILE, e); + } + } + + @Override + protected byte[] engineGenerateSeed(int size) { + byte[] seed = new byte[size]; + engineNextBytes(seed); + return seed; + } + + private DataInputStream getUrandomInputStream() { + synchronized (sLock) { + if (sUrandomIn == null) { + // NOTE: Consider inserting a BufferedInputStream between + // DataInputStream and FileInputStream if you need higher + // PRNG output performance and can live with future PRNG + // output being pulled into this process prematurely. + try { + sUrandomIn = new DataInputStream( + new FileInputStream(URANDOM_FILE)); + } catch (IOException e) { + throw new SecurityException("Failed to open " + + URANDOM_FILE + " for reading", e); + } + } + return sUrandomIn; + } + } + + private OutputStream getUrandomOutputStream() throws IOException { + synchronized (sLock) { + if (sUrandomOut == null) { + sUrandomOut = new FileOutputStream(URANDOM_FILE); + } + return sUrandomOut; + } + } + } + + /** + * Generates a device- and invocation-specific seed to be mixed into the + * Linux PRNG. + */ + private static byte[] generateSeed() { + try { + ByteArrayOutputStream seedBuffer = new ByteArrayOutputStream(); + DataOutputStream seedBufferOut = + new DataOutputStream(seedBuffer); + seedBufferOut.writeLong(System.currentTimeMillis()); + seedBufferOut.writeLong(System.nanoTime()); + seedBufferOut.writeInt(Process.myPid()); + seedBufferOut.writeInt(Process.myUid()); + seedBufferOut.write(BUILD_FINGERPRINT_AND_DEVICE_SERIAL); + seedBufferOut.close(); + return seedBuffer.toByteArray(); + } catch (IOException e) { + throw new SecurityException("Failed to generate seed", e); + } + } + + /** + * Gets the hardware serial number of this device. + * + * @return serial number or {@code null} if not available. + */ + private static String getDeviceSerialNumber() { + // We're using the Reflection API because Build.SERIAL is only available + // since API Level 9 (Gingerbread, Android 2.3). + try { + return (String) Build.class.getField("SERIAL").get(null); + } catch (Exception ignored) { + return null; + } + } + + private static byte[] getBuildFingerprintAndDeviceSerial() { + StringBuilder result = new StringBuilder(); + String fingerprint = Build.FINGERPRINT; + if (fingerprint != null) { + result.append(fingerprint); + } + String serial = getDeviceSerialNumber(); + if (serial != null) { + result.append(serial); + } + try { + return result.toString().getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("UTF-8 encoding not supported"); + } + } +}
\ No newline at end of file diff --git a/src/eu/siacs/conversations/utils/PhoneHelper.java b/src/eu/siacs/conversations/utils/PhoneHelper.java index 6355e378..773312bb 100644 --- a/src/eu/siacs/conversations/utils/PhoneHelper.java +++ b/src/eu/siacs/conversations/utils/PhoneHelper.java @@ -1,8 +1,8 @@ package eu.siacs.conversations.utils; -import java.util.Hashtable; +import java.util.ArrayList; +import java.util.List; -import android.app.Activity; import android.content.Context; import android.content.CursorLoader; import android.content.Loader; @@ -10,21 +10,15 @@ import android.content.Loader.OnLoadCompleteListener; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; -import android.os.Looper; import android.provider.ContactsContract; import android.provider.ContactsContract.Profile; -import android.provider.MediaStore; public class PhoneHelper { public static void loadPhoneContacts(Context context, final OnPhoneContactsLoadedListener listener) { - if (Looper.myLooper() == null) { - Looper.prepare(); - } - final Looper mLooper = Looper.myLooper(); - final Hashtable<String, Bundle> phoneContacts = new Hashtable<String, Bundle>(); - + final List<Bundle> phoneContacts = new ArrayList<Bundle>(); + final String[] PROJECTION = new String[] { ContactsContract.Data._ID, ContactsContract.Data.DISPLAY_NAME, ContactsContract.Data.PHOTO_THUMBNAIL_URI, @@ -58,15 +52,14 @@ public class PhoneHelper { .getColumnIndex(ContactsContract.Data.PHOTO_THUMBNAIL_URI))); contact.putString("lookup", cursor.getString(cursor .getColumnIndex(ContactsContract.Data.LOOKUP_KEY))); - phoneContacts.put( - cursor.getString(cursor - .getColumnIndex(ContactsContract.CommonDataKinds.Im.DATA)), - contact); + + contact.putString("jid",cursor.getString(cursor + .getColumnIndex(ContactsContract.CommonDataKinds.Im.DATA))); + phoneContacts.add(contact); } if (listener != null) { listener.onPhoneContactsLoaded(phoneContacts); } - mLooper.quit(); } }); mCursorLoader.startLoading(); diff --git a/src/eu/siacs/conversations/utils/UIHelper.java b/src/eu/siacs/conversations/utils/UIHelper.java index 8baa3c25..183d94f3 100644 --- a/src/eu/siacs/conversations/utils/UIHelper.java +++ b/src/eu/siacs/conversations/utils/UIHelper.java @@ -1,18 +1,19 @@ package eu.siacs.conversations.utils; import java.io.FileNotFoundException; -import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Calendar; import java.util.Date; import java.util.List; import java.util.Locale; +import java.util.regex.Pattern; +import java.util.regex.Matcher; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.MucOptions.User; import eu.siacs.conversations.ui.ConversationActivity; import eu.siacs.conversations.ui.ManageAccountActivity; @@ -27,7 +28,6 @@ import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.content.Intent; import android.content.SharedPreferences; -import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; @@ -38,11 +38,11 @@ import android.net.Uri; import android.preference.PreferenceManager; import android.provider.ContactsContract.Contacts; import android.support.v4.app.NotificationCompat; -import android.support.v4.app.NotificationCompat.InboxStyle; import android.support.v4.app.TaskStackBuilder; +import android.text.format.DateFormat; +import android.text.format.DateUtils; import android.text.Html; import android.util.DisplayMetrics; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.widget.LinearLayout; @@ -53,23 +53,56 @@ public class UIHelper { private static final int BG_COLOR = 0xFF181818; private static final int FG_COLOR = 0xFFE5E5E5; private static final int TRANSPARENT = 0x00000000; + private static final int DATE_NO_YEAR_FLAGS = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_YEAR | DateUtils.FORMAT_ABBREV_ALL; - public static String readableTimeDifference(long time) { + public static String readableTimeDifference(Context context, long time) { if (time == 0) { - return "just now"; + return context.getString(R.string.just_now); } Date date = new Date(time); long difference = (System.currentTimeMillis() - time) / 1000; if (difference < 60) { - return "just now"; - } else if (difference < 60 * 10) { - return difference / 60 + " min ago"; - } else if (difference < 60 * 60 * 24) { - SimpleDateFormat sdf = new SimpleDateFormat("HH:mm",Locale.US); - return sdf.format(date); + return context.getString(R.string.just_now); + } else if (difference < 60 * 2) { + return context.getString(R.string.minute_ago); + } else if (difference < 60 * 15) { + return context.getString(R.string.minutes_ago,Math.round(difference/60.0)); + } else if (today(date)) { + java.text.DateFormat df = DateFormat.getTimeFormat(context); + return df.format(date); } else { - SimpleDateFormat sdf = new SimpleDateFormat("MM/dd",Locale.US); - return sdf.format(date); + return DateUtils.formatDateTime(context, date.getTime(), DATE_NO_YEAR_FLAGS); + } + } + + private static boolean today(Date date) { + Calendar cal1 = Calendar.getInstance(); + Calendar cal2 = Calendar.getInstance(); + cal1.setTime(date); + cal2.setTimeInMillis(System.currentTimeMillis()); + return cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) && + cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR); + } + + public static String lastseen(Context context, long time) { + if (time==0) { + return context.getString(R.string.never_seen); + } + long difference = (System.currentTimeMillis() - time) / 1000; + if (difference < 60) { + return context.getString(R.string.last_seen_now); + } else if (difference < 60 * 2) { + return context.getString(R.string.last_seen_min); + } else if (difference < 60 * 60) { + return context.getString(R.string.last_seen_mins,Math.round(difference/60.0)); + } else if (difference < 60 * 60 * 2) { + return context.getString(R.string.last_seen_hour); + } else if (difference < 60 * 60 * 24) { + return context.getString(R.string.last_seen_hours,Math.round(difference/(60.0*60.0))); + } else if (difference < 60 * 60 * 48) { + return context.getString(R.string.last_seen_day); + } else { + return context.getString(R.string.last_seen_days,Math.round(difference/(60.0*60.0*24.0))); } } @@ -81,8 +114,24 @@ public class UIHelper { private static int getNameColor(String name) { int holoColors[] = { 0xFF1da9da, 0xFFb368d9, 0xFF83b600, 0xFFffa713, 0xFFe92727 }; - int color = holoColors[Math.abs(name.toLowerCase(Locale.getDefault()).hashCode()) % holoColors.length]; - return color; + return holoColors[Math.abs(name.toLowerCase(Locale.getDefault()).hashCode()) % holoColors.length]; + } + + private static void drawTile(Canvas canvas, String letter, int tileColor, int textColor, int left, int top, int right, int bottom) { + Paint tilePaint = new Paint(), textPaint = new Paint(); + tilePaint.setColor(tileColor); + textPaint.setColor(textColor); + textPaint.setTypeface(Typeface.create("sans-serif-light", Typeface.NORMAL)); + textPaint.setTextSize((float) ((right - left) * 0.8)); + Rect rect = new Rect(); + + canvas.drawRect(new Rect(left, top, right, bottom), tilePaint); + textPaint.getTextBounds(letter, 0, 1, rect); + float width = textPaint.measureText(letter); + canvas.drawText(letter, + (right+left)/2 - width/2, + (top+bottom)/2 + rect.height()/2, + textPaint); } private static Bitmap getUnknownContactPicture(String[] names, int size, int bgColor, int fgColor) { @@ -110,135 +159,46 @@ public class UIHelper { colors[3] = 0xFF444444; } } - Paint textPaint = new Paint(), tilePaint = new Paint(); - textPaint.setColor(fgColor); - textPaint.setTypeface(Typeface.create("sans-serif-light", Typeface.NORMAL)); - Rect rect, left, right, topLeft, bottomLeft, topRight, bottomRight; - float width; + + bitmap.eraseColor(bgColor); switch(tiles) { case 1: - bitmap.eraseColor(colors[0]); - - textPaint.setTextSize((float) (size * 0.8)); - textPaint.setAntiAlias(true); - rect = new Rect(); - textPaint.getTextBounds(letters[0], 0, 1, rect); - width = textPaint.measureText(letters[0]); - canvas.drawText(letters[0], (size / 2) - (width / 2), (size / 2) - + (rect.height() / 2), textPaint); + drawTile(canvas, letters[0], colors[0], fgColor, + 0, 0, size, size); break; case 2: - bitmap.eraseColor(bgColor); - - tilePaint.setColor(colors[0]); - left = new Rect(0, 0, (size/2)-1, size); - canvas.drawRect(left, tilePaint); - - tilePaint.setColor(colors[1]); - right = new Rect((size/2)+1, 0, size, size); - canvas.drawRect(right, tilePaint); - - textPaint.setTextSize((float) (size * 0.8*0.5)); - textPaint.setAntiAlias(true); - rect = new Rect(); - textPaint.getTextBounds(letters[0], 0, 1, rect); - width = textPaint.measureText(letters[0]); - canvas.drawText(letters[0], (size / 4) - (width / 2), (size / 2) - + (rect.height() / 2), textPaint); - textPaint.getTextBounds(letters[1], 0, 1, rect); - width = textPaint.measureText(letters[1]); - canvas.drawText(letters[1], (3 * size / 4) - (width / 2), (size / 2) - + (rect.height() / 2), textPaint); + drawTile(canvas, letters[0], colors[0], fgColor, + 0, 0, size/2 - 1, size); + drawTile(canvas, letters[1], colors[1], fgColor, + size/2 + 1, 0, size, size); break; case 3: - bitmap.eraseColor(bgColor); - - tilePaint.setColor(colors[0]); - left = new Rect(0, 0, (size/2)-1, size); - canvas.drawRect(left, tilePaint); - - tilePaint.setColor(colors[1]); - topRight = new Rect((size/2)+1, 0, size, (size/2 - 1)); - canvas.drawRect(topRight, tilePaint); - - tilePaint.setColor(colors[2]); - bottomRight = new Rect((size/2)+1, (size/2 + 1), size, size); - canvas.drawRect(bottomRight, tilePaint); - - textPaint.setTextSize((float) (size * 0.8*0.5)); - textPaint.setAntiAlias(true); - rect = new Rect(); - - textPaint.getTextBounds(letters[0], 0, 1, rect); - width = textPaint.measureText(letters[0]); - canvas.drawText(letters[0], (size / 4) - (width / 2), (size / 2) - + (rect.height() / 2), textPaint); - - textPaint.getTextBounds(letters[1], 0, 1, rect); - width = textPaint.measureText(letters[1]); - canvas.drawText(letters[1], (3 * size / 4) - (width / 2), (size / 4) - + (rect.height() / 2), textPaint); - - textPaint.getTextBounds(letters[2], 0, 1, rect); - width = textPaint.measureText(letters[2]); - canvas.drawText(letters[2], (3 * size / 4) - (width / 2), (3* size / 4) - + (rect.height() / 2), textPaint); + drawTile(canvas, letters[0], colors[0], fgColor, + 0, 0, size/2 - 1, size); + drawTile(canvas, letters[1], colors[1], fgColor, + size/2 + 1, 0, size, size/2 - 1); + drawTile(canvas, letters[2], colors[2], fgColor, + size/2 + 1, size/2 + 1, size, size); break; case 4: - bitmap.eraseColor(bgColor); - - tilePaint.setColor(colors[0]); - topLeft = new Rect(0, 0, (size/2)-1, (size/2)-1); - canvas.drawRect(topLeft, tilePaint); - - tilePaint.setColor(colors[1]); - bottomLeft = new Rect(0, (size/2)+1, (size/2)-1, size); - canvas.drawRect(bottomLeft, tilePaint); - - tilePaint.setColor(colors[2]); - topRight = new Rect((size/2)+1, 0, size, (size/2 - 1)); - canvas.drawRect(topRight, tilePaint); - - tilePaint.setColor(colors[3]); - bottomRight = new Rect((size/2)+1, (size/2 + 1), size, size); - canvas.drawRect(bottomRight, tilePaint); - - textPaint.setTextSize((float) (size * 0.8*0.5)); - textPaint.setAntiAlias(true); - rect = new Rect(); - - textPaint.getTextBounds(letters[0], 0, 1, rect); - width = textPaint.measureText(letters[0]); - canvas.drawText(letters[0], (size / 4) - (width / 2), (size / 4) - + (rect.height() / 2), textPaint); - - textPaint.getTextBounds(letters[1], 0, 1, rect); - width = textPaint.measureText(letters[1]); - canvas.drawText(letters[1], (size / 4) - (width / 2), (3* size / 4) - + (rect.height() / 2), textPaint); - - textPaint.getTextBounds(letters[2], 0, 1, rect); - width = textPaint.measureText(letters[2]); - canvas.drawText(letters[2], (3 * size / 4) - (width / 2), (size / 4) - + (rect.height() / 2), textPaint); - - textPaint.getTextBounds(letters[3], 0, 1, rect); - width = textPaint.measureText(letters[3]); - canvas.drawText(letters[3], (3 * size / 4) - (width / 2), (3* size / 4) - + (rect.height() / 2), textPaint); + drawTile(canvas, letters[0], colors[0], fgColor, + 0, 0, size/2 - 1, size/2 - 1); + drawTile(canvas, letters[1], colors[1], fgColor, + 0, size/2 + 1, size/2 - 1, size); + drawTile(canvas, letters[2], colors[2], fgColor, + size/2 + 1, 0, size, size/2 - 1); + drawTile(canvas, letters[3], colors[3], fgColor, + size/2 + 1, size/2 + 1, size, size); break; } - return bitmap; - } - private static Bitmap getUnknownContactPicture(String[] names, int size) { - return getUnknownContactPicture(names, size, UIHelper.BG_COLOR, UIHelper.FG_COLOR); + return bitmap; } - + private static Bitmap getMucContactPicture(Conversation conversation, int size, int bgColor, int fgColor) { List<User> members = conversation.getMucOptions().getUsers(); if (members.size() == 0) { @@ -255,13 +215,8 @@ public class UIHelper { public static Bitmap getContactPicture(Conversation conversation, int dpSize, Context context, boolean notification) { if(conversation.getMode() == Conversation.MODE_SINGLE) { - if (conversation.getContact() != null){ return getContactPicture(conversation.getContact(), dpSize, context, notification); - } else { - return getContactPicture(conversation.getName(false), dpSize, - context, notification); - } } else{ int fgColor = UIHelper.FG_COLOR, bgColor = (notification) ? @@ -273,10 +228,6 @@ public class UIHelper { } public static Bitmap getContactPicture(Contact contact, int dpSize, Context context, boolean notification) { - int fgColor = UIHelper.FG_COLOR, - bgColor = (notification) ? - UIHelper.BG_COLOR : UIHelper.TRANSPARENT; - String uri = contact.getProfilePhoto(); if (uri==null) { return getContactPicture(contact.getDisplayName(), dpSize, @@ -340,6 +291,15 @@ public class UIHelper { mNotificationManager.notify(1111, notification); } + private static Pattern generateNickHighlightPattern(String nick) { + // We expect a word boundary, i.e. space or start of string, followed by the + // nick (matched in case-insensitive manner), followed by optional + // punctuation (for example "bob: i disagree" or "how are you alice?"), + // followed by another word boundary. + return Pattern.compile("\\b"+nick+"\\p{Punct}?\\b", + Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE); + } + public static void updateNotification(Context context, List<Conversation> conversations, Conversation currentCon, boolean notify) { NotificationManager mNotificationManager = (NotificationManager) context @@ -360,7 +320,9 @@ public class UIHelper { if ((currentCon != null) &&(currentCon.getMode() == Conversation.MODE_MULTI)&&(!alwaysNotify)) { String nick = currentCon.getMucOptions().getNick(); - notify = currentCon.getLatestMessage().getBody().contains(nick); + Pattern highlight = generateNickHighlightPattern(nick); + Matcher m = highlight.matcher(currentCon.getLatestMessage().getBody()); + notify = m.find(); } List<Conversation> unread = new ArrayList<Conversation>(); @@ -412,7 +374,7 @@ public class UIHelper { .bigText(bigText.toString())); } else { NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle(); - style.setBigContentTitle(unread.size() + " unread Conversations"); + style.setBigContentTitle(unread.size() + " " + context.getString(R.string.unread_conversations)); StringBuilder names = new StringBuilder(); for (int i = 0; i < unread.size(); ++i) { targetUuid = unread.get(i).getUuid(); @@ -424,7 +386,7 @@ public class UIHelper { style.addLine(Html.fromHtml("<b>" + unread.get(i).getName(useSubject) + "</b> " + unread.get(i).getLatestMessage().getReadableBody(context))); } - mBuilder.setContentTitle(unread.size() + " unread Conversations"); + mBuilder.setContentTitle(unread.size() + " " + context.getString(R.string.unread_conversations)); mBuilder.setContentText(names.toString()); mBuilder.setStyle(style); } @@ -470,11 +432,13 @@ public class UIHelper { private static boolean wasHighlighted(Conversation conversation) { List<Message> messages = conversation.getMessages(); String nick = conversation.getMucOptions().getNick(); + Pattern highlight = generateNickHighlightPattern(nick); for(int i = messages.size() - 1; i >= 0; --i) { if (messages.get(i).isRead()) { break; } else { - if (messages.get(i).getBody().contains(nick)) { + Matcher m = highlight.matcher(messages.get(i).getBody()); + if (m.find()) { return true; } } @@ -518,7 +482,7 @@ public class UIHelper { public void onClick(DialogInterface dialog, int which) { contact.addOtrFingerprint(conversation.getOtrFingerprint()); msg.setVisibility(View.GONE); - activity.xmppConnectionService.updateContact(contact); + //activity.xmppConnectionService.updateContact(contact); } }); builder.setView(view); diff --git a/src/eu/siacs/conversations/utils/Validator.java b/src/eu/siacs/conversations/utils/Validator.java index fce953ae..51d8b25c 100644 --- a/src/eu/siacs/conversations/utils/Validator.java +++ b/src/eu/siacs/conversations/utils/Validator.java @@ -5,7 +5,7 @@ import java.util.regex.Pattern; public class Validator { public static final Pattern VALID_JID = - Pattern.compile("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6}$", Pattern.CASE_INSENSITIVE); + Pattern.compile("\\b^[A-Z0-9._%+-]+@([A-Z0-9.-]+\\.)?\\d{1,3}[.]\\d{1,3}[.]\\d{1,3}[.]\\d{1,3}\\b$|^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6}$", Pattern.CASE_INSENSITIVE); public static boolean isValidJid(String jid) { Matcher matcher = VALID_JID.matcher(jid); diff --git a/src/eu/siacs/conversations/xml/Element.java b/src/eu/siacs/conversations/xml/Element.java index ce1d10ce..f8e070f7 100644 --- a/src/eu/siacs/conversations/xml/Element.java +++ b/src/eu/siacs/conversations/xml/Element.java @@ -9,24 +9,24 @@ public class Element { protected Hashtable<String, String> attributes = new Hashtable<String, String>(); protected String content; protected List<Element> children = new ArrayList<Element>(); - + public Element(String name) { this.name = name; } - + public Element addChild(Element child) { this.content = null; children.add(child); return child; } - + public Element addChild(String name) { this.content = null; Element child = new Element(name); children.add(child); return child; } - + public Element addChild(String name, String xmlns) { this.content = null; Element child = new Element(name); @@ -34,64 +34,65 @@ public class Element { children.add(child); return child; } - + public Element setContent(String content) { this.content = content; this.children.clear(); return this; } - + public Element findChild(String name) { - for(Element child : this.children) { + for (Element child : this.children) { if (child.getName().equals(name)) { return child; } } return null; } - + public Element findChild(String name, String xmlns) { - for(Element child : this.children) { - if (child.getName().equals(name)&&(child.getAttribute("xmlns").equals(xmlns))) { + for (Element child : this.children) { + if (child.getName().equals(name) + && (child.getAttribute("xmlns").equals(xmlns))) { return child; } } return null; } - + public boolean hasChild(String name) { return findChild(name) != null; } - + public boolean hasChild(String name, String xmlns) { return findChild(name, xmlns) != null; } - - - + public List<Element> getChildren() { return this.children; } - + public Element setChildren(List<Element> children) { this.children = children; return this; } - + public String getContent() { return content; } - + public Element setAttribute(String name, String value) { - this.attributes.put(name, value); + if (name != null && value != null) { + this.attributes.put(name, value); + } return this; } - + public Element setAttributes(Hashtable<String, String> attributes) { this.attributes = attributes; return this; } - + public String getAttribute(String name) { if (this.attributes.containsKey(name)) { return this.attributes.get(name); @@ -99,14 +100,14 @@ public class Element { return null; } } - + public Hashtable<String, String> getAttributes() { return this.attributes; } - + public String toString() { StringBuilder elementOutput = new StringBuilder(); - if ((content==null)&&(children.size() == 0)) { + if ((content == null) && (children.size() == 0)) { Tag emptyTag = Tag.empty(name); emptyTag.setAtttributes(this.attributes); elementOutput.append(emptyTag.toString()); @@ -114,10 +115,10 @@ public class Element { Tag startTag = Tag.start(name); startTag.setAtttributes(this.attributes); elementOutput.append(startTag); - if (content!=null) { + if (content != null) { elementOutput.append(encodeEntities(content)); } else { - for(Element child : children) { + for (Element child : children) { elementOutput.append(child.toString()); } } @@ -130,13 +131,13 @@ public class Element { public String getName() { return name; } - + private String encodeEntities(String content) { - content = content.replace("&","&"); - content = content.replace("<","<"); - content = content.replace(">",">"); - content = content.replace("\"","""); - content = content.replace("'","'"); + content = content.replace("&", "&"); + content = content.replace("<", "<"); + content = content.replace(">", ">"); + content = content.replace("\"", """); + content = content.replace("'", "'"); return content; } diff --git a/src/eu/siacs/conversations/xml/XmlReader.java b/src/eu/siacs/conversations/xml/XmlReader.java index 0a82a5d8..b4b1647e 100644 --- a/src/eu/siacs/conversations/xml/XmlReader.java +++ b/src/eu/siacs/conversations/xml/XmlReader.java @@ -85,10 +85,8 @@ public class XmlReader { public Element readElement(Tag currentTag) throws XmlPullParserException, IOException { Element element = new Element(currentTag.getName()); - //Log.d(LOGTAG,"trying to read element "+element.getName()); element.setAttributes(currentTag.getAttributes()); Tag nextTag = this.readTag(); - //Log.d(LOGTAG,"next Tag is: "+nextTag.toString()); if(nextTag.isNo()) { element.setContent(nextTag.getName()); nextTag = this.readTag(); @@ -96,15 +94,16 @@ public class XmlReader { if (nextTag == null) { throw new IOException("unterupted mid tag"); } - //Log.d(LOGTAG,"reading till the end of "+element.getName()); while(!nextTag.isEnd(element.getName())) { if (!nextTag.isNo()) { Element child = this.readElement(nextTag); element.addChild(child); } nextTag = this.readTag(); + if (nextTag == null) { + throw new IOException("unterupted mid tag"); + } } - //Log.d(LOGTAG,"return with element"+element); return element; } } diff --git a/src/eu/siacs/conversations/xmpp/OnContactStatusChanged.java b/src/eu/siacs/conversations/xmpp/OnContactStatusChanged.java new file mode 100644 index 00000000..849e8e76 --- /dev/null +++ b/src/eu/siacs/conversations/xmpp/OnContactStatusChanged.java @@ -0,0 +1,7 @@ +package eu.siacs.conversations.xmpp; + +import eu.siacs.conversations.entities.Contact; + +public interface OnContactStatusChanged { + public void onContactStatusChanged(Contact contact, boolean online); +} diff --git a/src/eu/siacs/conversations/xmpp/XmppConnection.java b/src/eu/siacs/conversations/xmpp/XmppConnection.java index adb27ec8..2447b49b 100644 --- a/src/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/eu/siacs/conversations/xmpp/XmppConnection.java @@ -17,11 +17,9 @@ import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.HashMap; -import java.util.HashSet; import java.util.Hashtable; import java.util.Iterator; import java.util.List; -import java.util.Map; import java.util.Map.Entry; import javax.net.ssl.SSLContext; @@ -31,7 +29,6 @@ import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; -import org.json.JSONException; import org.xmlpull.v1.XmlPullParserException; import android.os.Bundle; @@ -40,6 +37,7 @@ import android.os.PowerManager.WakeLock; import android.os.SystemClock; import android.util.Log; import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.DNSHelper; import eu.siacs.conversations.utils.zlib.ZLibOutputStream; @@ -66,7 +64,7 @@ public class XmppConnection implements Runnable { private WakeLock wakeLock; - private SecureRandom random = new SecureRandom(); + private SecureRandom mRandom; private Socket socket; private XmlReader tagReader; @@ -76,20 +74,20 @@ public class XmppConnection implements Runnable { private boolean shouldAuthenticate = true; private Element streamFeatures; private HashMap<String, List<String>> disco = new HashMap<String, List<String>>(); - - private HashSet<String> pendingSubscriptions = new HashSet<String>(); - + private String streamId = null; private int smVersion = 3; - + private int stanzasReceived = 0; private int stanzasSent = 0; - + public long lastPaketReceived = 0; public long lastPingSent = 0; public long lastConnect = 0; public long lastSessionStarted = 0; + private int attempt = 0; + private static final int PACKET_IQ = 0; private static final int PACKET_MESSAGE = 1; private static final int PACKET_PRESENCE = 2; @@ -103,17 +101,25 @@ public class XmppConnection implements Runnable { private OnTLSExceptionReceived tlsListener = null; private OnBindListener bindListener = null; - public XmppConnection(Account account, PowerManager pm) { + public XmppConnection(Account account, XmppConnectionService service) { + this.mRandom = service.getRNG(); this.account = account; - this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,account.getJid()); + this.wakeLock = service.getPowerManager().newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, + account.getJid()); tagWriter = new TagWriter(); } protected void changeStatus(int nextStatus) { if (account.getStatus() != nextStatus) { - if ((nextStatus == Account.STATUS_OFFLINE)&&(account.getStatus() != Account.STATUS_CONNECTING)&&(account.getStatus() != Account.STATUS_ONLINE)) { + if ((nextStatus == Account.STATUS_OFFLINE) + && (account.getStatus() != Account.STATUS_CONNECTING) + && (account.getStatus() != Account.STATUS_ONLINE) + && (account.getStatus() != Account.STATUS_DISABLED)) { return; } + if (nextStatus == Account.STATUS_ONLINE) { + this.attempt = 0; + } account.setStatus(nextStatus); if (statusListener != null) { statusListener.onStatusChanged(account); @@ -122,17 +128,19 @@ public class XmppConnection implements Runnable { } protected void connect() { - Log.d(LOGTAG,account.getJid()+ ": connecting"); + Log.d(LOGTAG, account.getJid() + ": connecting"); lastConnect = SystemClock.elapsedRealtime(); + this.attempt++; try { - shouldAuthenticate = shouldBind = !account.isOptionSet(Account.OPTION_REGISTER); + shouldAuthenticate = shouldBind = !account + .isOptionSet(Account.OPTION_REGISTER); tagReader = new XmlReader(wakeLock); tagWriter = new TagWriter(); packetCallbacks.clear(); this.changeStatus(Account.STATUS_CONNECTING); Bundle namePort = DNSHelper.getSRVRecord(account.getServer()); if ("timeout".equals(namePort.getString("error"))) { - Log.d(LOGTAG,account.getJid()+": dns timeout"); + Log.d(LOGTAG, account.getJid() + ": dns timeout"); this.changeStatus(Account.STATUS_OFFLINE); return; } @@ -142,12 +150,12 @@ public class XmppConnection implements Runnable { if (srvRecordServer != null) { if (srvIpServer != null) { Log.d(LOGTAG, account.getJid() + ": using values from dns " - + srvRecordServer + "[" + srvIpServer + "]:" - + srvRecordPort); + + srvRecordServer + "[" + srvIpServer + "]:" + + srvRecordPort); socket = new Socket(srvIpServer, srvRecordPort); } else { Log.d(LOGTAG, account.getJid() + ": using values from dns " - + srvRecordServer + ":" + srvRecordPort); + + srvRecordServer + ":" + srvRecordPort); socket = new Socket(srvRecordServer, srvRecordPort); } } else { @@ -227,56 +235,60 @@ public class XmppConnection implements Runnable { } else if (nextTag.isStart("compressed")) { switchOverToZLib(nextTag); } else if (nextTag.isStart("success")) { - Log.d(LOGTAG, account.getJid() - + ": logged in"); + Log.d(LOGTAG, account.getJid() + ": logged in"); tagReader.readTag(); tagReader.reset(); sendStartStream(); processStream(tagReader.readTag()); break; } else if (nextTag.isStart("failure")) { - Element failure = tagReader.readElement(nextTag); + 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)); + response.setAttribute("xmlns", + "urn:ietf:params:xml:ns:xmpp-sasl"); + response.setContent(CryptoHelper.saslDigestMd5(account, + challange,mRandom)); tagWriter.writeElement(response); } else if (nextTag.isStart("enabled")) { this.stanzasSent = 0; Element enabled = tagReader.readElement(nextTag); if ("true".equals(enabled.getAttribute("resume"))) { this.streamId = enabled.getAttribute("id"); - Log.d(LOGTAG,account.getJid()+": stream managment("+smVersion+") enabled (resumable)"); + Log.d(LOGTAG, account.getJid() + ": stream managment(" + + smVersion + ") enabled (resumable)"); } else { - Log.d(LOGTAG,account.getJid()+": stream managment("+smVersion+") enabled"); + Log.d(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(); + Log.d(LOGTAG, account.getJid() + ": session resumed"); tagReader.readElement(nextTag); sendPing(); changeStatus(Account.STATUS_ONLINE); - Log.d(LOGTAG,account.getJid()+": session resumed"); } else if (nextTag.isStart("r")) { tagReader.readElement(nextTag); - AckPacket ack = new AckPacket(this.stanzasReceived,smVersion); - //Log.d(LOGTAG,ack.toString()); + AckPacket ack = new AckPacket(this.stanzasReceived, smVersion); + // Log.d(LOGTAG,ack.toString()); tagWriter.writeStanzaAsync(ack); } else if (nextTag.isStart("a")) { Element ack = tagReader.readElement(nextTag); lastPaketReceived = SystemClock.elapsedRealtime(); int serverSequence = Integer.parseInt(ack.getAttribute("h")); - if (serverSequence>this.stanzasSent) { + if (serverSequence > this.stanzasSent) { this.stanzasSent = serverSequence; } - //Log.d(LOGTAG,"server ack"+ack.toString()+" ("+this.stanzasSent+")"); + // Log.d(LOGTAG,"server ack"+ack.toString()+" ("+this.stanzasSent+")"); } else if (nextTag.isStart("failed")) { tagReader.readElement(nextTag); - Log.d(LOGTAG,account.getJid()+": resumption failed"); + Log.d(LOGTAG, account.getJid() + ": resumption failed"); streamId = null; if (account.getStatus() != Account.STATUS_ONLINE) { sendBindRequest(); @@ -319,16 +331,23 @@ public class XmppConnection implements Runnable { } element.setAttributes(currentTag.getAttributes()); Tag nextTag = tagReader.readTag(); + if (nextTag==null) { + throw new IOException("interrupted mid tag"); + } while (!nextTag.isEnd(element.getName())) { if (!nextTag.isNo()) { Element child = tagReader.readElement(nextTag); - if ((packetType == PACKET_IQ)&&("jingle".equals(child.getName()))) { + if ((packetType == PACKET_IQ) + && ("jingle".equals(child.getName()))) { element = new JinglePacket(); element.setAttributes(currentTag.getAttributes()); } element.addChild(child); } nextTag = tagReader.readTag(); + if (nextTag==null) { + throw new IOException("interrupted mid tag"); + } } ++stanzasReceived; lastPaketReceived = SystemClock.elapsedRealtime(); @@ -338,14 +357,15 @@ public class XmppConnection implements Runnable { private void processIq(Tag currentTag) throws XmlPullParserException, IOException { IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ); - + if (packet.getId() == null) { - return; //an iq packet without id is definitely invalid + return; // an iq packet without id is definitely invalid } - + if (packet instanceof JinglePacket) { - if (this.jingleListener !=null) { - this.jingleListener.onJinglePacketReceived(account, (JinglePacket) packet); + if (this.jingleListener != null) { + this.jingleListener.onJinglePacketReceived(account, + (JinglePacket) packet); } } else { if (packetCallbacks.containsKey(packet.getId())) { @@ -353,7 +373,7 @@ public class XmppConnection implements Runnable { ((OnIqPacketReceived) packetCallbacks.get(packet.getId())) .onIqPacketReceived(account, packet); } - + packetCallbacks.remove(packet.getId()); } else if (this.unregisteredIqListener != null) { this.unregisteredIqListener.onIqPacketReceived(account, packet); @@ -400,15 +420,18 @@ public class XmppConnection implements Runnable { tagWriter.writeElement(compress); } - private void switchOverToZLib(Tag currentTag) throws XmlPullParserException, - IOException, NoSuchAlgorithmException { + 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())); + tagWriter.setOutputStream(new ZLibOutputStream(tagWriter + .getOutputStream())); + tagReader + .setInputStream(new ZLibInputStream(tagReader.getInputStream())); sendStartStream(); - Log.d(LOGTAG,account.getJid()+": compression enabled"); + Log.d(LOGTAG, account.getJid() + ": compression enabled"); processStream(tagReader.readTag()); } @@ -420,17 +443,14 @@ public class XmppConnection implements Runnable { private void switchOverToTls(Tag currentTag) throws XmlPullParserException, IOException { - Tag nextTag = tagReader.readTag(); // should be proceed end tag + tagReader.readTag(); try { SSLContext sc = SSLContext.getInstance("TLS"); TrustManagerFactory tmf = TrustManagerFactory .getInstance(TrustManagerFactory.getDefaultAlgorithm()); - // Initialise the TMF as you normally would, for example: - // tmf.in try { tmf.init((KeyStore) null); } catch (KeyStoreException e1) { - // TODO Auto-generated catch block e1.printStackTrace(); } @@ -454,13 +474,15 @@ public class XmppConnection implements Runnable { if (e.getCause() instanceof CertPathValidatorException) { String sha; try { - MessageDigest sha1 = MessageDigest.getInstance("SHA1"); + MessageDigest sha1 = MessageDigest + .getInstance("SHA1"); sha1.update(chain[0].getEncoded()); sha = CryptoHelper.bytesToHex(sha1.digest()); if (!sha.equals(account.getSSLFingerprint())) { changeStatus(Account.STATUS_TLS_ERROR); - if (tlsListener!=null) { - tlsListener.onTLSExceptionReceived(sha,account); + if (tlsListener != null) { + tlsListener.onTLSExceptionReceived(sha, + account); } throw new CertificateException(); } @@ -483,12 +505,12 @@ public class XmppConnection implements Runnable { sc.init(null, wrappedTrustManagers, null); SSLSocketFactory factory = sc.getSocketFactory(); SSLSocket sslSocket = (SSLSocket) factory.createSocket(socket, - socket.getInetAddress().getHostAddress(), socket.getPort(), - true); + socket.getInetAddress().getHostAddress(), socket.getPort(), + true); tagReader.setInputStream(sslSocket.getInputStream()); tagWriter.setOutputStream(sslSocket.getOutputStream()); sendStartStream(); - Log.d(LOGTAG,account.getJid()+": TLS connection established"); + Log.d(LOGTAG, account.getJid() + ": TLS connection established"); processStream(tagReader.readTag()); sslSocket.close(); } catch (NoSuchAlgorithmException e1) { @@ -509,7 +531,7 @@ public class XmppConnection implements Runnable { auth.setContent(saslString); tagWriter.writeElement(auth); } - + private void sendSaslAuthDigestMd5() throws IOException { Element auth = new Element("auth"); auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl"); @@ -525,41 +547,47 @@ public class XmppConnection implements Runnable { sendStartTLS(); } else if (compressionAvailable()) { sendCompressionZlib(); - } else if (this.streamFeatures.hasChild("register")&&(account.isOptionSet(Account.OPTION_REGISTER))) { - sendRegistryRequest(); - } else if (!this.streamFeatures.hasChild("register")&&(account.isOptionSet(Account.OPTION_REGISTER))) { + } else if (this.streamFeatures.hasChild("register") + && (account.isOptionSet(Account.OPTION_REGISTER))) { + sendRegistryRequest(); + } else if (!this.streamFeatures.hasChild("register") + && (account.isOptionSet(Account.OPTION_REGISTER))) { changeStatus(Account.STATUS_REGISTRATION_NOT_SUPPORTED); disconnect(true); } else if (this.streamFeatures.hasChild("mechanisms") && shouldAuthenticate) { - List<String> mechanisms = extractMechanisms( streamFeatures.findChild("mechanisms")); + List<String> mechanisms = extractMechanisms(streamFeatures + .findChild("mechanisms")); if (mechanisms.contains("PLAIN")) { sendSaslAuthPlain(); } else if (mechanisms.contains("DIGEST-MD5")) { sendSaslAuthDigestMd5(); } - } else if (this.streamFeatures.hasChild("sm","urn:xmpp:sm:"+smVersion) && streamId != null) { - ResumePacket resume = new ResumePacket(this.streamId,stanzasReceived,smVersion); + } else if (this.streamFeatures.hasChild("sm", "urn:xmpp:sm:" + + smVersion) + && streamId != null) { + ResumePacket resume = new ResumePacket(this.streamId, + stanzasReceived, smVersion); this.tagWriter.writeStanzaAsync(resume); } else if (this.streamFeatures.hasChild("bind") && shouldBind) { sendBindRequest(); - if (this.streamFeatures.hasChild("session")) { - Log.d(LOGTAG,account.getJid()+": sending deprecated session"); - IqPacket startSession = new IqPacket(IqPacket.TYPE_SET); - startSession.addChild("session","urn:ietf:params:xml:ns:xmpp-session"); //setContent("") - this.sendIqPacket(startSession, null); - } } } private boolean compressionAvailable() { - if (!this.streamFeatures.hasChild("compression", "http://jabber.org/features/compress")) return false; - if (!ZLibOutputStream.SUPPORTED) return false; - if (!account.isOptionSet(Account.OPTION_USECOMPRESSION)) return false; + if (!this.streamFeatures.hasChild("compression", + "http://jabber.org/features/compress")) + return false; + if (!ZLibOutputStream.SUPPORTED) + return false; + if (!account.isOptionSet(Account.OPTION_USECOMPRESSION)) + return false; - Element compression = this.streamFeatures.findChild("compression", "http://jabber.org/features/compress"); + Element compression = this.streamFeatures.findChild("compression", + "http://jabber.org/features/compress"); for (Element child : compression.getChildren()) { - if (!"method".equals(child.getName())) continue; + if (!"method".equals(child.getName())) + continue; if ("zlib".equalsIgnoreCase(child.getContent())) { return true; @@ -569,8 +597,9 @@ public class XmppConnection implements Runnable { } private List<String> extractMechanisms(Element stream) { - ArrayList<String> mechanisms = new ArrayList<String>(stream.getChildren().size()); - for(Element child : stream.getChildren()) { + ArrayList<String> mechanisms = new ArrayList<String>(stream + .getChildren().size()); + for (Element child : stream.getChildren()) { mechanisms.add(child.getContent()); } return mechanisms; @@ -581,28 +610,35 @@ public class XmppConnection implements Runnable { register.query("jabber:iq:register"); register.setTo(account.getServer()); sendIqPacket(register, new OnIqPacketReceived() { - + @Override public void onIqPacketReceived(Account account, IqPacket packet) { Element instructions = packet.query().findChild("instructions"); - if (packet.query().hasChild("username")&&(packet.query().hasChild("password"))) { + if (packet.query().hasChild("username") + && (packet.query().hasChild("password"))) { IqPacket register = new IqPacket(IqPacket.TYPE_SET); - Element username = new Element("username").setContent(account.getUsername()); - Element password = new Element("password").setContent(account.getPassword()); + Element username = new Element("username") + .setContent(account.getUsername()); + Element password = new Element("password") + .setContent(account.getPassword()); register.query("jabber:iq:register").addChild(username); register.query().addChild(password); sendIqPacket(register, new OnIqPacketReceived() { - + @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType()==IqPacket.TYPE_RESULT) { - account.setOption(Account.OPTION_REGISTER, false); + public void onIqPacketReceived(Account account, + IqPacket packet) { + if (packet.getType() == IqPacket.TYPE_RESULT) { + account.setOption(Account.OPTION_REGISTER, + false); changeStatus(Account.STATUS_REGISTRATION_SUCCESSFULL); - } else if (packet.hasChild("error")&&(packet.findChild("error").hasChild("conflict"))){ + } else if (packet.hasChild("error") + && (packet.findChild("error") + .hasChild("conflict"))) { changeStatus(Account.STATUS_REGISTRATION_CONFLICT); } else { changeStatus(Account.STATUS_REGISTRATION_FAILED); - Log.d(LOGTAG,packet.toString()); + Log.d(LOGTAG, packet.toString()); } disconnect(true); } @@ -610,54 +646,49 @@ public class XmppConnection implements Runnable { } else { changeStatus(Account.STATUS_REGISTRATION_FAILED); disconnect(true); - Log.d(LOGTAG,account.getJid()+": could not register. instructions are"+instructions.getContent()); + Log.d(LOGTAG, account.getJid() + + ": could not register. instructions are" + + instructions.getContent()); } } }); } - private void sendInitialPresence() { - PresencePacket packet = new PresencePacket(); - packet.setAttribute("from", account.getFullJid()); - if (account.getKeys().has("pgp_signature")) { - try { - String signature = account.getKeys().getString("pgp_signature"); - packet.addChild("status").setContent("online"); - packet.addChild("x","jabber:x:signed").setContent(signature); - } catch (JSONException e) { - // - } - } - this.sendPresencePacket(packet); - } - private void sendBindRequest() throws IOException { IqPacket iq = new IqPacket(IqPacket.TYPE_SET); - iq.addChild("bind", "urn:ietf:params:xml:ns:xmpp-bind").addChild("resource").setContent(account.getResource()); - this.sendIqPacket(iq, new OnIqPacketReceived() { + iq.addChild("bind", "urn:ietf:params:xml:ns:xmpp-bind") + .addChild("resource").setContent(account.getResource()); + this.sendUnboundIqPacket(iq, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { String resource = packet.findChild("bind").findChild("jid") .getContent().split("/")[1]; account.setResource(resource); - if (streamFeatures.hasChild("sm","urn:xmpp:sm:3")) { + if (streamFeatures.hasChild("sm", "urn:xmpp:sm:3")) { smVersion = 3; EnablePacket enable = new EnablePacket(smVersion); tagWriter.writeStanzaAsync(enable); - } else if (streamFeatures.hasChild("sm","urn:xmpp:sm:2")) { + } else if (streamFeatures.hasChild("sm", "urn:xmpp:sm:2")) { smVersion = 2; EnablePacket enable = new EnablePacket(smVersion); tagWriter.writeStanzaAsync(enable); } - sendInitialPresence(); sendServiceDiscoveryInfo(account.getServer()); sendServiceDiscoveryItems(account.getServer()); - if (bindListener !=null) { + if (bindListener != null) { bindListener.onBind(account); } + changeStatus(Account.STATUS_ONLINE); } }); + if (this.streamFeatures.hasChild("session")) { + Log.d(LOGTAG, account.getJid() + ": sending deprecated session"); + IqPacket startSession = new IqPacket(IqPacket.TYPE_SET); + startSession.addChild("session", + "urn:ietf:params:xml:ns:xmpp-session"); + this.sendUnboundIqPacket(startSession, null); + } } private void sendServiceDiscoveryInfo(final String server) { @@ -672,25 +703,24 @@ public class XmppConnection implements Runnable { 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")); + features.add(elements.get(i).getAttribute("var")); } } disco.put(server, features); - + if (account.getServer().equals(server)) { enableAdvancedStreamFeatures(); } } }); } - + private void enableAdvancedStreamFeatures() { if (hasFeaturesCarbon()) { sendEnableCarbons(); } } - + private void sendServiceDiscoveryItems(final String server) { IqPacket iq = new IqPacket(IqPacket.TYPE_GET); iq.setTo(server); @@ -702,8 +732,7 @@ public class XmppConnection implements Runnable { 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"); + String jid = elements.get(i).getAttribute("jid"); sendServiceDiscoveryInfo(jid); } } @@ -713,7 +742,7 @@ public class XmppConnection implements Runnable { private void sendEnableCarbons() { IqPacket iq = new IqPacket(IqPacket.TYPE_SET); - iq.addChild("enable","urn:xmpp:carbons:2"); + iq.addChild("enable", "urn:xmpp:carbons:2"); this.sendIqPacket(iq, new OnIqPacketReceived() { @Override @@ -745,11 +774,11 @@ public class XmppConnection implements Runnable { } private String nextRandomId() { - return new BigInteger(50, random).toString(32); + return new BigInteger(50, mRandom).toString(32); } public void sendIqPacket(IqPacket packet, OnIqPacketReceived callback) { - if (packet.getId()==null) { + if (packet.getId() == null) { String id = nextRandomId(); packet.setAttribute("id", id); } @@ -757,6 +786,14 @@ public class XmppConnection implements Runnable { this.sendPacket(packet, callback); } + public void sendUnboundIqPacket(IqPacket packet, OnIqPacketReceived callback) { + if (packet.getId() == null) { + String id = nextRandomId(); + packet.setAttribute("id", id); + } + this.sendPacket(packet, callback); + } + public void sendMessagePacket(MessagePacket packet) { this.sendPacket(packet, null); } @@ -774,26 +811,27 @@ public class XmppConnection implements Runnable { OnPresencePacketReceived callback) { this.sendPacket(packet, callback); } - - private synchronized void sendPacket(final AbstractStanza packet, PacketReceived callback) { + + private synchronized void sendPacket(final AbstractStanza packet, + PacketReceived callback) { // TODO dont increment stanza count if packet = request packet or ack; ++stanzasSent; tagWriter.writeStanzaAsync(packet); if (callback != null) { - if (packet.getId()==null) { + if (packet.getId() == null) { packet.setId(nextRandomId()); } packetCallbacks.put(packet.getId(), callback); } } - + public void sendPing() { if (streamFeatures.hasChild("sm")) { tagWriter.writeStanzaAsync(new RequestPacket(smVersion)); } else { IqPacket iq = new IqPacket(IqPacket.TYPE_GET); iq.setFrom(account.getFullJid()); - iq.addChild("ping","urn:xmpp:ping"); + iq.addChild("ping", "urn:xmpp:ping"); this.sendIqPacket(iq, null); } } @@ -812,82 +850,95 @@ public class XmppConnection implements Runnable { OnPresencePacketReceived listener) { this.presenceListener = listener; } - - public void setOnJinglePacketReceivedListener(OnJinglePacketReceived listener) { + + public void setOnJinglePacketReceivedListener( + OnJinglePacketReceived listener) { this.jingleListener = listener; } public void setOnStatusChangedListener(OnStatusChanged listener) { this.statusListener = listener; } - - public void setOnTLSExceptionReceivedListener(OnTLSExceptionReceived listener) { + + public void setOnTLSExceptionReceivedListener( + OnTLSExceptionReceived listener) { this.tlsListener = listener; } - + public void setOnBindListener(OnBindListener listener) { this.bindListener = listener; } public void disconnect(boolean force) { changeStatus(Account.STATUS_OFFLINE); - Log.d(LOGTAG,"disconnecting"); + Log.d(LOGTAG, "disconnecting"); try { - if (force) { + if (force) { socket.close(); return; - } - if (tagWriter.isActive()) { - tagWriter.finish(); - while(!tagWriter.finished()) { - //Log.d(LOGTAG,"not yet finished"); - Thread.sleep(100); } - tagWriter.writeTag(Tag.end("stream:stream")); - } + new Thread(new Runnable() { + + @Override + public void run() { + if (tagWriter.isActive()) { + tagWriter.finish(); + try { + while (!tagWriter.finished()) { + Log.d(LOGTAG, "not yet finished"); + Thread.sleep(100); + } + tagWriter.writeTag(Tag.end("stream:stream")); + } catch (IOException e) { + Log.d(LOGTAG, "io exception during disconnect"); + } catch (InterruptedException e) { + Log.d(LOGTAG, "interrupted"); + } + } + } + }).start(); } catch (IOException e) { - Log.d(LOGTAG,"io exception during disconnect"); - } catch (InterruptedException e) { - Log.d(LOGTAG,"interupted while waiting for disconnect"); + Log.d(LOGTAG, "io exception during disconnect"); } } - + public boolean hasFeatureRosterManagment() { - if (this.streamFeatures==null) { + if (this.streamFeatures == null) { return false; } else { return this.streamFeatures.hasChild("ver"); } } - + public boolean hasFeatureStreamManagment() { - if (this.streamFeatures==null) { + if (this.streamFeatures == null) { return false; } else { return this.streamFeatures.hasChild("sm"); } } - + public boolean hasFeaturesCarbon() { return hasDiscoFeature(account.getServer(), "urn:xmpp:carbons:2"); } - + public boolean hasDiscoFeature(String server, String feature) { if (!disco.containsKey(server)) { return false; } return disco.get(server).contains(feature); } - + public String findDiscoItemByFeature(String feature) { - Iterator<Entry<String, List<String>>> it = this.disco.entrySet().iterator(); - while (it.hasNext()) { - Entry<String, List<String>> pairs = it.next(); - if (pairs.getValue().contains(feature)) { - return pairs.getKey(); - } - it.remove(); - } + Iterator<Entry<String, List<String>>> it = this.disco.entrySet() + .iterator(); + while (it.hasNext()) { + Entry<String, List<String>> pairs = it.next(); + if (pairs.getValue().contains(feature)&&pairs.getValue().size()==1) { + return pairs.getKey(); + } + it.remove(); + } return null; } @@ -898,7 +949,7 @@ public class XmppConnection implements Runnable { public int getReceivedStanzas() { return this.stanzasReceived; } - + public int getSentStanzas() { return this.stanzasSent; } @@ -906,13 +957,14 @@ public class XmppConnection implements Runnable { public String getMucServer() { return findDiscoItemByFeature("http://jabber.org/protocol/muc"); } - - public boolean hasPendingSubscription(String jid) { - return this.pendingSubscriptions.contains(jid); + + public int getTimeToNextAttempt() { + int interval = (int) (25 * Math.pow(1.5, attempt)); + int secondsSinceLast = (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000); + return interval - secondsSinceLast; } - - public void addPendingSubscription(String jid) { - Log.d(LOGTAG,"adding "+jid+" to pending subscriptions"); - this.pendingSubscriptions.add(jid); + + public int getAttempt() { + return this.attempt; } } diff --git a/src/eu/siacs/conversations/xmpp/jingle/JingleConnection.java b/src/eu/siacs/conversations/xmpp/jingle/JingleConnection.java index 8f88688a..85110c38 100644 --- a/src/eu/siacs/conversations/xmpp/jingle/JingleConnection.java +++ b/src/eu/siacs/conversations/xmpp/jingle/JingleConnection.java @@ -9,11 +9,11 @@ import java.util.Map.Entry; import android.graphics.BitmapFactory; import android.util.Log; - 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.utils.CryptoHelper; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.OnIqPacketReceived; import eu.siacs.conversations.xmpp.jingle.stanzas.Content; @@ -24,7 +24,7 @@ import eu.siacs.conversations.xmpp.stanzas.IqPacket; public class JingleConnection { private final String[] extensions = {"webp","jpeg","jpg","png"}; - private final String[] cryptoExtensions = {"pgp","gpg"}; + private final String[] cryptoExtensions = {"pgp","gpg","otr"}; private JingleConnectionManager mJingleConnectionManager; private XmppConnectionService mXmppConnectionService; @@ -244,6 +244,7 @@ public class JingleConnection { Element fileNameElement = fileOffer.findChild("name"); if (fileNameElement!=null) { boolean supportedFile = false; + Log.d("xmppService","file offer: "+fileNameElement.getContent()); String[] filename = fileNameElement.getContent().toLowerCase().split("\\."); if (Arrays.asList(this.extensions).contains(filename[filename.length - 1])) { supportedFile = true; @@ -251,7 +252,12 @@ public class JingleConnection { if (filename.length == 3) { if (Arrays.asList(this.extensions).contains(filename[filename.length -2])) { supportedFile = true; - this.message.setEncryption(Message.ENCRYPTION_PGP); + if (filename[filename.length - 1].equals("otr")) { + Log.d("xmppService","receiving otr file"); + this.message.setEncryption(Message.ENCRYPTION_OTR); + } else { + this.message.setEncryption(Message.ENCRYPTION_PGP); + } } } } @@ -269,6 +275,9 @@ public class JingleConnection { this.mXmppConnectionService.updateUi(conversation, true); } this.file = this.mXmppConnectionService.getFileBackend().getJingleFile(message,false); + if (message.getEncryption() == Message.ENCRYPTION_OTR) { + this.file.setKey(conversation.getSymmetricKey()); + } this.file.setExpectedSize(size); } else { this.sendCancel(); @@ -287,7 +296,14 @@ public class JingleConnection { if (message.getType() == Message.TYPE_IMAGE) { content.setTransportId(this.transportId); this.file = this.mXmppConnectionService.getFileBackend().getJingleFile(message,false); - content.setFileOffer(this.file); + if (message.getEncryption() == Message.ENCRYPTION_OTR) { + Conversation conversation = this.message.getConversation(); + this.mXmppConnectionService.renewSymmetricKey(conversation); + content.setFileOffer(this.file, true); + this.file.setKey(conversation.getSymmetricKey()); + } else { + content.setFileOffer(this.file,false); + } this.transportId = this.mJingleConnectionManager.nextRandomId(); content.setTransportId(this.transportId); content.socks5transport().setChildren(getCandidatesAsElements()); diff --git a/src/eu/siacs/conversations/xmpp/jingle/JingleFile.java b/src/eu/siacs/conversations/xmpp/jingle/JingleFile.java index 21cbd716..3672351b 100644 --- a/src/eu/siacs/conversations/xmpp/jingle/JingleFile.java +++ b/src/eu/siacs/conversations/xmpp/jingle/JingleFile.java @@ -1,6 +1,12 @@ package eu.siacs.conversations.xmpp.jingle; import java.io.File; +import java.security.Key; + +import javax.crypto.spec.SecretKeySpec; + +import eu.siacs.conversations.utils.CryptoHelper; +import android.util.Log; public class JingleFile extends File { @@ -8,6 +14,7 @@ public class JingleFile extends File { private long expectedSize = 0; private String sha1sum; + private Key aeskey; public JingleFile(String path) { super(path); @@ -18,7 +25,11 @@ public class JingleFile extends File { } public long getExpectedSize() { - return this.expectedSize; + if (this.aeskey!=null) { + return (this.expectedSize/16 + 1) * 16; + } else { + return this.expectedSize; + } } public void setExpectedSize(long size) { @@ -32,4 +43,23 @@ public class JingleFile extends File { public void setSha1Sum(String sum) { this.sha1sum = sum; } + + public void setKey(byte[] key) { + if (key.length>=32) { + byte[] secretKey = new byte[32]; + System.arraycopy(key, 0, secretKey, 0, 32); + this.aeskey = new SecretKeySpec(secretKey, "AES"); + } else if (key.length>=16) { + byte[] secretKey = new byte[16]; + System.arraycopy(key, 0, secretKey, 0, 16); + this.aeskey = new SecretKeySpec(secretKey, "AES"); + } else { + Log.d("xmppService","weird key"); + } + Log.d("xmppService","using aes key "+CryptoHelper.bytesToHex(this.aeskey.getEncoded())); + } + + public Key getKey() { + return this.aeskey; + } } diff --git a/src/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java b/src/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java index 8326fa39..a46e4a47 100644 --- a/src/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java +++ b/src/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java @@ -8,12 +8,10 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import android.util.Base64; -import android.util.Log; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.OnIqPacketReceived; -import eu.siacs.conversations.xmpp.PacketReceived; import eu.siacs.conversations.xmpp.stanzas.IqPacket; public class JingleInbandTransport extends JingleTransport { @@ -39,7 +37,6 @@ public class JingleInbandTransport extends JingleTransport { private OnIqPacketReceived onAckReceived = new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { - Log.d("xmppService", "on ack received"); if (packet.getType() == IqPacket.TYPE_RESULT) { sendNextBlock(); } @@ -82,7 +79,6 @@ public class JingleInbandTransport extends JingleTransport { public void receive(JingleFile file, OnFileTransmitted callback) { this.onFileTransmitted = callback; this.file = file; - Log.d("xmppService", "receiving file over ibb"); try { this.digest = MessageDigest.getInstance("SHA-1"); digest.reset(); @@ -101,7 +97,6 @@ public class JingleInbandTransport extends JingleTransport { public void send(JingleFile file, OnFileTransmitted callback) { this.onFileTransmitted = callback; this.file = file; - Log.d("xmppService", "sending file over ibb"); try { this.digest = MessageDigest.getInstance("SHA-1"); this.digest.reset(); @@ -151,10 +146,8 @@ public class JingleInbandTransport extends JingleTransport { this.fileOutputStream.write(buffer); this.digest.update(buffer); - Log.d("xmppService", "remaining file size:" + this.remainingSize); if (this.remainingSize <= 0) { file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest())); - Log.d("xmppService","file name: "+file.getAbsolutePath()); fileOutputStream.flush(); fileOutputStream.close(); this.onFileTransmitted.onFileTransmitted(file); @@ -179,7 +172,7 @@ public class JingleInbandTransport extends JingleTransport { this.account.getXmppConnection().sendIqPacket( packet.generateRespone(IqPacket.TYPE_RESULT), null); } else { - Log.d("xmppServic","couldnt deliver payload "+packet.toString()); + //TODO some sort of exception } } } diff --git a/src/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java b/src/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java index ea5d7285..4fef0388 100644 --- a/src/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java +++ b/src/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java @@ -6,17 +6,15 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.io.ObjectInputStream.GetField; import java.net.Socket; import java.net.UnknownHostException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; -import eu.siacs.conversations.utils.CryptoHelper; -import eu.siacs.conversations.xml.Element; - import android.util.Log; -import android.widget.Button; +import eu.siacs.conversations.utils.CryptoHelper; public class JingleSocks5Transport extends JingleTransport { private JingleCandidate candidate; @@ -94,17 +92,20 @@ public class JingleSocks5Transport extends JingleTransport { @Override public void run() { - FileInputStream fileInputStream = null; + InputStream fileInputStream = null; try { MessageDigest digest = MessageDigest.getInstance("SHA-1"); digest.reset(); - fileInputStream = new FileInputStream(file); + fileInputStream = getInputStream(file); int count; + long txBytes = 0; byte[] buffer = new byte[8192]; while ((count = fileInputStream.read(buffer)) > 0) { + txBytes += count; outputStream.write(buffer, 0, count); digest.update(buffer, 0, count); } + Log.d("xmppService","txBytes="+txBytes); outputStream.flush(); file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest())); if (callback!=null) { @@ -145,28 +146,26 @@ public class JingleSocks5Transport extends JingleTransport { inputStream.skip(45); file.getParentFile().mkdirs(); file.createNewFile(); - FileOutputStream fileOutputStream = new FileOutputStream(file); + OutputStream fileOutputStream = getOutputStream(file); long remainingSize = file.getExpectedSize(); byte[] buffer = new byte[8192]; int count = buffer.length; + long rxBytes = 0; while(remainingSize > 0) { - Log.d("xmppService","remaning size:"+remainingSize); - if (remainingSize<=count) { - count = (int) remainingSize; - } - count = inputStream.read(buffer, 0, count); + count = inputStream.read(buffer); if (count==-1) { - Log.d("xmppService","end of stream"); + Log.d("xmppService","read end"); } else { + rxBytes += count; fileOutputStream.write(buffer, 0, count); digest.update(buffer, 0, count); remainingSize-=count; } } + Log.d("xmppService","rx bytes="+rxBytes); fileOutputStream.flush(); fileOutputStream.close(); file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest())); - Log.d("xmppService","transmitted filename was: "+file.getAbsolutePath()); callback.onFileTransmitted(file); } catch (FileNotFoundException e) { // TODO Auto-generated catch block @@ -194,9 +193,8 @@ public class JingleSocks5Transport extends JingleTransport { if (this.socket!=null) { try { this.socket.close(); - Log.d("xmppService","cloesd socket with "+candidate.getHost()+":"+candidate.getPort()); } catch (IOException e) { - Log.d("xmppService","error closing socket with "+candidate.getHost()+":"+candidate.getPort()); + } } } diff --git a/src/eu/siacs/conversations/xmpp/jingle/JingleTransport.java b/src/eu/siacs/conversations/xmpp/jingle/JingleTransport.java index 6e9482a9..5db4b715 100644 --- a/src/eu/siacs/conversations/xmpp/jingle/JingleTransport.java +++ b/src/eu/siacs/conversations/xmpp/jingle/JingleTransport.java @@ -1,9 +1,77 @@ package eu.siacs.conversations.xmpp.jingle; -import eu.siacs.conversations.xml.Element; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.Cipher; +import javax.crypto.CipherOutputStream; +import javax.crypto.CipherInputStream; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.IvParameterSpec; + +import android.util.Log; public abstract class JingleTransport { public abstract void connect(final OnTransportConnected callback); public abstract void receive(final JingleFile file, final OnFileTransmitted callback); public abstract void send(final JingleFile file, final OnFileTransmitted callback); + private byte[] iv = {0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0xf}; + + protected InputStream getInputStream(JingleFile file) throws FileNotFoundException { + if (file.getKey() == null) { + return new FileInputStream(file); + } else { + try { + IvParameterSpec ips = new IvParameterSpec(iv); + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + cipher.init(Cipher.ENCRYPT_MODE, file.getKey(),ips); + Log.d("xmppService","opening encrypted input stream"); + return new CipherInputStream(new FileInputStream(file), cipher); + } catch (NoSuchAlgorithmException e) { + Log.d("xmppService","no such algo: "+e.getMessage()); + return null; + } catch (NoSuchPaddingException e) { + Log.d("xmppService","no such padding: "+e.getMessage()); + return null; + } catch (InvalidKeyException e) { + Log.d("xmppService","invalid key: "+e.getMessage()); + return null; + } catch (InvalidAlgorithmParameterException e) { + Log.d("xmppService","invavid iv:"+e.getMessage()); + return null; + } + } + } + + protected OutputStream getOutputStream(JingleFile file) throws FileNotFoundException { + if (file.getKey() == null) { + return new FileOutputStream(file); + } else { + try { + IvParameterSpec ips = new IvParameterSpec(iv); + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + cipher.init(Cipher.DECRYPT_MODE, file.getKey(),ips); + Log.d("xmppService","opening encrypted output stream"); + return new CipherOutputStream(new FileOutputStream(file), cipher); + } catch (NoSuchAlgorithmException e) { + Log.d("xmppService","no such algo: "+e.getMessage()); + return null; + } catch (NoSuchPaddingException e) { + Log.d("xmppService","no such padding: "+e.getMessage()); + return null; + } catch (InvalidKeyException e) { + Log.d("xmppService","invalid key: "+e.getMessage()); + return null; + } catch (InvalidAlgorithmParameterException e) { + Log.d("xmppService","invavid iv:"+e.getMessage()); + return null; + } + } + } } diff --git a/src/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java b/src/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java index abede91a..494ff0d6 100644 --- a/src/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java +++ b/src/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java @@ -25,12 +25,16 @@ public class Content extends Element { this.transportId = sid; } - public void setFileOffer(JingleFile actualFile) { + public void setFileOffer(JingleFile actualFile, boolean otr) { Element description = this.addChild("description", "urn:xmpp:jingle:apps:file-transfer:3"); Element offer = description.addChild("offer"); Element file = offer.addChild("file"); file.addChild("size").setContent(""+actualFile.getSize()); - file.addChild("name").setContent(actualFile.getName()); + if (otr) { + file.addChild("name").setContent(actualFile.getName()+".otr"); + } else { + file.addChild("name").setContent(actualFile.getName()); + } } public Element getFileOffer() { diff --git a/src/eu/siacs/conversations/xmpp/stanzas/IqPacket.java b/src/eu/siacs/conversations/xmpp/stanzas/IqPacket.java index 48e5c0ec..1d4e44d1 100644 --- a/src/eu/siacs/conversations/xmpp/stanzas/IqPacket.java +++ b/src/eu/siacs/conversations/xmpp/stanzas/IqPacket.java @@ -69,7 +69,6 @@ public class IqPacket extends AbstractStanza { public IqPacket generateRespone(int type) { IqPacket packet = new IqPacket(type); - packet.setFrom(this.getTo()); packet.setTo(this.getFrom()); packet.setId(this.getId()); return packet; diff --git a/src/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java b/src/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java index 941bda4f..64a9edc3 100644 --- a/src/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java +++ b/src/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java @@ -37,6 +37,10 @@ public class MessagePacket extends AbstractStanza { case TYPE_GROUPCHAT: this.setAttribute("type", "groupchat"); break; + case TYPE_UNKNOWN: + break; + case TYPE_NORMAL: + break; default: this.setAttribute("type","chat"); break; |