From acf80bddd07d5579cdb20a888b3f9e99e53030a5 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 28 Feb 2014 18:46:01 +0100 Subject: rebranding --- src/eu/siacs/conversations/crypto/OtrEngine.java | 232 +++++ src/eu/siacs/conversations/crypto/PgpEngine.java | 148 ++++ .../conversations/entities/AbstractEntity.java | 25 + src/eu/siacs/conversations/entities/Account.java | 221 +++++ src/eu/siacs/conversations/entities/Contact.java | 296 +++++++ .../siacs/conversations/entities/Conversation.java | 284 +++++++ src/eu/siacs/conversations/entities/Message.java | 144 ++++ .../siacs/conversations/entities/MucOptions.java | 5 + src/eu/siacs/conversations/entities/Presences.java | 76 ++ .../conversations/persistance/DatabaseBackend.java | 284 +++++++ .../persistance/OnPhoneContactsMerged.java | 5 + .../services/XmppConnectionService.java | 946 +++++++++++++++++++++ .../conversations/ui/ConversationActivity.java | 481 +++++++++++ .../conversations/ui/ConversationFragment.java | 602 +++++++++++++ .../conversations/ui/DialogContactDetails.java | 218 +++++ src/eu/siacs/conversations/ui/EditAccount.java | 138 +++ .../conversations/ui/ManageAccountActivity.java | 312 +++++++ .../conversations/ui/NewConversationActivity.java | 332 ++++++++ .../ui/OnAccountListChangedListener.java | 5 + .../ui/OnConversationListChangedListener.java | 5 + .../conversations/ui/OnRosterFetchedListener.java | 9 + .../siacs/conversations/ui/SettingsActivity.java | 16 + .../siacs/conversations/ui/SettingsFragment.java | 15 + src/eu/siacs/conversations/ui/XmppActivity.java | 52 ++ src/eu/siacs/conversations/utils/DNSHelper.java | 93 ++ .../siacs/conversations/utils/MessageParser.java | 149 ++++ .../utils/OnPhoneContactsLoadedListener.java | 9 + src/eu/siacs/conversations/utils/PhoneHelper.java | 87 ++ src/eu/siacs/conversations/utils/SASL.java | 24 + src/eu/siacs/conversations/utils/UIHelper.java | 210 +++++ src/eu/siacs/conversations/utils/Validator.java | 14 + src/eu/siacs/conversations/xml/Element.java | 101 +++ src/eu/siacs/conversations/xml/Tag.java | 99 +++ src/eu/siacs/conversations/xml/TagWriter.java | 64 ++ src/eu/siacs/conversations/xml/XmlReader.java | 102 +++ src/eu/siacs/conversations/xmpp/IqPacket.java | 40 + src/eu/siacs/conversations/xmpp/MessagePacket.java | 81 ++ .../conversations/xmpp/OnIqPacketReceived.java | 7 + .../xmpp/OnMessagePacketReceived.java | 7 + .../xmpp/OnPresencePacketReceived.java | 7 + .../siacs/conversations/xmpp/OnStatusChanged.java | 7 + .../siacs/conversations/xmpp/PresencePacket.java | 13 + .../siacs/conversations/xmpp/XmppConnection.java | 446 ++++++++++ 43 files changed, 6411 insertions(+) create mode 100644 src/eu/siacs/conversations/crypto/OtrEngine.java create mode 100644 src/eu/siacs/conversations/crypto/PgpEngine.java create mode 100644 src/eu/siacs/conversations/entities/AbstractEntity.java create mode 100644 src/eu/siacs/conversations/entities/Account.java create mode 100644 src/eu/siacs/conversations/entities/Contact.java create mode 100644 src/eu/siacs/conversations/entities/Conversation.java create mode 100644 src/eu/siacs/conversations/entities/Message.java create mode 100644 src/eu/siacs/conversations/entities/MucOptions.java create mode 100644 src/eu/siacs/conversations/entities/Presences.java create mode 100644 src/eu/siacs/conversations/persistance/DatabaseBackend.java create mode 100644 src/eu/siacs/conversations/persistance/OnPhoneContactsMerged.java create mode 100644 src/eu/siacs/conversations/services/XmppConnectionService.java create mode 100644 src/eu/siacs/conversations/ui/ConversationActivity.java create mode 100644 src/eu/siacs/conversations/ui/ConversationFragment.java create mode 100644 src/eu/siacs/conversations/ui/DialogContactDetails.java create mode 100644 src/eu/siacs/conversations/ui/EditAccount.java create mode 100644 src/eu/siacs/conversations/ui/ManageAccountActivity.java create mode 100644 src/eu/siacs/conversations/ui/NewConversationActivity.java create mode 100644 src/eu/siacs/conversations/ui/OnAccountListChangedListener.java create mode 100644 src/eu/siacs/conversations/ui/OnConversationListChangedListener.java create mode 100644 src/eu/siacs/conversations/ui/OnRosterFetchedListener.java create mode 100644 src/eu/siacs/conversations/ui/SettingsActivity.java create mode 100644 src/eu/siacs/conversations/ui/SettingsFragment.java create mode 100644 src/eu/siacs/conversations/ui/XmppActivity.java create mode 100644 src/eu/siacs/conversations/utils/DNSHelper.java create mode 100644 src/eu/siacs/conversations/utils/MessageParser.java create mode 100644 src/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java create mode 100644 src/eu/siacs/conversations/utils/PhoneHelper.java create mode 100644 src/eu/siacs/conversations/utils/SASL.java create mode 100644 src/eu/siacs/conversations/utils/UIHelper.java create mode 100644 src/eu/siacs/conversations/utils/Validator.java create mode 100644 src/eu/siacs/conversations/xml/Element.java create mode 100644 src/eu/siacs/conversations/xml/Tag.java create mode 100644 src/eu/siacs/conversations/xml/TagWriter.java create mode 100644 src/eu/siacs/conversations/xml/XmlReader.java create mode 100644 src/eu/siacs/conversations/xmpp/IqPacket.java create mode 100644 src/eu/siacs/conversations/xmpp/MessagePacket.java create mode 100644 src/eu/siacs/conversations/xmpp/OnIqPacketReceived.java create mode 100644 src/eu/siacs/conversations/xmpp/OnMessagePacketReceived.java create mode 100644 src/eu/siacs/conversations/xmpp/OnPresencePacketReceived.java create mode 100644 src/eu/siacs/conversations/xmpp/OnStatusChanged.java create mode 100644 src/eu/siacs/conversations/xmpp/PresencePacket.java create mode 100644 src/eu/siacs/conversations/xmpp/XmppConnection.java (limited to 'src/eu/siacs/conversations') diff --git a/src/eu/siacs/conversations/crypto/OtrEngine.java b/src/eu/siacs/conversations/crypto/OtrEngine.java new file mode 100644 index 00000000..eca01a73 --- /dev/null +++ b/src/eu/siacs/conversations/crypto/OtrEngine.java @@ -0,0 +1,232 @@ +package eu.siacs.conversations.crypto; + +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.DSAPrivateKeySpec; +import java.security.spec.DSAPublicKeySpec; +import java.security.spec.InvalidKeySpecException; + +import org.json.JSONException; +import org.json.JSONObject; + +import android.content.Context; +import android.util.Log; + +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.persistance.DatabaseBackend; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.MessagePacket; + +import net.java.otr4j.OtrEngineHost; +import net.java.otr4j.OtrException; +import net.java.otr4j.OtrPolicy; +import net.java.otr4j.OtrPolicyImpl; +import net.java.otr4j.session.InstanceTag; +import net.java.otr4j.session.SessionID; + +public class OtrEngine implements OtrEngineHost { + + private static final String LOGTAG = "xmppService"; + + private Account account; + private OtrPolicy otrPolicy; + private KeyPair keyPair; + private Context context; + + public OtrEngine(Context context, Account account) { + this.account = account; + this.otrPolicy = new OtrPolicyImpl(); + this.otrPolicy.setAllowV1(false); + this.otrPolicy.setAllowV2(true); + this.otrPolicy.setAllowV3(true); + this.keyPair = loadKey(account.getKeys()); + } + + private KeyPair loadKey(JSONObject keys) { + if (keys == null) { + return null; + } + try { + BigInteger x = new BigInteger(keys.getString("otr_x"),16); + BigInteger y = new BigInteger(keys.getString("otr_y"),16); + BigInteger p = new BigInteger(keys.getString("otr_p"),16); + BigInteger q = new BigInteger(keys.getString("otr_q"),16); + BigInteger g = new BigInteger(keys.getString("otr_g"),16); + KeyFactory keyFactory = KeyFactory.getInstance("DSA"); + DSAPublicKeySpec pubKeySpec = new DSAPublicKeySpec(y, p, q, g); + DSAPrivateKeySpec privateKeySpec = new DSAPrivateKeySpec(x, p, q, g); + PublicKey publicKey = keyFactory.generatePublic(pubKeySpec); + PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec); + return new KeyPair(publicKey, privateKey); + } catch (JSONException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (NoSuchAlgorithmException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (InvalidKeySpecException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return null; + } + + private void saveKey() { + PublicKey publicKey = keyPair.getPublic(); + PrivateKey privateKey = keyPair.getPrivate(); + KeyFactory keyFactory; + try { + keyFactory = KeyFactory.getInstance("DSA"); + DSAPrivateKeySpec privateKeySpec = keyFactory.getKeySpec(privateKey, DSAPrivateKeySpec.class); + DSAPublicKeySpec publicKeySpec = keyFactory.getKeySpec(publicKey, DSAPublicKeySpec.class); + this.account.setKey("otr_x",privateKeySpec.getX().toString(16)); + this.account.setKey("otr_g",privateKeySpec.getG().toString(16)); + this.account.setKey("otr_p",privateKeySpec.getP().toString(16)); + this.account.setKey("otr_q",privateKeySpec.getQ().toString(16)); + this.account.setKey("otr_y",publicKeySpec.getY().toString(16)); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } catch (InvalidKeySpecException e) { + e.printStackTrace(); + } + + } + + @Override + public void askForSecret(SessionID arg0, InstanceTag arg1, String arg2) { + // TODO Auto-generated method stub + + } + + @Override + public void finishedSessionMessage(SessionID arg0, String arg1) + throws OtrException { + // TODO Auto-generated method stub + + } + + @Override + public String getFallbackMessage(SessionID arg0) { + // TODO Auto-generated method stub + return null; + } + + @Override + public byte[] getLocalFingerprintRaw(SessionID arg0) { + // TODO Auto-generated method stub + return null; + } + + public PublicKey getPublicKey() { + return this.keyPair.getPublic(); + } + + @Override + public KeyPair getLocalKeyPair(SessionID arg0) throws OtrException { + if (this.keyPair==null) { + KeyPairGenerator kg; + try { + kg = KeyPairGenerator.getInstance("DSA"); + this.keyPair = kg.genKeyPair(); + this.saveKey(); + DatabaseBackend.getInstance(context).updateAccount(account); + } catch (NoSuchAlgorithmException e) { + Log.d(LOGTAG,"error generating key pair "+e.getMessage()); + } + } + return this.keyPair; + } + + @Override + public String getReplyForUnreadableMessage(SessionID arg0) { + // TODO Auto-generated method stub + return null; + } + + @Override + public OtrPolicy getSessionPolicy(SessionID arg0) { + return otrPolicy; + } + + @Override + public void injectMessage(SessionID session, String body) throws OtrException { + MessagePacket packet = new MessagePacket(); + packet.setFrom(account.getFullJid()); //sender + packet.setTo(session.getAccountID()+"/"+session.getUserID()); //reciepient + packet.setBody(body); + Element privateTag = new Element("private"); + privateTag.setAttribute("xmlns","urn:xmpp:carbons:2"); + packet.addChild(privateTag); + packet.setType(MessagePacket.TYPE_CHAT); + account.getXmppConnection().sendMessagePacket(packet); + } + + @Override + public void messageFromAnotherInstanceReceived(SessionID arg0) { + // TODO Auto-generated method stub + + } + + @Override + public void multipleInstancesDetected(SessionID arg0) { + // TODO Auto-generated method stub + + } + + @Override + public void requireEncryptedMessage(SessionID arg0, String arg1) + throws OtrException { + // TODO Auto-generated method stub + + } + + @Override + public void showError(SessionID arg0, String arg1) throws OtrException { + // TODO Auto-generated method stub + + } + + @Override + public void smpAborted(SessionID arg0) throws OtrException { + // TODO Auto-generated method stub + + } + + @Override + public void smpError(SessionID arg0, int arg1, boolean arg2) + throws OtrException { + // TODO Auto-generated method stub + + } + + @Override + public void unencryptedMessageReceived(SessionID arg0, String arg1) + throws OtrException { + // TODO Auto-generated method stub + + } + + @Override + public void unreadableMessageReceived(SessionID arg0) throws OtrException { + // TODO Auto-generated method stub + + } + + @Override + public void unverify(SessionID arg0, String arg1) { + // TODO Auto-generated method stub + + } + + @Override + public void verify(SessionID arg0, String arg1, boolean arg2) { + // TODO Auto-generated method stub + + } + +} diff --git a/src/eu/siacs/conversations/crypto/PgpEngine.java b/src/eu/siacs/conversations/crypto/PgpEngine.java new file mode 100644 index 00000000..ba000c04 --- /dev/null +++ b/src/eu/siacs/conversations/crypto/PgpEngine.java @@ -0,0 +1,148 @@ +package eu.siacs.conversations.crypto; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; + +import org.openintents.openpgp.OpenPgpError; +import org.openintents.openpgp.OpenPgpSignatureResult; +import org.openintents.openpgp.util.OpenPgpApi; +import org.openintents.openpgp.util.OpenPgpConstants; + +import android.app.PendingIntent; +import android.os.Bundle; +import android.util.Log; + +public class PgpEngine { + private OpenPgpApi api; + + public PgpEngine(OpenPgpApi api) { + this.api = api; + } + + public String decrypt(String message) throws UserInputRequiredException, + OpenPgpException { + InputStream is = new ByteArrayInputStream(message.getBytes()); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + Bundle result = api.decryptAndVerify(is, os); + switch (result.getInt(OpenPgpConstants.RESULT_CODE)) { + case OpenPgpConstants.RESULT_CODE_SUCCESS: + return os.toString(); + case OpenPgpConstants.RESULT_CODE_USER_INTERACTION_REQUIRED: + throw new UserInputRequiredException( + (PendingIntent) result + .getParcelable(OpenPgpConstants.RESULT_INTENT)); + case OpenPgpConstants.RESULT_CODE_ERROR: + throw new OpenPgpException( + (OpenPgpError) result + .getParcelable(OpenPgpConstants.RESULT_ERRORS)); + default: + return null; + } + } + + public String encrypt(long keyId, String message) { + Bundle params = new Bundle(); + params.putBoolean(OpenPgpConstants.PARAMS_REQUEST_ASCII_ARMOR, true); + long[] keyIds = { keyId }; + params.putLongArray(OpenPgpConstants.PARAMS_KEY_IDS, keyIds); + + InputStream is = new ByteArrayInputStream(message.getBytes()); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + Bundle result = api.encrypt(params, is, os); + StringBuilder encryptedMessageBody = new StringBuilder(); + String[] lines = os.toString().split("\n"); + for (int i = 3; i < lines.length - 1; ++i) { + encryptedMessageBody.append(lines[i].trim()); + } + return encryptedMessageBody.toString(); + } + + public long fetchKeyId(String status, String signature) + throws OpenPgpException { + StringBuilder pgpSig = new StringBuilder(); + pgpSig.append("-----BEGIN PGP SIGNED MESSAGE-----"); + pgpSig.append('\n'); + pgpSig.append("Hash: SHA1"); + pgpSig.append('\n'); + pgpSig.append('\n'); + pgpSig.append(status); + pgpSig.append('\n'); + pgpSig.append("-----BEGIN PGP SIGNATURE-----"); + pgpSig.append('\n'); + pgpSig.append('\n'); + pgpSig.append(signature.replace("\n", "").trim()); + pgpSig.append('\n'); + pgpSig.append("-----END PGP SIGNATURE-----"); + Bundle params = new Bundle(); + params.putBoolean(OpenPgpConstants.PARAMS_REQUEST_ASCII_ARMOR, true); + InputStream is = new ByteArrayInputStream(pgpSig.toString().getBytes()); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + Bundle result = api.decryptAndVerify(params, is, os); + switch (result.getInt(OpenPgpConstants.RESULT_CODE)) { + case OpenPgpConstants.RESULT_CODE_SUCCESS: + OpenPgpSignatureResult sigResult = result + .getParcelable(OpenPgpConstants.RESULT_SIGNATURE); + return sigResult.getKeyId(); + case OpenPgpConstants.RESULT_CODE_USER_INTERACTION_REQUIRED: + break; + case OpenPgpConstants.RESULT_CODE_ERROR: + throw new OpenPgpException( + (OpenPgpError) result + .getParcelable(OpenPgpConstants.RESULT_ERRORS)); + } + return 0; + } + + public String generateSignature(String status) + throws UserInputRequiredException { + Bundle params = new Bundle(); + params.putBoolean(OpenPgpConstants.PARAMS_REQUEST_ASCII_ARMOR, true); + InputStream is = new ByteArrayInputStream(status.getBytes()); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + Bundle result = api.sign(params, is, os); + StringBuilder signatureBuilder = new StringBuilder(); + switch (result.getInt(OpenPgpConstants.RESULT_CODE)) { + case OpenPgpConstants.RESULT_CODE_SUCCESS: + String[] lines = os.toString().split("\n"); + for (int i = 7; i < lines.length - 1; ++i) { + signatureBuilder.append(lines[i].trim()); + } + break; + case OpenPgpConstants.RESULT_CODE_USER_INTERACTION_REQUIRED: + UserInputRequiredException exception = new UserInputRequiredException( + (PendingIntent) result + .getParcelable(OpenPgpConstants.RESULT_INTENT)); + throw exception; + case OpenPgpConstants.RESULT_CODE_ERROR: + break; + } + return signatureBuilder.toString(); + } + + public class UserInputRequiredException extends Exception { + private static final long serialVersionUID = -6913480043269132016L; + private PendingIntent pi; + + public UserInputRequiredException(PendingIntent pi) { + this.pi = pi; + } + + public PendingIntent getPendingIntent() { + return this.pi; + } + } + + public class OpenPgpException extends Exception { + private static final long serialVersionUID = -7324789703473056077L; + private OpenPgpError error; + + public OpenPgpException(OpenPgpError openPgpError) { + this.error = openPgpError; + } + + public OpenPgpError getOpenPgpError() { + return this.error; + } + } +} diff --git a/src/eu/siacs/conversations/entities/AbstractEntity.java b/src/eu/siacs/conversations/entities/AbstractEntity.java new file mode 100644 index 00000000..0297fa66 --- /dev/null +++ b/src/eu/siacs/conversations/entities/AbstractEntity.java @@ -0,0 +1,25 @@ +package eu.siacs.conversations.entities; + +import java.io.Serializable; + +import android.content.ContentValues; + +public abstract class AbstractEntity implements Serializable { + + private static final long serialVersionUID = -1895605706690653719L; + + public static final String UUID = "uuid"; + + protected String uuid; + + public String getUuid() { + return this.uuid; + } + + public abstract ContentValues getContentValues(); + + public boolean equals(AbstractEntity entity) { + return this.getUuid().equals(entity.getUuid()); + } + +} diff --git a/src/eu/siacs/conversations/entities/Account.java b/src/eu/siacs/conversations/entities/Account.java new file mode 100644 index 00000000..a596aba1 --- /dev/null +++ b/src/eu/siacs/conversations/entities/Account.java @@ -0,0 +1,221 @@ +package eu.siacs.conversations.entities; + +import java.security.interfaces.DSAPublicKey; + +import net.java.otr4j.crypto.OtrCryptoEngineImpl; +import net.java.otr4j.crypto.OtrCryptoException; + +import org.json.JSONException; +import org.json.JSONObject; + +import eu.siacs.conversations.crypto.OtrEngine; +import eu.siacs.conversations.xmpp.XmppConnection; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.util.JsonReader; +import android.util.Log; + +public class Account extends AbstractEntity{ + + private static final long serialVersionUID = 6174825093869578035L; + + public static final String TABLENAME = "accounts"; + + public static final String USERNAME = "username"; + public static final String SERVER = "server"; + public static final String PASSWORD = "password"; + public static final String OPTIONS = "options"; + public static final String ROSTERVERSION = "rosterversion"; + public static final String KEYS = "keys"; + + public static final int OPTION_USETLS = 0; + public static final int OPTION_DISABLED = 1; + + public static final int STATUS_DISABLED = -1; + public static final int STATUS_OFFLINE = 0; + public static final int STATUS_ONLINE = 1; + public static final int STATUS_UNAUTHORIZED = 2; + public static final int STATUS_NOINTERNET = 3; + public static final int STATUS_TLS_ERROR = 4; + public static final int STATUS_SERVER_NOT_FOUND = 5; + + protected String username; + protected String server; + protected String password; + protected int options = 0; + protected String rosterVersion; + protected String resource; + protected int status = 0; + protected JSONObject keys = new JSONObject(); + + protected boolean online = false; + + transient OtrEngine otrEngine = null; + transient XmppConnection xmppConnection = null; + + private String otrFingerprint; + + public Account() { + this.uuid = "0"; + } + + public Account(String username, String server, String password) { + this(java.util.UUID.randomUUID().toString(),username,server,password,0,null,""); + } + public Account(String uuid, String username, String server,String password, int options, String rosterVersion, String keys) { + this.uuid = uuid; + this.username = username; + this.server = server; + this.password = password; + this.options = options; + this.rosterVersion = rosterVersion; + try { + this.keys = new JSONObject(keys); + } catch (JSONException e) { + + } + } + + public boolean isOptionSet(int option) { + return ((options & (1 << option)) != 0); + } + + public void setOption(int option, boolean value) { + if (value) { + this.options |= 1 << option; + } else { + this.options &= ~(1 << option); + } + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getServer() { + return server; + } + + public void setServer(String server) { + this.server = server; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public void setStatus(int status) { + this.status = status; + } + + public int getStatus() { + if (isOptionSet(OPTION_DISABLED)) { + return STATUS_DISABLED; + } else { + return this.status; + } + } + + public void setResource(String resource) { + this.resource = resource; + } + + public String getJid() { + return username+"@"+server; + } + + public JSONObject getKeys() { + return keys; + } + + public boolean setKey(String keyName, String keyValue) { + try { + this.keys.put(keyName, keyValue); + return true; + } catch (JSONException e) { + return false; + } + } + + @Override + public ContentValues getContentValues() { + ContentValues values = new ContentValues(); + values.put(UUID,uuid); + values.put(USERNAME, username); + values.put(SERVER, server); + values.put(PASSWORD, password); + values.put(OPTIONS,options); + values.put(KEYS,this.keys.toString()); + values.put(ROSTERVERSION,rosterVersion); + return values; + } + + public static Account fromCursor(Cursor cursor) { + return new Account(cursor.getString(cursor.getColumnIndex(UUID)), + cursor.getString(cursor.getColumnIndex(USERNAME)), + cursor.getString(cursor.getColumnIndex(SERVER)), + cursor.getString(cursor.getColumnIndex(PASSWORD)), + cursor.getInt(cursor.getColumnIndex(OPTIONS)), + cursor.getString(cursor.getColumnIndex(ROSTERVERSION)), + cursor.getString(cursor.getColumnIndex(KEYS)) + ); + } + + + public OtrEngine getOtrEngine(Context context) { + if (otrEngine==null) { + otrEngine = new OtrEngine(context,this); + } + return this.otrEngine; + } + + public XmppConnection getXmppConnection() { + return this.xmppConnection; + } + + public void setXmppConnection(XmppConnection connection) { + this.xmppConnection = connection; + } + + public String getFullJid() { + return this.getJid()+"/"+this.resource; + } + + public String getOtrFingerprint() { + if (this.otrFingerprint == null) { + try { + DSAPublicKey pubkey = (DSAPublicKey) this.otrEngine.getPublicKey(); + StringBuilder builder = new StringBuilder(new OtrCryptoEngineImpl().getFingerprint(pubkey)); + builder.insert(8, " "); + builder.insert(17, " "); + builder.insert(26, " "); + builder.insert(35, " "); + this.otrFingerprint = builder.toString(); + } catch (OtrCryptoException e) { + + } + } + return this.otrFingerprint; + } + + public String getRosterVersion() { + if (this.rosterVersion==null) { + return ""; + } else { + return this.rosterVersion; + } + } + + public void setRosterVersion(String version) { + this.rosterVersion = version; + } +} diff --git a/src/eu/siacs/conversations/entities/Contact.java b/src/eu/siacs/conversations/entities/Contact.java new file mode 100644 index 00000000..0eed39ed --- /dev/null +++ b/src/eu/siacs/conversations/entities/Contact.java @@ -0,0 +1,296 @@ +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; +import org.json.JSONException; +import org.json.JSONObject; + +import eu.siacs.conversations.xml.Element; + +import android.content.ContentValues; +import android.database.Cursor; + +public class Contact extends AbstractEntity implements Serializable { + private static final long serialVersionUID = -4570817093119419962L; + + public static final String TABLENAME = "contacts"; + + public static final String DISPLAYNAME = "name"; + public static final String JID = "jid"; + public static final String SUBSCRIPTION = "subscription"; + 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 jid; + protected int subscription = 0; + protected String systemAccount; + protected String photoUri; + protected JSONObject keys = new JSONObject(); + protected Presences presences = new Presences(); + + protected Account account; + + protected boolean inRoster = true; + + 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; + this.accountUuid = account; + this.displayName = displayName; + this.jid = jid; + this.subscription = subscription; + this.photoUri = photoUri; + this.systemAccount = systemAccount; + if (keys == null) { + keys = ""; + } + try { + this.keys = new JSONObject(keys); + } catch (JSONException e) { + this.keys = new JSONObject(); + } + this.presences = Presences.fromJsonString(presences); + } + + public String getDisplayName() { + return this.displayName; + } + + public String getProfilePhoto() { + return this.photoUri; + } + + public String getJid() { + return this.jid; + } + + public boolean match(String needle) { + return (jid.toLowerCase().contains(needle.toLowerCase()) || (displayName + .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(JID, jid); + values.put(SUBSCRIPTION, 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)), + cursor.getString(cursor.getColumnIndex(JID)), + cursor.getInt(cursor.getColumnIndex(SUBSCRIPTION)), + cursor.getString(cursor.getColumnIndex(PHOTOURI)), + cursor.getString(cursor.getColumnIndex(SYSTEMACCOUNT)), + cursor.getString(cursor.getColumnIndex(KEYS)), + cursor.getString(cursor.getColumnIndex(PRESENCES))); + } + + public int getSubscription() { + return this.subscription; + } + + public void setSystemAccount(String account) { + this.systemAccount = account; + } + + public void setAccount(Account account) { + this.account = account; + this.accountUuid = account.getUuid(); + } + + public Account getAccount() { + return this.account; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public boolean couldBeMuc() { + String[] split = this.getJid().split("@"); + if (split.length != 2) { + return false; + } else { + String[] domainParts = split[1].split("\\."); + if (domainParts.length < 3) { + return false; + } else { + return (domainParts[0].equals("conf") + || domainParts[0].equals("conference") || domainParts[0] + .equals("muc")); + } + } + } + + public Hashtable getPresences() { + return this.presences.getPresences(); + } + + public void updatePresence(String resource, int status) { + this.presences.updatePresence(resource, status); + } + + public void removePresence(String resource) { + this.presences.removePresence(resource); + } + + public int getMostAvailableStatus() { + return this.presences.getMostAvailableStatus(); + } + + public void setPresences(Presences pres) { + this.presences = pres; + } + + public void setPhotoUri(String uri) { + this.photoUri = uri; + } + + public void setDisplayName(String name) { + this.displayName = name; + } + + public String getSystemAccount() { + return systemAccount; + } + + public Set getOtrFingerprints() { + Set set = new HashSet(); + try { + if (this.keys.has("otr_fingerprints")) { + JSONArray fingerprints = this.keys.getJSONArray("otr_fingerprints"); + for (int i = 0; i < fingerprints.length(); ++i) { + set.add(fingerprints.getString(i)); + } + } + } catch (JSONException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return set; + } + + public void addOtrFingerprint(String print) { + try { + JSONArray fingerprints; + if (!this.keys.has("otr_fingerprints")) { + fingerprints = new JSONArray(); + + } else { + fingerprints = this.keys.getJSONArray("otr_fingerprints"); + } + fingerprints.put(print); + this.keys.put("otr_fingerprints", fingerprints); + } catch (JSONException e) { + + } + } + + public void setPgpKeyId(long keyId) { + try { + this.keys.put("pgp_keyid", keyId); + } catch (JSONException e) { + + } + } + + public long getPgpKeyId() { + if (this.keys.has("pgp_keyid")) { + try { + return this.keys.getLong("pgp_keyid"); + } catch (JSONException e) { + return 0; + } + } else { + return 0; + } + } + + public void setSubscriptionOption(int option) { + this.subscription |= 1 << option; + } + + public void resetSubscriptionOption(int option) { + this.subscription &= ~(1 << option); + } + + public boolean getSubscriptionOption(int option) { + return ((this.subscription & (1 << option)) != 0); + } + + 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); + } else if (subscription.equals("from")) { + this.resetSubscriptionOption(Contact.Subscription.TO); + this.setSubscriptionOption(Contact.Subscription.FROM); + } else if (subscription.equals("both")) { + this.setSubscriptionOption(Contact.Subscription.TO); + this.setSubscriptionOption(Contact.Subscription.FROM); + } + } + + if ((ask!=null)&&(ask.equals("subscribe"))) { + this.setSubscriptionOption(Contact.Subscription.ASKING); + } else { + this.resetSubscriptionOption(Contact.Subscription.ASKING); + } + } + + + public class Subscription { + 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; + } +} diff --git a/src/eu/siacs/conversations/entities/Conversation.java b/src/eu/siacs/conversations/entities/Conversation.java new file mode 100644 index 00000000..d1186a7d --- /dev/null +++ b/src/eu/siacs/conversations/entities/Conversation.java @@ -0,0 +1,284 @@ +package eu.siacs.conversations.entities; + +import java.security.interfaces.DSAPublicKey; +import java.util.ArrayList; +import java.util.List; + +import eu.siacs.conversations.crypto.OtrEngine; +import eu.siacs.conversations.xmpp.XmppConnection; + +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; +import android.net.Uri; +import android.util.Log; + +public class Conversation extends AbstractEntity { + + private static final long serialVersionUID = -6727528868973996739L; + + public static final String TABLENAME = "conversations"; + + public static final int STATUS_AVAILABLE = 0; + public static final int STATUS_ARCHIVED = 1; + public static final int STATUS_DELETED = 2; + + public static final int MODE_MULTI = 1; + public static final int MODE_SINGLE = 0; + + public static final String NAME = "name"; + public static final String ACCOUNT = "accountUuid"; + public static final String CONTACT = "contactUuid"; + public static final String CONTACTJID = "contactJid"; + public static final String STATUS = "status"; + public static final String CREATED = "created"; + public static final String MODE = "mode"; + + private String name; + private String contactUuid; + private String accountUuid; + private String contactJid; + private int status; + private long created; + private int mode; + + private transient List messages = null; + private transient Account account = null; + private transient Contact contact; + + private transient SessionImpl otrSession; + + private transient String otrFingerprint = null; + + public int nextMessageEncryption = Message.ENCRYPTION_NONE; + + private transient MucOptions mucOptions = null; + + public Conversation(String name, Account account, + String contactJid, int mode) { + this(java.util.UUID.randomUUID().toString(), name, null, account.getUuid(), contactJid, System + .currentTimeMillis(), STATUS_AVAILABLE,mode); + this.account = account; + } + + public Conversation(String uuid, String name, String contactUuid, + String accountUuid, String contactJid, long created, int status, int mode) { + this.uuid = uuid; + this.name = name; + this.contactUuid = contactUuid; + this.accountUuid = accountUuid; + this.contactJid = contactJid; + this.created = created; + this.status = status; + this.mode = mode; + } + + public List getMessages() { + if (messages == null) this.messages = new ArrayList(); //prevent null pointer + + //populate with Conversation (this) + + for(Message msg : messages) { + msg.setConversation(this); + } + + return messages; + } + + public boolean isRead() { + if ((this.messages == null)||(this.messages.size() == 0)) return true; + return this.messages.get(this.messages.size() - 1).isRead(); + } + + public void markRead() { + if (this.messages == null) return; + for(int i = this.messages.size() -1; i >= 0; --i) { + if (messages.get(i).isRead()) return; + this.messages.get(i).markRead(); + } + } + + public Message getLatestMessage() { + if ((this.messages == null)||(this.messages.size()==0)) { + Message message = new Message(this,"",Message.ENCRYPTION_NONE); + message.setTime(getCreated()); + return message; + } else { + return this.messages.get(this.messages.size() - 1); + } + } + + public void setMessages(List msgs) { + this.messages = msgs; + } + + public String getName() { + if (this.contact!=null) { + return this.contact.getDisplayName(); + } else { + return this.name; + } + } + + public String getProfilePhotoString() { + if (this.contact==null) { + return null; + } else { + return this.contact.getProfilePhoto(); + } + } + + public String getAccountUuid() { + return this.accountUuid; + } + + public Account getAccount() { + return this.account; + } + + public Contact getContact() { + return this.contact; + } + + public void setContact(Contact contact) { + this.contact = contact; + if (contact!=null) { + this.contactUuid = contact.getUuid(); + } + } + + public void setAccount(Account account) { + this.account = account; + } + + public String getContactJid() { + return this.contactJid; + } + + public Uri getProfilePhotoUri() { + if (this.getProfilePhotoString() != null) { + return Uri.parse(this.getProfilePhotoString()); + } + return null; + } + + public int getStatus() { + return this.status; + } + + public long getCreated() { + return this.created; + } + + public ContentValues getContentValues() { + ContentValues values = new ContentValues(); + values.put(UUID, uuid); + values.put(NAME, name); + values.put(CONTACT, contactUuid); + values.put(ACCOUNT, accountUuid); + values.put(CONTACTJID, contactJid); + values.put(CREATED, created); + values.put(STATUS, status); + values.put(MODE,mode); + return values; + } + + public static Conversation fromCursor(Cursor cursor) { + return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)), + cursor.getString(cursor.getColumnIndex(NAME)), + cursor.getString(cursor.getColumnIndex(CONTACT)), + cursor.getString(cursor.getColumnIndex(ACCOUNT)), + cursor.getString(cursor.getColumnIndex(CONTACTJID)), + cursor.getLong(cursor.getColumnIndex(CREATED)), + cursor.getInt(cursor.getColumnIndex(STATUS)), + cursor.getInt(cursor.getColumnIndex(MODE))); + } + + public void setStatus(int status) { + this.status = status; + } + + public int getMode() { + return this.mode; + } + + public void setMode(int mode) { + this.mode = mode; + } + + public void startOtrSession(Context context, String presence) { + Log.d("xmppService","starting otr session with "+presence); + SessionID sessionId = new SessionID(this.getContactJid(),presence,"xmpp"); + this.otrSession = new SessionImpl(sessionId, getAccount().getOtrEngine(context)); + try { + this.otrSession.startSession(); + } catch (OtrException e) { + Log.d("xmppServic","couldnt start otr"); + } + } + + public SessionImpl getOtrSession() { + return this.otrSession; + } + + public void resetOtrSession() { + this.otrSession = null; + } + + public void endOtrIfNeeded() throws OtrException { + if (this.otrSession!=null) { + if (this.otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) { + this.otrSession.endSession(); + } + } + 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; + } + } + + public String getOtrFingerprint() { + if (this.otrFingerprint == null) { + try { + DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession().getRemotePublicKey(); + StringBuilder builder = new StringBuilder(new OtrCryptoEngineImpl().getFingerprint(remotePubKey)); + builder.insert(8, " "); + builder.insert(17, " "); + builder.insert(26, " "); + builder.insert(35, " "); + this.otrFingerprint = builder.toString(); + } catch (OtrCryptoException e) { + + } + } + return this.otrFingerprint; + } + + public MucOptions getMucOptions() { + if (this.mucOptions == null) { + this.mucOptions = new MucOptions(); + } + return this.mucOptions ; + } + + public void resetMucOptions() { + this.mucOptions = null; + } +} diff --git a/src/eu/siacs/conversations/entities/Message.java b/src/eu/siacs/conversations/entities/Message.java new file mode 100644 index 00000000..0fce2a5b --- /dev/null +++ b/src/eu/siacs/conversations/entities/Message.java @@ -0,0 +1,144 @@ +package eu.siacs.conversations.entities; + +import android.content.ContentValues; +import android.database.Cursor; + +public class Message extends AbstractEntity { + + private static final long serialVersionUID = 7222081895167103025L; + + public static final String TABLENAME = "messages"; + + public static final int STATUS_RECIEVED = 0; + public static final int STATUS_UNSEND = 1; + public static final int STATUS_SEND = 2; + public static final int STATUS_ERROR = 3; + + public static final int ENCRYPTION_NONE = 0; + public static final int ENCRYPTION_PGP = 1; + public static final int ENCRYPTION_OTR = 2; + public static final int ENCRYPTION_DECRYPTED = 3; + + public static String CONVERSATION = "conversationUuid"; + public static String COUNTERPART = "counterpart"; + public static String BODY = "body"; + public static String TIME_SENT = "timeSent"; + public static String ENCRYPTION = "encryption"; + public static String STATUS = "status"; + + protected String conversationUuid; + protected String counterpart; + protected String body; + protected long timeSent; + protected int encryption; + protected int status; + protected boolean read = true; + + protected transient Conversation conversation = null; + + public Message(Conversation conversation, String body, int encryption) { + this(java.util.UUID.randomUUID().toString(), conversation.getUuid(), + conversation.getContactJid(), body, System.currentTimeMillis(), encryption, + Message.STATUS_UNSEND); + this.conversation = conversation; + } + + public Message(Conversation conversation, String counterpart, String body, int encryption, int status) { + this(java.util.UUID.randomUUID().toString(), conversation.getUuid(),counterpart, body, System.currentTimeMillis(), encryption,status); + this.conversation = conversation; + } + + public Message(String uuid, String conversationUUid, String counterpart, + String body, long timeSent, int encryption, int status) { + this.uuid = uuid; + this.conversationUuid = conversationUUid; + this.counterpart = counterpart; + this.body = body; + this.timeSent = timeSent; + this.encryption = encryption; + this.status = status; + } + + @Override + public ContentValues getContentValues() { + ContentValues values = new ContentValues(); + values.put(UUID, uuid); + values.put(CONVERSATION, conversationUuid); + values.put(COUNTERPART, counterpart); + values.put(BODY, body); + values.put(TIME_SENT, timeSent); + values.put(ENCRYPTION, encryption); + values.put(STATUS, status); + return values; + } + + public String getConversationUuid() { + return conversationUuid; + } + + public Conversation getConversation() { + return this.conversation; + } + + public String getCounterpart() { + return counterpart; + } + + public String getBody() { + return body; + } + + public long getTimeSent() { + return timeSent; + } + + public int getEncryption() { + return encryption; + } + + public int getStatus() { + return status; + } + + public static Message fromCursor(Cursor cursor) { + return new Message(cursor.getString(cursor.getColumnIndex(UUID)), + cursor.getString(cursor.getColumnIndex(CONVERSATION)), + cursor.getString(cursor.getColumnIndex(COUNTERPART)), + cursor.getString(cursor.getColumnIndex(BODY)), + cursor.getLong(cursor.getColumnIndex(TIME_SENT)), + cursor.getInt(cursor.getColumnIndex(ENCRYPTION)), + cursor.getInt(cursor.getColumnIndex(STATUS))); + } + + public void setConversation(Conversation conv) { + this.conversation = conv; + } + + public void setStatus(int status) { + this.status = status; + } + + public boolean isRead() { + return this.read; + } + + public void markRead() { + this.read = true; + } + + public void markUnread() { + this.read = false; + } + + public void setTime(long time) { + this.timeSent = time; + } + + public void setEncryption(int encryption) { + this.encryption = encryption; + } + + public void setBody(String body) { + this.body = body; + } +} diff --git a/src/eu/siacs/conversations/entities/MucOptions.java b/src/eu/siacs/conversations/entities/MucOptions.java new file mode 100644 index 00000000..4a738e65 --- /dev/null +++ b/src/eu/siacs/conversations/entities/MucOptions.java @@ -0,0 +1,5 @@ +package eu.siacs.conversations.entities; + +public class MucOptions { + +} diff --git a/src/eu/siacs/conversations/entities/Presences.java b/src/eu/siacs/conversations/entities/Presences.java new file mode 100644 index 00000000..af7926a8 --- /dev/null +++ b/src/eu/siacs/conversations/entities/Presences.java @@ -0,0 +1,76 @@ +package eu.siacs.conversations.entities; + +import java.util.Hashtable; +import java.util.Iterator; +import java.util.Map.Entry; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +public class Presences { + + public static final int CHAT = -1; + public static final int ONLINE = 0; + public static final int AWAY = 1; + public static final int XA = 2; + public static final int DND = 3; + public static final int OFFLINE = 4; + + private Hashtable presences = new Hashtable(); + + public Hashtable getPresences() { + return this.presences; + } + + public void updatePresence(String resource, int status) { + this.presences.put(resource, status); + } + + public void removePresence(String resource) { + this.presences.remove(resource); + } + + public int getMostAvailableStatus() { + int status = OFFLINE; + Iterator> it = presences.entrySet().iterator(); + while (it.hasNext()) { + Entry entry = it.next(); + if (entry.getValue()> it = presences.entrySet().iterator(); + + while (it.hasNext()) { + Entry 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; + } +} diff --git a/src/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/eu/siacs/conversations/persistance/DatabaseBackend.java new file mode 100644 index 00000000..1a6f934a --- /dev/null +++ b/src/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -0,0 +1,284 @@ +package eu.siacs.conversations.persistance; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +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 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 { + + private static DatabaseBackend instance = null; + + private static final String DATABASE_NAME = "history"; + private static final int DATABASE_VERSION = 1; + + public DatabaseBackend(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL("PRAGMA foreign_keys=ON;"); + db.execSQL("create table " + Account.TABLENAME + "(" + Account.UUID + + " TEXT PRIMARY KEY," + Account.USERNAME + " TEXT," + + Account.SERVER + " TEXT," + Account.PASSWORD + " TEXT," + + Account.ROSTERVERSION + " TEXT," + Account.OPTIONS + + " NUMBER, "+Account.KEYS+" TEXT)"); + db.execSQL("create table " + Conversation.TABLENAME + " (" + + Conversation.UUID + " TEXT PRIMARY KEY, " + Conversation.NAME + + " TEXT, " + Conversation.CONTACT + " TEXT, " + + Conversation.ACCOUNT + " TEXT, " + Conversation.CONTACTJID + + " TEXT, " + Conversation.CREATED + " NUMBER, " + + Conversation.STATUS + " NUMBER," + Conversation.MODE + + " NUMBER," + "FOREIGN KEY(" + Conversation.ACCOUNT + + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + + ") ON DELETE CASCADE);"); + db.execSQL("create table " + Message.TABLENAME + "( " + Message.UUID + + " TEXT PRIMARY KEY, " + Message.CONVERSATION + " TEXT, " + + Message.TIME_SENT + " NUMBER, " + Message.COUNTERPART + + " TEXT, " + Message.BODY + " TEXT, " + Message.ENCRYPTION + + " NUMBER, " + Message.STATUS + " 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);"); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int arg1, int arg2) { + // TODO Auto-generated method stub + + } + + public static synchronized DatabaseBackend getInstance(Context context) { + if (instance == null) { + instance = new DatabaseBackend(context); + } + return instance; + } + + public void createConversation(Conversation conversation) { + SQLiteDatabase db = this.getWritableDatabase(); + db.insert(Conversation.TABLENAME, null, conversation.getContentValues()); + } + + public void createMessage(Message message) { + SQLiteDatabase db = this.getWritableDatabase(); + db.insert(Message.TABLENAME, null, message.getContentValues()); + } + + public void createAccount(Account account) { + SQLiteDatabase db = this.getWritableDatabase(); + db.insert(Account.TABLENAME, null, account.getContentValues()); + } + + public void createContact(Contact contact) { + SQLiteDatabase db = this.getWritableDatabase(); + db.insert(Contact.TABLENAME, null, contact.getContentValues()); + } + + public int getConversationCount() { + SQLiteDatabase db = this.getReadableDatabase(); + Cursor cursor = db.rawQuery("select count(uuid) as count from " + + Conversation.TABLENAME + " where " + Conversation.STATUS + + "=" + Conversation.STATUS_AVAILABLE, null); + cursor.moveToFirst(); + return cursor.getInt(0); + } + + public List getConversations(int status) { + List list = new ArrayList(); + SQLiteDatabase db = this.getReadableDatabase(); + String[] selectionArgs = { "" + status }; + Cursor cursor = db.rawQuery("select * from " + Conversation.TABLENAME + + " where " + Conversation.STATUS + " = ? order by " + + Conversation.CREATED + " desc", selectionArgs); + while (cursor.moveToNext()) { + list.add(Conversation.fromCursor(cursor)); + } + return list; + } + + public List getMessages(Conversation conversation, int limit) { + List list = new ArrayList(); + 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)); + if (cursor.getCount() > 0) { + cursor.moveToLast(); + do { + list.add(Message.fromCursor(cursor)); + } while (cursor.moveToPrevious()); + } + return list; + } + + public Conversation findConversation(Account account, String contactJid) { + SQLiteDatabase db = this.getReadableDatabase(); + String[] selectionArgs = { account.getUuid(), contactJid }; + Cursor cursor = db.query(Conversation.TABLENAME, null, + Conversation.ACCOUNT + "=? AND " + Conversation.CONTACTJID + "=?", + selectionArgs, null, null, null); + if (cursor.getCount() == 0) + return null; + cursor.moveToFirst(); + return Conversation.fromCursor(cursor); + } + + public void updateConversation(Conversation conversation) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] args = { conversation.getUuid() }; + db.update(Conversation.TABLENAME, conversation.getContentValues(), + Conversation.UUID + "=?", args); + } + + public List getAccounts() { + List list = new ArrayList(); + SQLiteDatabase db = this.getReadableDatabase(); + Cursor cursor = db.query(Account.TABLENAME, null, null, null, null, + null, null); + Log.d("gultsch", "found " + cursor.getCount() + " accounts"); + while (cursor.moveToNext()) { + list.add(Account.fromCursor(cursor)); + } + return list; + } + + public void updateAccount(Account account) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] args = { account.getUuid() }; + db.update(Account.TABLENAME, account.getContentValues(), Account.UUID + + "=?", args); + } + + public void deleteAccount(Account account) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] args = { account.getUuid() }; + db.delete(Account.TABLENAME, Account.UUID + "=?", args); + } + + @Override + public SQLiteDatabase getWritableDatabase() { + SQLiteDatabase db = super.getWritableDatabase(); + db.execSQL("PRAGMA foreign_keys=ON;"); + return db; + } + + public void updateMessage(Message message) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] args = { message.getUuid() }; + db.update(Message.TABLENAME, message.getContentValues(), Message.UUID + + "=?", args); + } + + public void updateContact(Contact contact) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] args = { contact.getUuid() }; + 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 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)); + contact.setPresences(Presences.fromJsonString(cursor.getString(1))); + updateContact(contact); + } else { + contact.setUuid(UUID.randomUUID().toString()); + createContact(contact); + } + } + } + + public List getContacts(Account account) { + List list = new ArrayList(); + 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); + } + while (cursor.moveToNext()) { + list.add(Contact.fromCursor(cursor)); + } + return list; + } + + public List getContats(String where) { + List list = new ArrayList(); + SQLiteDatabase db = this.getReadableDatabase(); + Cursor cursor = db.query(Contact.TABLENAME, null, where, null, null, null, null); + while (cursor.moveToNext()) { + list.add(Contact.fromCursor(cursor)); + } + 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); + } + + public void deleteMessage(Message message) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] args = { message.getUuid() }; + db.delete(Message.TABLENAME, Message.UUID + "=?", args); + } + + public void deleteContact(Contact contact) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] args = { contact.getUuid() }; + db.delete(Contact.TABLENAME, Contact.UUID + "=?", args); + } + + +} diff --git a/src/eu/siacs/conversations/persistance/OnPhoneContactsMerged.java b/src/eu/siacs/conversations/persistance/OnPhoneContactsMerged.java new file mode 100644 index 00000000..6a457b17 --- /dev/null +++ b/src/eu/siacs/conversations/persistance/OnPhoneContactsMerged.java @@ -0,0 +1,5 @@ +package eu.siacs.conversations.persistance; + +public interface OnPhoneContactsMerged { + public void phoneContactsMerged(); +} diff --git a/src/eu/siacs/conversations/services/XmppConnectionService.java b/src/eu/siacs/conversations/services/XmppConnectionService.java new file mode 100644 index 00000000..218d5088 --- /dev/null +++ b/src/eu/siacs/conversations/services/XmppConnectionService.java @@ -0,0 +1,946 @@ +package eu.siacs.conversations.services; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Hashtable; +import java.util.List; + +import org.json.JSONException; +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.crypto.PgpEngine; +import eu.siacs.conversations.crypto.PgpEngine.OpenPgpException; +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.persistance.DatabaseBackend; +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.utils.MessageParser; +import eu.siacs.conversations.utils.OnPhoneContactsLoadedListener; +import eu.siacs.conversations.utils.PhoneHelper; +import eu.siacs.conversations.utils.UIHelper; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.IqPacket; +import eu.siacs.conversations.xmpp.MessagePacket; +import eu.siacs.conversations.xmpp.OnIqPacketReceived; +import eu.siacs.conversations.xmpp.OnMessagePacketReceived; +import eu.siacs.conversations.xmpp.OnPresencePacketReceived; +import eu.siacs.conversations.xmpp.OnStatusChanged; +import eu.siacs.conversations.xmpp.PresencePacket; +import eu.siacs.conversations.xmpp.XmppConnection; +import android.app.NotificationManager; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.database.ContentObserver; +import android.database.DatabaseUtils; +import android.os.Binder; +import android.os.Bundle; +import android.os.IBinder; +import android.os.PowerManager; +import android.preference.PreferenceManager; +import android.provider.ContactsContract; +import android.util.Log; + +public class XmppConnectionService extends Service { + + protected static final String LOGTAG = "xmppService"; + public DatabaseBackend databaseBackend; + + public long startDate; + + private List accounts; + private List conversations = null; + + public OnConversationListChangedListener convChangedListener = null; + private OnAccountListChangedListener accountChangedListener = null; + + private ContentObserver contactObserver = new ContentObserver(null) { + @Override + public void onChange(boolean selfChange) { + super.onChange(selfChange); + Log.d(LOGTAG, "contact list has changed"); + mergePhoneContactsWithRoster(null); + } + }; + + private XmppConnectionService service = this; + + private final IBinder mBinder = new XmppConnectionBinder(); + private OnMessagePacketReceived messageListener = new OnMessagePacketReceived() { + + @Override + public void onMessagePacketReceived(Account account, + MessagePacket packet) { + Message message = null; + boolean notify = false; + if ((packet.getType() == MessagePacket.TYPE_CHAT)) { + String pgpBody = MessageParser.getPgpBody(packet); + if (pgpBody != null) { + message = MessageParser.parsePgpChat(pgpBody, packet, + account, service); + notify = false; + } else if (packet.hasChild("body") + && (packet.getBody().startsWith("?OTR"))) { + message = MessageParser.parseOtrChat(packet, account, + service); + notify = true; + } else if (packet.hasChild("body")) { + message = MessageParser.parsePlainTextChat(packet, account, + service); + notify = true; + } else if (packet.hasChild("received") + || (packet.hasChild("sent"))) { + message = MessageParser.parseCarbonMessage(packet, account, + service); + } + + } else if (packet.getType() == MessagePacket.TYPE_GROUPCHAT) { + message = MessageParser + .parseGroupchat(packet, account, service); + if (message != null) { + notify = (message.getStatus() == Message.STATUS_RECIEVED); + } + } else if (packet.getType() == MessagePacket.TYPE_ERROR) { + message = MessageParser.parseError(packet, account, service); + } else { + Log.d(LOGTAG, "unparsed message " + packet.toString()); + } + if (message == 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 (notify) { + message.markUnread(); + } + Conversation conversation = message.getConversation(); + conversation.getMessages().add(message); + if (packet.getType() != MessagePacket.TYPE_ERROR) { + databaseBackend.createMessage(message); + } + if (convChangedListener != null) { + convChangedListener.onConversationListChanged(); + } else { + if (notify) { + NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + mNotificationManager.notify(2342, UIHelper + .getUnreadMessageNotification( + getApplicationContext(), conversation)); + } + } + } + }; + private OnStatusChanged statusListener = new OnStatusChanged() { + + @Override + public void onStatusChanged(Account account) { + if (accountChangedListener != null) { + accountChangedListener.onAccountListChangedListener(); + } + if (account.getStatus() == Account.STATUS_ONLINE) { + databaseBackend.clearPresences(account); + connectMultiModeConversations(account); + List conversations = getConversations(); + for (int i = 0; i < conversations.size(); ++i) { + if (conversations.get(i).getAccount() == account) { + sendUnsendMessages(conversations.get(i)); + } + } + if (convChangedListener != null) { + convChangedListener.onConversationListChanged(); + } + if (account.getKeys().has("pgp_signature")) { + try { + sendPgpPresence(account, account.getKeys().getString("pgp_signature")); + } catch (JSONException e) { + // + } + } + } + } + }; + + private OnPresencePacketReceived presenceListener = new OnPresencePacketReceived() { + + @Override + public void onPresencePacketReceived(Account account, + PresencePacket packet) { + if (packet.hasChild("x")&&(packet.findChild("x").getAttribute("xmlns").startsWith("http://jabber.org/protocol/muc"))) { + Log.d(LOGTAG,"got muc presence "+packet.toString()); + } else { + String[] fromParts = packet.getAttribute("from").split("/"); + Contact contact = findContact(account, fromParts[0]); + if (contact == null) { + // most likely self or roster not synced + return; + } + String type = packet.getAttribute("type"); + if (type == null) { + Element show = packet.findChild("show"); + if (show == null) { + contact.updatePresence(fromParts[1], Presences.ONLINE); + } else if (show.getContent().equals("away")) { + contact.updatePresence(fromParts[1], Presences.AWAY); + } else if (show.getContent().equals("xa")) { + contact.updatePresence(fromParts[1], Presences.XA); + } else if (show.getContent().equals("chat")) { + contact.updatePresence(fromParts[1], Presences.CHAT); + } else if (show.getContent().equals("dnd")) { + contact.updatePresence(fromParts[1], Presences.DND); + } + PgpEngine pgp = getPgpEngine(); + if (pgp!=null) { + Element x = packet.findChild("x"); + if ((x != null) + && (x.getAttribute("xmlns").equals("jabber:x:signed"))) { + try { + Log.d(LOGTAG,"pgp signature for contact" +packet.getAttribute("from")); + contact.setPgpKeyId(pgp.fetchKeyId(packet.findChild("status") + .getContent(), x.getContent())); + } catch (OpenPgpException e) { + Log.d(LOGTAG,"faulty pgp. just ignore"); + } + } + } + databaseBackend.updateContact(contact); + } else if (type.equals("unavailable")) { + if (fromParts.length != 2) { + // Log.d(LOGTAG,"received presence with no resource "+packet.toString()); + } else { + contact.removePresence(fromParts[1]); + databaseBackend.updateContact(contact); + } + } else if (type.equals("subscribe")) { + if (contact + .getSubscriptionOption(Contact.Subscription.PREEMPTIVE_GRANT)) { + sendPresenceUpdatesTo(contact); + contact.setSubscriptionOption(Contact.Subscription.FROM); + contact.resetSubscriptionOption(Contact.Subscription.PREEMPTIVE_GRANT); + replaceContactInConversation(contact.getJid(), contact); + databaseBackend.updateContact(contact); + if ((contact + .getSubscriptionOption(Contact.Subscription.ASKING)) + && (!contact + .getSubscriptionOption(Contact.Subscription.TO))) { + requestPresenceUpdatesFrom(contact); + } + } else { + // TODO: ask user to handle it maybe + } + } else { + //Log.d(LOGTAG, packet.toString()); + } + replaceContactInConversation(contact.getJid(), contact); + } + } + }; + + private OnIqPacketReceived unknownIqListener = new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.hasChild("query")) { + Element query = packet.findChild("query"); + String xmlns = query.getAttribute("xmlns"); + if ((xmlns != null) && (xmlns.equals("jabber:iq:roster"))) { + processRosterItems(account, query); + mergePhoneContactsWithRoster(null); + } + } + } + }; + + private OpenPgpServiceConnection pgpServiceConnection; + private PgpEngine mPgpEngine = null; + + public PgpEngine getPgpEngine() { + if (pgpServiceConnection.isBound()) { + if (this.mPgpEngine == null) { + this.mPgpEngine = new PgpEngine(new OpenPgpApi( + getApplicationContext(), + pgpServiceConnection.getService())); + } + return mPgpEngine; + } else { + return null; + } + + } + + private void processRosterItems(Account account, Element elements) { + String version = elements.getAttribute("ver"); + if (version != null) { + account.setRosterVersion(version); + databaseBackend.updateAccount(account); + } + for (Element item : elements.getChildren()) { + if (item.getName().equals("item")) { + String jid = item.getAttribute("jid"); + 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); + } + } else { + if (subscription.equals("remove")) { + databaseBackend.deleteContact(contact); + replaceContactInConversation(contact.getJid(), null); + } else { + contact.parseSubscriptionFromElement(item); + databaseBackend.updateContact(contact); + replaceContactInConversation(contact.getJid(), contact); + } + } + } + } + } + + private void replaceContactInConversation(String jid, Contact contact) { + List conversations = getConversations(); + for (int i = 0; i < conversations.size(); ++i) { + if ((conversations.get(i).getContactJid().equals(jid))) { + conversations.get(i).setContact(contact); + break; + } + } + } + + public class XmppConnectionBinder extends Binder { + public XmppConnectionService getService() { + return XmppConnectionService.this; + } + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + for (Account account : accounts) { + if (account.getXmppConnection() == null) { + if (!account.isOptionSet(Account.OPTION_DISABLED)) { + account.setXmppConnection(this.createConnection(account)); + } + } + } + return START_STICKY; + } + + @Override + public void onCreate() { + databaseBackend = DatabaseBackend.getInstance(getApplicationContext()); + this.accounts = databaseBackend.getAccounts(); + + getContentResolver().registerContentObserver( + ContactsContract.Contacts.CONTENT_URI, true, contactObserver); + this.pgpServiceConnection = new OpenPgpServiceConnection( + getApplicationContext(), "org.sufficientlysecure.keychain"); + this.pgpServiceConnection.bindToService(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + for (Account account : accounts) { + if (account.getXmppConnection() != null) { + disconnect(account); + } + } + } + + public XmppConnection createConnection(Account account) { + PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + XmppConnection connection = new XmppConnection(account, pm); + connection.setOnMessagePacketReceivedListener(this.messageListener); + connection.setOnStatusChangedListener(this.statusListener); + connection.setOnPresencePacketReceivedListener(this.presenceListener); + connection + .setOnUnregisteredIqPacketReceivedListener(this.unknownIqListener); + Thread thread = new Thread(connection); + thread.start(); + return connection; + } + + public void sendMessage(Message message, String presence) { + Account account = message.getConversation().getAccount(); + Conversation conv = message.getConversation(); + boolean saveInDb = false; + boolean addToConversation = false; + if (account.getStatus() == Account.STATUS_ONLINE) { + MessagePacket packet; + if (message.getEncryption() == Message.ENCRYPTION_OTR) { + if (!conv.hasValidOtrSession()) { + // starting otr session. messages will be send later + conv.startOtrSession(getApplicationContext(), presence); + } else if (conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) { + // otr session aleary exists, creating message packet + // accordingly + packet = prepareMessagePacket(account, message, + conv.getOtrSession()); + account.getXmppConnection().sendMessagePacket(packet); + message.setStatus(Message.STATUS_SEND); + } + saveInDb = true; + addToConversation = true; + } else if (message.getEncryption() == Message.ENCRYPTION_PGP) { + long keyId = message.getConversation().getContact() + .getPgpKeyId(); + packet = new MessagePacket(); + packet.setType(MessagePacket.TYPE_CHAT); + packet.setFrom(message.getConversation().getAccount() + .getFullJid()); + packet.setTo(message.getCounterpart()); + packet.setBody("This is an XEP-0027 encryted message"); + Element x = new Element("x"); + x.setAttribute("xmlns", "jabber:x:encrypted"); + x.setContent(this.getPgpEngine().encrypt(keyId, + message.getBody())); + packet.addChild(x); + account.getXmppConnection().sendMessagePacket(packet); + message.setStatus(Message.STATUS_SEND); + message.setEncryption(Message.ENCRYPTION_DECRYPTED); + saveInDb = true; + addToConversation = true; + } else { + // don't encrypt + if (message.getConversation().getMode() == Conversation.MODE_SINGLE) { + message.setStatus(Message.STATUS_SEND); + saveInDb = true; + addToConversation = true; + } + + packet = prepareMessagePacket(account, message, null); + account.getXmppConnection().sendMessagePacket(packet); + } + } else { + // account is offline + saveInDb = true; + addToConversation = true; + + } + if (saveInDb) { + databaseBackend.createMessage(message); + } + if (addToConversation) { + conv.getMessages().add(message); + if (convChangedListener != null) { + convChangedListener.onConversationListChanged(); + } + } + + } + + private void sendUnsendMessages(Conversation conversation) { + for (int i = 0; i < conversation.getMessages().size(); ++i) { + if (conversation.getMessages().get(i).getStatus() == Message.STATUS_UNSEND) { + 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--; + } + } + } + } + + public MessagePacket prepareMessagePacket(Account account, Message message, + Session otrSession) { + MessagePacket packet = new MessagePacket(); + if (message.getConversation().getMode() == Conversation.MODE_SINGLE) { + packet.setType(MessagePacket.TYPE_CHAT); + if (otrSession != null) { + try { + packet.setBody(otrSession.transformSending(message + .getBody())); + } catch (OtrException e) { + Log.d(LOGTAG, + account.getJid() + + ": could not encrypt message to " + + message.getCounterpart()); + } + Element privateMarker = new Element("private"); + privateMarker.setAttribute("xmlns", "urn:xmpp:carbons:2"); + packet.addChild(privateMarker); + 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()); + } + } else if (message.getConversation().getMode() == Conversation.MODE_MULTI) { + packet.setType(MessagePacket.TYPE_GROUPCHAT); + packet.setBody(message.getBody()); + packet.setTo(message.getCounterpart()); + packet.setFrom(account.getJid()); + } + return packet; + } + + public void getRoster(Account account, + final OnRosterFetchedListener listener) { + List contacts = databaseBackend.getContacts(account); + for (int i = 0; i < contacts.size(); ++i) { + contacts.get(i).setAccount(account); + } + if (listener != null) { + listener.onRosterFetched(contacts); + } + } + + public void updateRoster(final Account account, + final OnRosterFetchedListener listener) { + IqPacket iqPacket = new IqPacket(IqPacket.TYPE_GET); + Element query = new Element("query"); + query.setAttribute("xmlns", "jabber:iq:roster"); + query.setAttribute("ver", account.getRosterVersion()); + iqPacket.addChild(query); + account.getXmppConnection().sendIqPacket(iqPacket, + new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(final Account account, + IqPacket packet) { + Element roster = packet.findChild("query"); + if (roster != null) { + processRosterItems(account, roster); + StringBuilder mWhere = new StringBuilder(); + mWhere.append("jid NOT IN("); + List 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 contactsToDelete = databaseBackend + .getContats(mWhere.toString()); + for (Contact contact : contactsToDelete) { + databaseBackend.deleteContact(contact); + replaceContactInConversation(contact.getJid(), + null); + } + + } + mergePhoneContactsWithRoster(new OnPhoneContactsMerged() { + + @Override + public void phoneContactsMerged() { + if (listener != null) { + getRoster(account, listener); + } + } + }); + } + }); + } + + public void mergePhoneContactsWithRoster( + final OnPhoneContactsMerged listener) { + PhoneHelper.loadPhoneContacts(getApplicationContext(), + new OnPhoneContactsLoadedListener() { + @Override + public void onPhoneContactsLoaded( + Hashtable phoneContacts) { + List contacts = databaseBackend + .getContacts(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()); + String systemAccount = phoneContact + .getInt("phoneid") + + "#" + + phoneContact.getString("lookup"); + contact.setSystemAccount(systemAccount); + contact.setPhotoUri(phoneContact + .getString("photouri")); + contact.setDisplayName(phoneContact + .getString("displayname")); + databaseBackend.updateContact(contact); + replaceContactInConversation(contact.getJid(), + contact); + } else { + if ((contact.getSystemAccount() != null) + || (contact.getProfilePhoto() != null)) { + contact.setSystemAccount(null); + contact.setPhotoUri(null); + databaseBackend.updateContact(contact); + replaceContactInConversation( + contact.getJid(), contact); + } + } + } + if (listener != null) { + listener.phoneContactsMerged(); + } + } + }); + } + + public List getConversations() { + if (this.conversations == null) { + Hashtable accountLookupTable = new Hashtable(); + for (Account account : this.accounts) { + accountLookupTable.put(account.getUuid(), account); + } + this.conversations = databaseBackend + .getConversations(Conversation.STATUS_AVAILABLE); + for (Conversation conv : this.conversations) { + Account account = accountLookupTable.get(conv.getAccountUuid()); + conv.setAccount(account); + conv.setContact(findContact(account, conv.getContactJid())); + conv.setMessages(databaseBackend.getMessages(conv, 50)); + } + } + return this.conversations; + } + + public List getAccounts() { + return this.accounts; + } + + public Contact findContact(Account account, String jid) { + Contact contact = databaseBackend.findContact(account, jid); + if (contact != null) { + contact.setAccount(account); + } + return contact; + } + + public Conversation findOrCreateConversation(Account account, String jid, + boolean muc) { + for (Conversation conv : this.getConversations()) { + if ((conv.getAccount().equals(account)) + && (conv.getContactJid().equals(jid))) { + return conv; + } + } + Conversation conversation = databaseBackend.findConversation(account, + jid); + if (conversation != null) { + conversation.setStatus(Conversation.STATUS_AVAILABLE); + conversation.setAccount(account); + if (muc) { + conversation.setMode(Conversation.MODE_MULTI); + if (account.getStatus() == Account.STATUS_ONLINE) { + joinMuc(conversation); + } + } else { + conversation.setMode(Conversation.MODE_SINGLE); + } + this.databaseBackend.updateConversation(conversation); + conversation.setContact(findContact(account, + conversation.getContactJid())); + } else { + String conversationName; + Contact contact = findContact(account, jid); + if (contact != null) { + conversationName = contact.getDisplayName(); + } else { + conversationName = jid.split("@")[0]; + } + if (muc) { + conversation = new Conversation(conversationName, account, jid, + Conversation.MODE_MULTI); + if (account.getStatus() == Account.STATUS_ONLINE) { + joinMuc(conversation); + } + } else { + conversation = new Conversation(conversationName, account, jid, + Conversation.MODE_SINGLE); + } + conversation.setContact(contact); + this.databaseBackend.createConversation(conversation); + } + this.conversations.add(conversation); + if (this.convChangedListener != null) { + this.convChangedListener.onConversationListChanged(); + } + return conversation; + } + + public void archiveConversation(Conversation conversation) { + if (conversation.getMode() == Conversation.MODE_MULTI) { + leaveMuc(conversation); + } else { + try { + conversation.endOtrIfNeeded(); + } catch (OtrException e) { + Log.d(LOGTAG, + "error ending otr session for " + + conversation.getName()); + } + } + this.databaseBackend.updateConversation(conversation); + this.conversations.remove(conversation); + if (this.convChangedListener != null) { + this.convChangedListener.onConversationListChanged(); + } + } + + public int getConversationCount() { + return this.databaseBackend.getConversationCount(); + } + + public void createAccount(Account account) { + databaseBackend.createAccount(account); + this.accounts.add(account); + account.setXmppConnection(this.createConnection(account)); + if (accountChangedListener != null) + accountChangedListener.onAccountListChangedListener(); + } + + public void deleteContact(Contact 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("subscription", "remove"); + query.addChild(item); + iq.addChild(query); + contact.getAccount().getXmppConnection().sendIqPacket(iq, null); + replaceContactInConversation(contact.getJid(), null); + databaseBackend.deleteContact(contact); + } + + public void updateAccount(Account account) { + databaseBackend.updateAccount(account); + if (account.getXmppConnection() != null) { + disconnect(account); + } + if (!account.isOptionSet(Account.OPTION_DISABLED)) { + account.setXmppConnection(this.createConnection(account)); + } + if (accountChangedListener != null) + accountChangedListener.onAccountListChangedListener(); + } + + public void deleteAccount(Account account) { + Log.d(LOGTAG, "called delete account"); + if (account.getXmppConnection() != null) { + this.disconnect(account); + } + databaseBackend.deleteAccount(account); + this.accounts.remove(account); + if (accountChangedListener != null) + accountChangedListener.onAccountListChangedListener(); + } + + public void setOnConversationListChangedListener( + OnConversationListChangedListener listener) { + this.convChangedListener = listener; + } + + public void removeOnConversationListChangedListener() { + this.convChangedListener = null; + } + + public void setOnAccountListChangedListener( + OnAccountListChangedListener listener) { + this.accountChangedListener = listener; + } + + public void removeOnAccountListChangedListener() { + this.accountChangedListener = null; + } + + public void connectMultiModeConversations(Account account) { + List conversations = getConversations(); + for (int i = 0; i < conversations.size(); i++) { + Conversation conversation = conversations.get(i); + if ((conversation.getMode() == Conversation.MODE_MULTI) + && (conversation.getAccount() == account)) { + joinMuc(conversation); + } + } + } + + public void joinMuc(Conversation conversation) { + String muc = conversation.getContactJid(); + PresencePacket packet = new PresencePacket(); + packet.setAttribute("to", muc + "/" + + conversation.getAccount().getUsername()); + Element x = new Element("x"); + x.setAttribute("xmlns", "http://jabber.org/protocol/muc"); + if (conversation.getMessages().size() != 0) { + Element history = new Element("history"); + long lastMsgTime = conversation.getLatestMessage().getTimeSent(); + long diff = (System.currentTimeMillis() - lastMsgTime) / 1000 - 1; + history.setAttribute("seconds", diff + ""); + x.addChild(history); + } + packet.addChild(x); + conversation.getAccount().getXmppConnection() + .sendPresencePacket(packet); + } + + public void leaveMuc(Conversation conversation) { + + } + + public void disconnect(Account account) { + List conversations = getConversations(); + for (int i = 0; i < conversations.size(); i++) { + Conversation conversation = conversations.get(i); + if (conversation.getAccount() == account) { + if (conversation.getMode() == Conversation.MODE_MULTI) { + leaveMuc(conversation); + } else { + try { + conversation.endOtrIfNeeded(); + } catch (OtrException e) { + Log.d(LOGTAG, "error ending otr session for " + + conversation.getName()); + } + } + } + } + account.getXmppConnection().disconnect(); + Log.d(LOGTAG, "disconnected account: " + account.getJid()); + account.setXmppConnection(null); + } + + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } + + public void updateContact(Contact contact) { + databaseBackend.updateContact(contact); + } + + public void updateMessage(Message message) { + databaseBackend.updateMessage(message); + } + + public void createContact(Contact contact) { + SharedPreferences sharedPref = PreferenceManager + .getDefaultSharedPreferences(getApplicationContext()); + boolean autoGrant = sharedPref.getBoolean("grant_new_contacts", true); + if (autoGrant) { + contact.setSubscriptionOption(Contact.Subscription.PREEMPTIVE_GRANT); + contact.setSubscriptionOption(Contact.Subscription.ASKING); + } + 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); + Account account = contact.getAccount(); + account.getXmppConnection().sendIqPacket(iq, null); + if (autoGrant) { + requestPresenceUpdatesFrom(contact); + } + replaceContactInConversation(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) { + 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); + account.getXmppConnection().sendPresencePacket(packet); + } + + public void generatePgpAnnouncement(Account account) + throws PgpEngine.UserInputRequiredException { + if (account.getStatus() == Account.STATUS_ONLINE) { + String signature = getPgpEngine().generateSignature("online"); + account.setKey("pgp_signature", signature); + databaseBackend.updateAccount(account); + sendPgpPresence(account, signature); + } + } +} \ No newline at end of file diff --git a/src/eu/siacs/conversations/ui/ConversationActivity.java b/src/eu/siacs/conversations/ui/ConversationActivity.java new file mode 100644 index 00000000..7660b8fd --- /dev/null +++ b/src/eu/siacs/conversations/ui/ConversationActivity.java @@ -0,0 +1,481 @@ +package eu.siacs.conversations.ui; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +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.net.Uri; +import android.os.Bundle; +import android.app.AlertDialog; +import android.app.FragmentTransaction; +import android.app.NotificationManager; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.graphics.Typeface; +import android.support.v4.widget.SlidingPaneLayout; +import android.support.v4.widget.SlidingPaneLayout.PanelSlideListener; +import android.util.Log; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.InputMethodManager; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.widget.PopupMenu; +import android.widget.PopupMenu.OnMenuItemClickListener; +import android.widget.TextView; +import android.widget.ImageView; + +public class ConversationActivity extends XmppActivity { + + public static final String VIEW_CONVERSATION = "viewConversation"; + public static final String CONVERSATION = "conversationUuid"; + + public static final int REQUEST_SEND_MESSAGE = 0x75441; + public static final int REQUEST_DECRYPT_PGP = 0x76783; + + protected SlidingPaneLayout spl; + + private List conversationList = new ArrayList(); + private Conversation selectedConversation = null; + private ListView listView; + + private boolean paneShouldBeOpen = true; + private ArrayAdapter listAdapter; + + private OnConversationListChangedListener onConvChanged = new OnConversationListChangedListener() { + + @Override + public void onConversationListChanged() { + conversationList.clear(); + conversationList.addAll(xmppConnectionService + .getConversations()); + runOnUiThread(new Runnable() { + + @Override + public void run() { + updateConversationList(); + if(paneShouldBeOpen) { + if (conversationList.size() >= 1) { + swapConversationFragment(); + } else { + startActivity(new Intent(getApplicationContext(), NewConversationActivity.class)); + finish(); + } + } + ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager().findFragmentByTag("conversation"); + if (selectedFragment!=null) { + selectedFragment.updateMessages(); + } + } + }); + } + }; + + private DialogInterface.OnClickListener addToRoster = new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + String jid = getSelectedConversation().getContactJid(); + Account account = getSelectedConversation().getAccount(); + String name = jid.split("@")[0]; + Contact contact = new Contact(account, name, jid, null); + xmppConnectionService.createContact(contact); + } + }; + + public List getConversationList() { + return this.conversationList; + } + + public Conversation getSelectedConversation() { + return this.selectedConversation; + } + + public ListView getConversationListView() { + return this.listView; + } + + public SlidingPaneLayout getSlidingPaneLayout() { + return this.spl; + } + + public boolean shouldPaneBeOpen() { + return paneShouldBeOpen; + } + + public void updateConversationList() { + if (conversationList.size() >= 1) { + Collections.sort(this.conversationList, new Comparator() { + @Override + public int compare(Conversation lhs, Conversation rhs) { + return (int) (rhs.getLatestMessage().getTimeSent() - lhs.getLatestMessage().getTimeSent()); + } + }); + } + this.listView.invalidateViews(); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + + super.onCreate(savedInstanceState); + + setContentView(R.layout.fragment_conversations_overview); + + listView = (ListView) findViewById(R.id.list); + + this.listAdapter = new ArrayAdapter(this, + R.layout.conversation_list_row, conversationList) { + @Override + public View getView(int position, View view, ViewGroup parent) { + if (view == null) { + LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); + view = (View) inflater.inflate( + R.layout.conversation_list_row, null); + } + Conversation conv = getItem(position); + TextView convName = (TextView) view.findViewById(R.id.conversation_name); + convName.setText(conv.getName()); + TextView convLastMsg = (TextView) view.findViewById(R.id.conversation_lastmsg); + convLastMsg.setText(conv.getLatestMessage().getBody()); + + if(!conv.isRead()) { + convName.setTypeface(null,Typeface.BOLD); + convLastMsg.setTypeface(null,Typeface.BOLD); + } else { + convName.setTypeface(null,Typeface.NORMAL); + convLastMsg.setTypeface(null,Typeface.NORMAL); + } + + ((TextView) view.findViewById(R.id.conversation_lastupdate)) + .setText(UIHelper.readableTimeDifference(getItem(position).getLatestMessage().getTimeSent())); + + Uri profilePhoto = getItem(position).getProfilePhotoUri(); + ImageView imageView = (ImageView) view.findViewById(R.id.conversation_image); + if (profilePhoto!=null) { + imageView.setImageURI(profilePhoto); + } else { + imageView.setImageBitmap(UIHelper.getUnknownContactPicture(getItem(position).getName(),200)); + } + + + ((ImageView) view.findViewById(R.id.conversation_image)) + .setImageURI(getItem(position).getProfilePhotoUri()); + return view; + } + + }; + + listView.setAdapter(this.listAdapter); + + listView.setOnItemClickListener(new OnItemClickListener() { + + @Override + public void onItemClick(AdapterView arg0, View clickedView, + int position, long arg3) { + paneShouldBeOpen = false; + if (selectedConversation != conversationList.get(position)) { + selectedConversation = conversationList.get(position); + swapConversationFragment(); //.onBackendConnected(conversationList.get(position)); + } else { + spl.closePane(); + } + } + }); + spl = (SlidingPaneLayout) findViewById(R.id.slidingpanelayout); + spl.setParallaxDistance(150); + spl.setShadowResource(R.drawable.es_slidingpane_shadow); + spl.setSliderFadeColor(0); + spl.setPanelSlideListener(new PanelSlideListener() { + + @Override + public void onPanelOpened(View arg0) { + paneShouldBeOpen = true; + getActionBar().setDisplayHomeAsUpEnabled(false); + getActionBar().setTitle(R.string.app_name); + invalidateOptionsMenu(); + + InputMethodManager inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + + View focus = getCurrentFocus(); + + if (focus != null) { + + inputManager.hideSoftInputFromWindow( + focus.getWindowToken(), + InputMethodManager.HIDE_NOT_ALWAYS); + } + } + + @Override + public void onPanelClosed(View arg0) { + paneShouldBeOpen = false; + if (conversationList.size() > 0) { + getActionBar().setDisplayHomeAsUpEnabled(true); + getActionBar().setTitle(getSelectedConversation().getName()); + invalidateOptionsMenu(); + if (!getSelectedConversation().isRead()) { + getSelectedConversation().markRead(); + updateConversationList(); + } + } + } + + @Override + public void onPanelSlide(View arg0, float arg1) { + // TODO Auto-generated method stub + + } + }); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.conversations, menu); + MenuItem menuSecure = (MenuItem) menu.findItem(R.id.action_security); + MenuItem menuArchive = (MenuItem) menu.findItem(R.id.action_archive); + MenuItem menuMucDetails = (MenuItem) menu.findItem(R.id.action_muc_details); + MenuItem menuContactDetails = (MenuItem) menu.findItem(R.id.action_contact_details); + + if (spl.isOpen()) { + menuArchive.setVisible(false); + menuMucDetails.setVisible(false); + menuContactDetails.setVisible(false); + menuSecure.setVisible(false); + } else { + ((MenuItem) menu.findItem(R.id.action_add)).setVisible(false); + if (this.getSelectedConversation()!=null) { + if (this.getSelectedConversation().getMode() == Conversation.MODE_MULTI) { + menuMucDetails.setVisible(true); + menuContactDetails.setVisible(false); + menuSecure.setVisible(false); + menuArchive.setTitle("Leave conference"); + } else { + menuContactDetails.setVisible(true); + menuMucDetails.setVisible(false); + if (this.getSelectedConversation().getLatestMessage().getEncryption() != Message.ENCRYPTION_NONE) { + menuSecure.setIcon(R.drawable.ic_action_secure); + } + } + } + } + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + spl.openPane(); + break; + case R.id.action_settings: + startActivity(new Intent(this, SettingsActivity.class)); + break; + case R.id.action_accounts: + startActivity(new Intent(this, ManageAccountActivity.class)); + break; + case R.id.action_add: + startActivity(new Intent(this, NewConversationActivity.class)); + break; + case R.id.action_archive: + Conversation conv = getSelectedConversation(); + conv.setStatus(Conversation.STATUS_ARCHIVED); + paneShouldBeOpen = true; + spl.openPane(); + xmppConnectionService.archiveConversation(conv); + selectedConversation = conversationList.get(0); + break; + case R.id.action_contact_details: + DialogContactDetails details = new DialogContactDetails(); + Contact contact = this.getSelectedConversation().getContact(); + if (contact != null) { + contact.setAccount(this.selectedConversation.getAccount()); + details.setContact(contact); + details.show(getFragmentManager(), "details"); + } else { + String jid = getSelectedConversation().getContactJid(); + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(jid); + builder.setMessage("The contact is not in your roster. Would you like to add it."); + builder.setNegativeButton("Cancel", null); + builder.setPositiveButton("Add",addToRoster); + builder.create().show(); + } + break; + case R.id.action_security: + final Conversation selConv = getSelectedConversation(); + View menuItemView = findViewById(R.id.action_security); + PopupMenu popup = new PopupMenu(this, menuItemView); + final ConversationFragment fragment = (ConversationFragment) getFragmentManager().findFragmentByTag("conversation"); + if (fragment!=null) { + popup.setOnMenuItemClickListener(new OnMenuItemClickListener() { + + @Override + public boolean onMenuItemClick(MenuItem item) { + switch (item.getItemId()) { + case R.id.encryption_choice_none: + selConv.nextMessageEncryption = Message.ENCRYPTION_NONE; + item.setChecked(true); + break; + case R.id.encryption_choice_otr: + selConv.nextMessageEncryption = Message.ENCRYPTION_OTR; + item.setChecked(true); + break; + case R.id.encryption_choice_pgp: + selConv.nextMessageEncryption = Message.ENCRYPTION_PGP; + item.setChecked(true); + break; + default: + selConv.nextMessageEncryption = Message.ENCRYPTION_NONE; + break; + } + fragment.updateChatMsgHint(); + return true; + } + }); + popup.inflate(R.menu.encryption_choices); + switch (selConv.nextMessageEncryption) { + 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); + break; + case Message.ENCRYPTION_PGP: + popup.getMenu().findItem(R.id.encryption_choice_pgp).setChecked(true); + break; + case Message.ENCRYPTION_DECRYPTED: + popup.getMenu().findItem(R.id.encryption_choice_pgp).setChecked(true); + break; + default: + popup.getMenu().findItem(R.id.encryption_choice_none).setChecked(true); + break; + } + popup.show(); + } + + break; + default: + break; + } + return super.onOptionsItemSelected(item); + } + + protected ConversationFragment swapConversationFragment() { + ConversationFragment selectedFragment = new ConversationFragment(); + + FragmentTransaction transaction = getFragmentManager() + .beginTransaction(); + transaction.replace(R.id.selected_conversation, selectedFragment,"conversation"); + transaction.commit(); + return selectedFragment; + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (!spl.isOpen()) { + spl.openPane(); + return false; + } + } + return super.onKeyDown(keyCode, event); + } + + public void onStart() { + super.onStart(); + NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + nm.cancelAll(); + if (conversationList.size()>=1) { + onConvChanged.onConversationListChanged(); + } + } + + @Override + protected void onStop() { + Log.d("gultsch","called on stop in conversation activity"); + if (xmppConnectionServiceBound) { + xmppConnectionService.removeOnConversationListChangedListener(); + } + super.onStop(); + } + + @Override + void onBackendConnected() { + + xmppConnectionService.setOnConversationListChangedListener(this.onConvChanged); + + if (conversationList.size()==0) { + conversationList.clear(); + conversationList.addAll(xmppConnectionService + .getConversations()); + + this.updateConversationList(); + } + + if ((getIntent().getAction().equals(Intent.ACTION_VIEW) && (!handledViewIntent))) { + if (getIntent().getType().equals( + ConversationActivity.VIEW_CONVERSATION)) { + handledViewIntent = true; + + String convToView = (String) getIntent().getExtras().get(CONVERSATION); + + for(int i = 0; i < conversationList.size(); ++i) { + if (conversationList.get(i).getUuid().equals(convToView)) { + selectedConversation = conversationList.get(i); + } + } + paneShouldBeOpen = false; + swapConversationFragment(); + } + } else { + if (xmppConnectionService.getAccounts().size() == 0) { + startActivity(new Intent(this, ManageAccountActivity.class)); + finish(); + } else if (conversationList.size() <= 0) { + //add no history + startActivity(new Intent(this, NewConversationActivity.class)); + finish(); + } else { + spl.openPane(); + //find currently loaded fragment + ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager().findFragmentByTag("conversation"); + if (selectedFragment!=null) { + Log.d("gultsch","ConversationActivity. found old fragment."); + selectedFragment.onBackendConnected(); + } else { + Log.d("gultsch","conversationactivity. no old fragment found. creating new one"); + selectedConversation = conversationList.get(0); + Log.d("gultsch","selected conversation is #"+selectedConversation); + swapConversationFragment(); + } + } + } + } + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (resultCode == RESULT_OK) { + if (requestCode == REQUEST_DECRYPT_PGP) { + ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager().findFragmentByTag("conversation"); + if (selectedFragment!=null) { + selectedFragment.hidePgpPassphraseBox(); + } + } + } + } +} diff --git a/src/eu/siacs/conversations/ui/ConversationFragment.java b/src/eu/siacs/conversations/ui/ConversationFragment.java new file mode 100644 index 00000000..ff06fafc --- /dev/null +++ b/src/eu/siacs/conversations/ui/ConversationFragment.java @@ -0,0 +1,602 @@ +package eu.siacs.conversations.ui; + +import java.io.FileNotFoundException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import net.java.otr4j.session.SessionStatus; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.crypto.PgpEngine.OpenPgpException; +import eu.siacs.conversations.crypto.PgpEngine.UserInputRequiredException; +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.utils.PhoneHelper; +import eu.siacs.conversations.utils.UIHelper; +import android.app.AlertDialog; +import android.app.Fragment; +import android.content.DialogInterface; +import android.content.IntentSender; +import android.content.SharedPreferences; +import android.content.IntentSender.SendIntentException; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Typeface; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.ListView; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.TextView; + +public class ConversationFragment extends Fragment { + + protected Conversation conversation; + protected ListView messagesView; + protected LayoutInflater inflater; + protected List messageList = new ArrayList(); + protected ArrayAdapter messageListAdapter; + protected Contact contact; + protected BitmapCache mBitmapCache = new BitmapCache(); + + protected String queuedPqpMessage = null; + + private EditText chatMsg; + + protected Bitmap selfBitmap; + + private IntentSender askForPassphraseIntent = null; + + private OnClickListener sendMsgListener = new OnClickListener() { + + @Override + public void onClick(View v) { + if (chatMsg.getText().length() < 1) + return; + Message message = new Message(conversation, chatMsg.getText() + .toString(), conversation.nextMessageEncryption); + if (conversation.nextMessageEncryption == Message.ENCRYPTION_OTR) { + sendOtrMessage(message); + } else if (conversation.nextMessageEncryption == Message.ENCRYPTION_PGP) { + sendPgpMessage(message); + } else { + sendPlainTextMessage(message); + } + } + }; + protected OnClickListener clickToDecryptListener = new OnClickListener() { + + @Override + public void onClick(View v) { + Log.d("gultsch","clicked to decrypt"); + if (askForPassphraseIntent!=null) { + try { + getActivity().startIntentSenderForResult(askForPassphraseIntent, ConversationActivity.REQUEST_DECRYPT_PGP, null, 0, 0, 0); + } catch (SendIntentException e) { + Log.d("gultsch","couldnt fire intent"); + } + } + } + }; + private LinearLayout pgpInfo; + + public void hidePgpPassphraseBox() { + pgpInfo.setVisibility(View.GONE); + } + + public void updateChatMsgHint() { + if (conversation.getMode() == Conversation.MODE_MULTI) { + chatMsg.setHint("Send message to conference"); + } else { + switch (conversation.nextMessageEncryption) { + case Message.ENCRYPTION_NONE: + chatMsg.setHint("Send plain text message"); + break; + case Message.ENCRYPTION_OTR: + chatMsg.setHint("Send OTR encrypted message"); + break; + case Message.ENCRYPTION_PGP: + chatMsg.setHint("Send openPGP encryted messeage"); + break; + case Message.ENCRYPTION_DECRYPTED: + chatMsg.setHint("Send openPGP encryted messeage"); + break; + default: + break; + } + } + } + + @Override + public View onCreateView(final LayoutInflater inflater, + ViewGroup container, Bundle savedInstanceState) { + + this.inflater = inflater; + + final View view = inflater.inflate(R.layout.fragment_conversation, + container, false); + chatMsg = (EditText) view.findViewById(R.id.textinput); + ImageButton sendButton = (ImageButton) view + .findViewById(R.id.textSendButton); + sendButton.setOnClickListener(this.sendMsgListener); + + pgpInfo = (LinearLayout) view.findViewById(R.id.pgp_keyentry); + pgpInfo.setOnClickListener(clickToDecryptListener); + + messagesView = (ListView) view.findViewById(R.id.messages_view); + + messageListAdapter = new ArrayAdapter(this.getActivity() + .getApplicationContext(), R.layout.message_sent, + this.messageList) { + + private static final int SENT = 0; + private static final int RECIEVED = 1; + private static final int ERROR = 2; + + @Override + public int getViewTypeCount() { + return 3; + } + + @Override + public int getItemViewType(int position) { + if (getItem(position).getStatus() == Message.STATUS_RECIEVED) { + return RECIEVED; + } else if (getItem(position).getStatus() == Message.STATUS_ERROR) { + return ERROR; + } else { + return SENT; + } + } + + @Override + public View getView(int position, View view, ViewGroup parent) { + Message item = getItem(position); + int type = getItemViewType(position); + ViewHolder viewHolder; + if (view == null) { + viewHolder = new ViewHolder(); + switch (type) { + case SENT: + view = (View) inflater.inflate(R.layout.message_sent, + null); + viewHolder.imageView = (ImageView) view + .findViewById(R.id.message_photo); + viewHolder.imageView.setImageBitmap(selfBitmap); + break; + case RECIEVED: + view = (View) inflater.inflate( + R.layout.message_recieved, null); + viewHolder.imageView = (ImageView) view + .findViewById(R.id.message_photo); + if (item.getConversation().getMode() == Conversation.MODE_SINGLE) { + Uri uri = item.getConversation() + .getProfilePhotoUri(); + if (uri != null) { + viewHolder.imageView + .setImageBitmap(mBitmapCache.get(item + .getConversation().getName(), + uri)); + } else { + viewHolder.imageView + .setImageBitmap(mBitmapCache.get(item + .getConversation().getName(), + null)); + } + } + break; + case ERROR: + view = (View) inflater.inflate(R.layout.message_error, + null); + viewHolder.imageView = (ImageView) view + .findViewById(R.id.message_photo); + viewHolder.imageView.setImageBitmap(mBitmapCache + .getError()); + break; + default: + viewHolder = null; + break; + } + 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 == RECIEVED) { + if (item.getConversation().getMode() == Conversation.MODE_MULTI) { + if (item.getCounterpart() != null) { + viewHolder.imageView.setImageBitmap(mBitmapCache + .get(item.getCounterpart(), null)); + } else { + viewHolder.imageView + .setImageBitmap(mBitmapCache.get(item + .getConversation().getName(), null)); + } + } + } + String body = item.getBody(); + if (body != null) { + if (item.getEncryption() == Message.ENCRYPTION_PGP) { + viewHolder.messageBody.setText(getString(R.string.encrypted_message)); + viewHolder.messageBody.setTextColor(0xff33B5E5); + viewHolder.messageBody.setTypeface(null,Typeface.ITALIC); + } else { + viewHolder.messageBody.setText(body.trim()); + viewHolder.messageBody.setTextColor(0xff000000); + viewHolder.messageBody.setTypeface(null, Typeface.NORMAL); + } + } + if (item.getStatus() == Message.STATUS_UNSEND) { + viewHolder.time.setTypeface(null, Typeface.ITALIC); + viewHolder.time.setText("sending\u2026"); + } else { + viewHolder.time.setTypeface(null, Typeface.NORMAL); + if ((item.getConversation().getMode() == Conversation.MODE_SINGLE) + || (type != RECIEVED)) { + viewHolder.time.setText(UIHelper + .readableTimeDifference(item.getTimeSent())); + } else { + viewHolder.time.setText(item.getCounterpart() + + " \u00B7 " + + UIHelper.readableTimeDifference(item + .getTimeSent())); + } + } + return view; + } + }; + messagesView.setAdapter(messageListAdapter); + + return view; + } + + protected Bitmap findSelfPicture() { + SharedPreferences sharedPref = PreferenceManager + .getDefaultSharedPreferences(getActivity() + .getApplicationContext()); + boolean showPhoneSelfContactPicture = sharedPref.getBoolean( + "show_phone_selfcontact_picture", true); + + Bitmap self = null; + + if (showPhoneSelfContactPicture) { + Uri selfiUri = PhoneHelper.getSefliUri(getActivity()); + if (selfiUri != null) { + try { + self = BitmapFactory.decodeStream(getActivity() + .getContentResolver().openInputStream(selfiUri)); + } catch (FileNotFoundException e) { + self = null; + } + } + } + if (self == null) { + self = UIHelper.getUnknownContactPicture(conversation.getAccount() + .getJid(), 200); + } + + final Bitmap selfBitmap = self; + return selfBitmap; + } + + @Override + public void onStart() { + super.onStart(); + ConversationActivity activity = (ConversationActivity) getActivity(); + + if (activity.xmppConnectionServiceBound) { + this.onBackendConnected(); + } + } + + public void onBackendConnected() { + final ConversationActivity activity = (ConversationActivity) getActivity(); + this.conversation = activity.getSelectedConversation(); + this.selfBitmap = findSelfPicture(); + updateMessages(); + // rendering complete. now go tell activity to close pane + if (!activity.shouldPaneBeOpen()) { + activity.getSlidingPaneLayout().closePane(); + activity.getActionBar().setDisplayHomeAsUpEnabled(true); + activity.getActionBar().setTitle(conversation.getName()); + activity.invalidateOptionsMenu(); + if (!conversation.isRead()) { + conversation.markRead(); + activity.updateConversationList(); + } + } + if (queuedPqpMessage != null) { + this.conversation.nextMessageEncryption = Message.ENCRYPTION_PGP; + Message message = new Message(conversation, queuedPqpMessage, + Message.ENCRYPTION_PGP); + sendPgpMessage(message); + } + } + + public void updateMessages() { + ConversationActivity activity = (ConversationActivity) getActivity(); + List encryptedMessages = new LinkedList(); + for(Message message : this.conversation.getMessages()) { + if (message.getEncryption() == Message.ENCRYPTION_PGP) { + encryptedMessages.add(message); + } + } + if (encryptedMessages.size() > 0) { + DecryptMessage task = new DecryptMessage(); + Message[] msgs = new Message[encryptedMessages.size()]; + task.execute(encryptedMessages.toArray(msgs)); + } + this.messageList.clear(); + this.messageList.addAll(this.conversation.getMessages()); + this.messageListAdapter.notifyDataSetChanged(); + if (messageList.size() >= 1) { + int latestEncryption = this.conversation.getLatestMessage() + .getEncryption(); + if (latestEncryption== Message.ENCRYPTION_DECRYPTED) { + conversation.nextMessageEncryption = Message.ENCRYPTION_PGP; + } else { + conversation.nextMessageEncryption = latestEncryption; + } + makeFingerprintWarning(latestEncryption); + } + getActivity().invalidateOptionsMenu(); + updateChatMsgHint(); + int size = this.messageList.size(); + if (size >= 1) + messagesView.setSelection(size - 1); + if (!activity.shouldPaneBeOpen()) { + conversation.markRead(); + activity.updateConversationList(); + } + } + + protected void makeFingerprintWarning(int latestEncryption) { + final LinearLayout fingerprintWarning = (LinearLayout) getView() + .findViewById(R.id.new_fingerprint); + if (conversation.getContact() != null) { + Set 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); + } + } else { + fingerprintWarning.setVisibility(View.GONE); + } + } + + protected void sendPlainTextMessage(Message message) { + ConversationActivity activity = (ConversationActivity) getActivity(); + activity.xmppConnectionService.sendMessage(message, null); + chatMsg.setText(""); + } + + protected void sendPgpMessage(final Message message) { + ConversationActivity activity = (ConversationActivity) getActivity(); + final XmppConnectionService xmppService = activity.xmppConnectionService; + Contact contact = message.getConversation().getContact(); + if (contact.getPgpKeyId() != 0) { + xmppService.sendMessage(message, null); + chatMsg.setText(""); + } else { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setTitle("No openPGP key found"); + builder.setIconAttribute(android.R.attr.alertDialogIcon); + builder.setMessage("There is no openPGP key assoziated with this contact"); + builder.setNegativeButton("Cancel", null); + builder.setPositiveButton("Send plain text", + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + conversation.nextMessageEncryption = Message.ENCRYPTION_NONE; + message.setEncryption(Message.ENCRYPTION_NONE); + xmppService.sendMessage(message, null); + chatMsg.setText(""); + } + }); + builder.create().show(); + } + } + + public void resendPgpMessage(String msg) { + this.queuedPqpMessage = msg; + } + + protected void sendOtrMessage(final Message message) { + ConversationActivity activity = (ConversationActivity) getActivity(); + final XmppConnectionService xmppService = activity.xmppConnectionService; + if (conversation.hasValidOtrSession()) { + activity.xmppConnectionService.sendMessage(message, null); + chatMsg.setText(""); + } else { + Hashtable presences; + if (conversation.getContact() != null) { + presences = conversation.getContact().getPresences(); + } else { + presences = null; + } + if ((presences == null) || (presences.size() == 0)) { + AlertDialog.Builder builder = new AlertDialog.Builder( + getActivity()); + builder.setTitle("Contact is offline"); + builder.setIconAttribute(android.R.attr.alertDialogIcon); + builder.setMessage("Sending OTR encrypted messages to an offline contact is impossible."); + builder.setPositiveButton("Send plain text", + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, + int which) { + conversation.nextMessageEncryption = Message.ENCRYPTION_NONE; + message.setEncryption(Message.ENCRYPTION_NONE); + xmppService.sendMessage(message, null); + chatMsg.setText(""); + } + }); + builder.setNegativeButton("Cancel", null); + builder.create().show(); + } else if (presences.size() == 1) { + xmppService.sendMessage(message, (String) presences.keySet() + .toArray()[0]); + chatMsg.setText(""); + } else { + AlertDialog.Builder builder = new AlertDialog.Builder( + getActivity()); + builder.setTitle("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) { + xmppService.sendMessage(message, + presencesArray[which]); + chatMsg.setText(""); + } + }); + builder.create().show(); + } + } + } + + private static class ViewHolder { + + protected TextView time; + protected TextView messageBody; + protected ImageView imageView; + + } + + private class BitmapCache { + private HashMap bitmaps = new HashMap(); + private Bitmap error = null; + + public Bitmap get(String name, Uri uri) { + if (bitmaps.containsKey(name)) { + return bitmaps.get(name); + } else { + Bitmap bm; + if (uri != null) { + try { + bm = BitmapFactory.decodeStream(getActivity() + .getContentResolver().openInputStream(uri)); + } catch (FileNotFoundException e) { + bm = UIHelper.getUnknownContactPicture(name, 200); + } + } else { + bm = UIHelper.getUnknownContactPicture(name, 200); + } + bitmaps.put(name, bm); + return bm; + } + } + + public Bitmap getError() { + if (error == null) { + error = UIHelper.getErrorPicture(200); + } + return error; + } + } + + class DecryptMessage extends AsyncTask { + + @Override + protected Boolean doInBackground(Message... params) { + final ConversationActivity activity = (ConversationActivity) getActivity(); + askForPassphraseIntent = null; + for(int i = 0; i < params.length; ++i) { + if (params[i].getEncryption() == Message.ENCRYPTION_PGP) { + String body = params[i].getBody(); + String decrypted = null; + try { + if (activity==null) { + return false; + } + Log.d("gultsch","calling to decrypt message id #"+params[i].getUuid()); + decrypted = activity.xmppConnectionService.getPgpEngine().decrypt(body); + } catch (UserInputRequiredException e) { + askForPassphraseIntent = e.getPendingIntent().getIntentSender(); + activity.runOnUiThread(new Runnable() { + + @Override + public void run() { + pgpInfo.setVisibility(View.VISIBLE); + } + }); + + return false; + + } catch (OpenPgpException e) { + Log.d("gultsch","error decrypting pgp"); + } + if (decrypted!=null) { + params[i].setBody(decrypted); + params[i].setEncryption(Message.ENCRYPTION_DECRYPTED); + activity.xmppConnectionService.updateMessage(params[i]); + } + if (activity!=null) { + activity.runOnUiThread(new Runnable() { + + @Override + public void run() { + messageListAdapter.notifyDataSetChanged(); + } + }); + } + } + if (activity!=null) { + activity.runOnUiThread(new Runnable() { + + @Override + public void run() { + activity.updateConversationList(); + } + }); + } + } + return true; + } + + } +} diff --git a/src/eu/siacs/conversations/ui/DialogContactDetails.java b/src/eu/siacs/conversations/ui/DialogContactDetails.java new file mode 100644 index 00000000..1210bc3c --- /dev/null +++ b/src/eu/siacs/conversations/ui/DialogContactDetails.java @@ -0,0 +1,218 @@ +package eu.siacs.conversations.ui; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Presences; +import eu.siacs.conversations.utils.UIHelper; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; +import android.provider.ContactsContract.CommonDataKinds; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.Intents; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.CheckBox; +import android.widget.QuickContactBadge; +import android.widget.TextView; + +public class DialogContactDetails extends DialogFragment { + + private Contact contact = null; + boolean displayingInRoster = false; + + private DialogContactDetails mDetailsDialog = this; + private XmppActivity activity; + + private CheckBox send; + private CheckBox receive; + + private DialogInterface.OnClickListener askRemoveFromRoster = new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setTitle("Delete from roster"); + builder.setMessage("Do you want to delete "+contact.getJid()+" from your roster. The conversation assoziated with this account will not be removed."); + builder.setNegativeButton("Cancel", null); + builder.setPositiveButton("Delete",removeFromRoster); + builder.create().show(); + } + }; + + private DialogInterface.OnClickListener removeFromRoster = new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + activity.xmppConnectionService.deleteContact(contact); + mDetailsDialog.dismiss(); + } + }; + + private DialogInterface.OnClickListener addToPhonebook = new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT); + intent.setType(Contacts.CONTENT_ITEM_TYPE); + intent.putExtra(Intents.Insert.IM_HANDLE,contact.getJid()); + intent.putExtra(Intents.Insert.IM_PROTOCOL,CommonDataKinds.Im.PROTOCOL_JABBER); + intent.putExtra("finishActivityOnSaveCompleted", true); + getActivity().startActivityForResult(intent,0); + mDetailsDialog.dismiss(); + } + }; + + private DialogInterface.OnClickListener updateSubscriptions = new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + boolean needsUpdating = false; + if (contact.getSubscriptionOption(Contact.Subscription.FROM)) { + if (!send.isChecked()) { + contact.resetSubscriptionOption(Contact.Subscription.FROM); + contact.resetSubscriptionOption(Contact.Subscription.PREEMPTIVE_GRANT); + activity.xmppConnectionService.stopPresenceUpdatesTo(contact); + needsUpdating=true; + } + } else { + if (contact.getSubscriptionOption(Contact.Subscription.PREEMPTIVE_GRANT)) { + if (!send.isChecked()) { + contact.resetSubscriptionOption(Contact.Subscription.PREEMPTIVE_GRANT); + needsUpdating=true; + } + } else { + if (send.isChecked()) { + contact.setSubscriptionOption(Contact.Subscription.PREEMPTIVE_GRANT); + needsUpdating=true; + } + } + } + if (contact.getSubscriptionOption(Contact.Subscription.TO)) { + if (!receive.isChecked()) { + contact.resetSubscriptionOption(Contact.Subscription.TO); + activity.xmppConnectionService.stopPresenceUpdatesFrom(contact); + needsUpdating=true; + } + } else { + if (contact.getSubscriptionOption(Contact.Subscription.ASKING)) { + if (!receive.isChecked()) { + contact.resetSubscriptionOption(Contact.Subscription.ASKING); + activity.xmppConnectionService.stopPresenceUpdatesFrom(contact); + needsUpdating=true; + } + } else { + if (receive.isChecked()) { + contact.setSubscriptionOption(Contact.Subscription.ASKING); + activity.xmppConnectionService.requestPresenceUpdatesFrom(contact); + needsUpdating=true; + } + } + } + if (needsUpdating) { + activity.xmppConnectionService.updateContact(contact); + } + } + }; + + public void setContact(Contact contact) { + this.contact = contact; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + this.activity = (XmppActivity) getActivity(); + AlertDialog.Builder builder = new AlertDialog.Builder(this.activity); + LayoutInflater inflater = getActivity().getLayoutInflater(); + View view = inflater.inflate(R.layout.dialog_contact_details, null); + TextView contactJid = (TextView) view.findViewById(R.id.details_contactjid); + TextView accountJid = (TextView) view.findViewById(R.id.details_account); + TextView status = (TextView) view.findViewById(R.id.details_contactstatus); + send = (CheckBox) view.findViewById(R.id.details_send_presence); + receive = (CheckBox) view.findViewById(R.id.details_receive_presence); + //ImageView contactPhoto = (ImageView) view.findViewById(R.id.details_contact_picture); + QuickContactBadge badge = (QuickContactBadge) view.findViewById(R.id.details_contact_badge); + + if (contact.getSubscriptionOption(Contact.Subscription.FROM)) { + send.setChecked(true); + } else { + send.setText("Preemptively grant subscription request"); + if (contact.getSubscriptionOption(Contact.Subscription.PREEMPTIVE_GRANT)) { + send.setChecked(true); + } else { + send.setChecked(false); + } + } + if (contact.getSubscriptionOption(Contact.Subscription.TO)) { + receive.setChecked(true); + } else { + receive.setText("Request presence updates"); + if (contact.getSubscriptionOption(Contact.Subscription.ASKING)) { + receive.setChecked(true); + } else { + receive.setChecked(false); + } + } + + switch (contact.getMostAvailableStatus()) { + case Presences.CHAT: + status.setText("free to chat"); + status.setTextColor(0xFF83b600); + break; + case Presences.ONLINE: + status.setText("online"); + status.setTextColor(0xFF83b600); + break; + case Presences.AWAY: + status.setText("away"); + status.setTextColor(0xFFffa713); + break; + case Presences.XA: + status.setText("extended away"); + status.setTextColor(0xFFffa713); + break; + case Presences.DND: + status.setText("do not disturb"); + status.setTextColor(0xFFe92727); + break; + case Presences.OFFLINE: + status.setText("offline"); + status.setTextColor(0xFFe92727); + break; + default: + status.setText("offline"); + status.setTextColor(0xFFe92727); + break; + } + contactJid.setText(contact.getJid()); + accountJid.setText(contact.getAccount().getJid()); + + UIHelper.prepareContactBadge(getActivity(), badge, contact); + + if (contact.getSystemAccount()==null) { + badge.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + 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.create().show(); + } + }); + } + + builder.setView(view); + builder.setTitle(contact.getDisplayName()); + + builder.setNeutralButton("Done", this.updateSubscriptions); + builder.setPositiveButton("Remove from roster", this.askRemoveFromRoster); + return builder.create(); + } +} diff --git a/src/eu/siacs/conversations/ui/EditAccount.java b/src/eu/siacs/conversations/ui/EditAccount.java new file mode 100644 index 00000000..3ec74174 --- /dev/null +++ b/src/eu/siacs/conversations/ui/EditAccount.java @@ -0,0 +1,138 @@ +package eu.siacs.conversations.ui; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.utils.Validator; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.CompoundButton.OnCheckedChangeListener; +import android.widget.EditText; +import android.widget.TextView; + +public class EditAccount extends DialogFragment { + + protected Account account; + + public void setAccount(Account account) { + this.account = account; + } + + public interface EditAccountListener { + public void onAccountEdited(Account account); + } + + protected EditAccountListener listener = null; + + public void setEditAccountListener(EditAccountListener listener) { + this.listener = listener; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + LayoutInflater inflater = getActivity().getLayoutInflater(); + View view = inflater.inflate(R.layout.edit_account_dialog, null); + final EditText jidText = (EditText) view.findViewById(R.id.account_jid); + final TextView confirmPwDesc = (TextView) view + .findViewById(R.id.account_confirm_password_desc); + CheckBox useTLS = (CheckBox) view.findViewById(R.id.account_usetls); + + final EditText password = (EditText) view + .findViewById(R.id.account_password); + final EditText passwordConfirm = (EditText) view + .findViewById(R.id.account_password_confirm2); + final CheckBox registerAccount = (CheckBox) view + .findViewById(R.id.edit_account_register_new); + + final String okButtonDesc; + + if (account != null) { + builder.setTitle("Edit account"); + registerAccount.setVisibility(View.GONE); + jidText.setText(account.getJid()); + password.setText(account.getPassword()); + okButtonDesc = "Edit"; + if (account.isOptionSet(Account.OPTION_USETLS)) { + useTLS.setChecked(true); + } else { + useTLS.setChecked(false); + } + } else { + builder.setTitle("Add account"); + okButtonDesc = "Add"; + } + + registerAccount + .setOnCheckedChangeListener(new OnCheckedChangeListener() { + + @Override + public void onCheckedChanged(CompoundButton buttonView, + boolean isChecked) { + AlertDialog d = (AlertDialog) getDialog(); + Button positiveButton = (Button) d + .getButton(Dialog.BUTTON_POSITIVE); + if (isChecked) { + positiveButton.setText("Register"); + passwordConfirm.setVisibility(View.VISIBLE); + confirmPwDesc.setVisibility(View.VISIBLE); + } else { + passwordConfirm.setVisibility(View.GONE); + positiveButton.setText("Add"); + confirmPwDesc.setVisibility(View.GONE); + } + } + }); + + builder.setView(view); + builder.setNeutralButton("Cancel", null); + builder.setPositiveButton(okButtonDesc, null); + return builder.create(); + } + + @Override + public void onStart() { + super.onStart(); + final AlertDialog d = (AlertDialog) getDialog(); + Button positiveButton = (Button) d.getButton(Dialog.BUTTON_POSITIVE); + positiveButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + EditText jidEdit = (EditText) d.findViewById(R.id.account_jid); + String jid = jidEdit.getText().toString(); + EditText passwordEdit = (EditText) d + .findViewById(R.id.account_password); + String password = passwordEdit.getText().toString(); + CheckBox useTLS = (CheckBox) d.findViewById(R.id.account_usetls); + String username; + String server; + if (Validator.isValidJid(jid)) { + String[] parts = jid.split("@"); + username = parts[0]; + server = parts[1]; + } else { + jidEdit.setError("Invalid Jabber ID"); + return; + } + if (account != null) { + account.setPassword(password); + account.setUsername(username); + account.setServer(server); + } else { + account = new Account(username, server, password); + } + account.setOption(Account.OPTION_USETLS, useTLS.isChecked()); + if (listener != null) { + listener.onAccountEdited(account); + d.dismiss(); + } + } + }); + } +} diff --git a/src/eu/siacs/conversations/ui/ManageAccountActivity.java b/src/eu/siacs/conversations/ui/ManageAccountActivity.java new file mode 100644 index 00000000..22b82e77 --- /dev/null +++ b/src/eu/siacs/conversations/ui/ManageAccountActivity.java @@ -0,0 +1,312 @@ +package eu.siacs.conversations.ui; + +import java.util.ArrayList; +import java.util.List; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.crypto.PgpEngine; +import eu.siacs.conversations.crypto.PgpEngine.UserInputRequiredException; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.ui.EditAccount.EditAccountListener; +import android.app.Activity; +import android.app.AlertDialog; +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.util.Log; +import android.view.ActionMode; +import android.view.ActionMode.Callback; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.AdapterView.OnItemLongClickListener; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.widget.TextView; + +public class ManageAccountActivity extends XmppActivity implements ActionMode.Callback { + + public static final int REQUEST_ANNOUNCE_PGP = 0x73731; + + protected boolean isActionMode = false; + protected ActionMode actionMode; + protected Account selectedAccountForActionMode = null; + + protected List accountList = new ArrayList(); + protected ListView accountListView; + protected ArrayAdapter accountListViewAdapter; + protected OnAccountListChangedListener accountChanged = new OnAccountListChangedListener() { + + @Override + public void onAccountListChangedListener() { + Log.d("xmppService", "ui on account list changed listener"); + accountList.clear(); + accountList.addAll(xmppConnectionService.getAccounts()); + runOnUiThread(new Runnable() { + + @Override + public void run() { + if (accountList.size() == 1) { + startActivity(new Intent(getApplicationContext(), + NewConversationActivity.class)); + } + accountListViewAdapter.notifyDataSetChanged(); + } + }); + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + + super.onCreate(savedInstanceState); + + setContentView(R.layout.manage_accounts); + + accountListView = (ListView) findViewById(R.id.account_list); + accountListViewAdapter = new ArrayAdapter( + getApplicationContext(), R.layout.account_row, this.accountList) { + @Override + public View getView(int position, View view, ViewGroup parent) { + Account account = getItem(position); + if (view == null) { + LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); + view = (View) inflater.inflate(R.layout.account_row, null); + } + ((TextView) view.findViewById(R.id.account_jid)) + .setText(account.getJid()); + TextView statusView = (TextView) view + .findViewById(R.id.account_status); + switch (account.getStatus()) { + case Account.STATUS_DISABLED: + statusView.setText("temporarily disabled"); + statusView.setTextColor(0xFF1da9da); + break; + case Account.STATUS_ONLINE: + statusView.setText("online"); + statusView.setTextColor(0xFF83b600); + break; + case Account.STATUS_OFFLINE: + statusView.setText("offline"); + statusView.setTextColor(0xFFe92727); + break; + case Account.STATUS_UNAUTHORIZED: + statusView.setText("unauthorized"); + statusView.setTextColor(0xFFe92727); + break; + case Account.STATUS_SERVER_NOT_FOUND: + statusView.setText("server not found"); + statusView.setTextColor(0xFFe92727); + break; + default: + break; + } + + return view; + } + }; + final Activity activity = this; + accountListView.setAdapter(this.accountListViewAdapter); + accountListView.setOnItemClickListener(new OnItemClickListener() { + + @Override + public void onItemClick(AdapterView arg0, View view, + int position, long arg3) { + if (!isActionMode) { + EditAccount dialog = new EditAccount(); + dialog.setAccount(accountList.get(position)); + dialog.setEditAccountListener(new EditAccountListener() { + + @Override + public void onAccountEdited(Account account) { + xmppConnectionService.updateAccount(account); + } + }); + dialog.show(getFragmentManager(), "edit_account"); + } else { + selectedAccountForActionMode = accountList.get(position); + actionMode.invalidate(); + } + } + }); + accountListView.setOnItemLongClickListener(new OnItemLongClickListener() { + + @Override + public boolean onItemLongClick(AdapterView arg0, View view, + int position, long arg3) { + if (!isActionMode) { + accountListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); + accountListView.setItemChecked(position,true); + selectedAccountForActionMode = accountList.get(position); + actionMode = activity.startActionMode((Callback) activity); + return true; + } else { + return false; + } + } + }); + } + + @Override + protected void onStop() { + if (xmppConnectionServiceBound) { + xmppConnectionService.removeOnAccountListChangedListener(); + } + super.onStop(); + } + + @Override + void onBackendConnected() { + xmppConnectionService.setOnAccountListChangedListener(accountChanged); + this.accountList.clear(); + this.accountList.addAll(xmppConnectionService.getAccounts()); + accountListViewAdapter.notifyDataSetChanged(); + if (this.accountList.size() == 0) { + getActionBar().setDisplayHomeAsUpEnabled(false); + addAccount(); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.manageaccounts, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_settings: + startActivity(new Intent(this, SettingsActivity.class)); + break; + case R.id.action_add_account: + addAccount(); + break; + default: + break; + } + return super.onOptionsItemSelected(item); + } + + protected void addAccount() { + final Activity activity = this; + EditAccount dialog = new EditAccount(); + dialog.setEditAccountListener(new EditAccountListener() { + + @Override + public void onAccountEdited(Account account) { + xmppConnectionService.createAccount(account); + activity.getActionBar().setDisplayHomeAsUpEnabled(true); + } + }); + dialog.show(getFragmentManager(), "add_account"); + } + + @Override + public boolean onActionItemClicked(final ActionMode mode, MenuItem item) { + if (item.getItemId()==R.id.account_disable) { + selectedAccountForActionMode.setOption(Account.OPTION_DISABLED, true); + xmppConnectionService.updateAccount(selectedAccountForActionMode); + mode.finish(); + } else if (item.getItemId()==R.id.account_enable) { + selectedAccountForActionMode.setOption(Account.OPTION_DISABLED, false); + xmppConnectionService.updateAccount(selectedAccountForActionMode); + mode.finish(); + } else if (item.getItemId()==R.id.account_delete) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("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() { + + @Override + public void onClick(DialogInterface dialog, int which) { + xmppConnectionService.deleteAccount(selectedAccountForActionMode); + selectedAccountForActionMode = null; + mode.finish(); + } + }); + builder.setNegativeButton("Cancel",null); + builder.create().show(); + } else if (item.getItemId()==R.id.announce_pgp) { + mode.finish(); + try { + xmppConnectionService.generatePgpAnnouncement(selectedAccountForActionMode); + } catch (PgpEngine.UserInputRequiredException e) { + try { + startIntentSenderForResult(e.getPendingIntent().getIntentSender(), REQUEST_ANNOUNCE_PGP, null, 0, 0, 0); + } catch (SendIntentException e1) { + Log.d("gultsch","sending intent failed"); + } + } + } + return true; + } + + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + MenuInflater inflater = mode.getMenuInflater(); + inflater.inflate(R.menu.manageaccounts_context, menu); + return true; + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + // TODO Auto-generated method stub + + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + if (selectedAccountForActionMode.isOptionSet(Account.OPTION_DISABLED)) { + menu.findItem(R.id.account_enable).setVisible(true); + menu.findItem(R.id.account_disable).setVisible(false); + } else { + menu.findItem(R.id.account_disable).setVisible(true); + menu.findItem(R.id.account_enable).setVisible(false); + } + return true; + } + + @Override + public void onActionModeStarted(ActionMode mode) { + super.onActionModeStarted(mode); + this.isActionMode = true; + } + + @Override + public void onActionModeFinished(ActionMode mode) { + super.onActionModeFinished(mode); + this.isActionMode = false; + accountListView.clearChoices(); + accountListView.requestLayout(); + accountListView.post(new Runnable() { + @Override + public void run() { + accountListView.setChoiceMode(ListView.CHOICE_MODE_NONE); + } + }); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (resultCode == RESULT_OK) { + if (requestCode == REQUEST_ANNOUNCE_PGP) { + try { + xmppConnectionService.generatePgpAnnouncement(selectedAccountForActionMode); + } catch (UserInputRequiredException e) { + Log.d("gultsch","already came back. ignoring"); + } + } + } + } +} diff --git a/src/eu/siacs/conversations/ui/NewConversationActivity.java b/src/eu/siacs/conversations/ui/NewConversationActivity.java new file mode 100644 index 00000000..628a3047 --- /dev/null +++ b/src/eu/siacs/conversations/ui/NewConversationActivity.java @@ -0,0 +1,332 @@ +package eu.siacs.conversations.ui; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +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.utils.UIHelper; +import eu.siacs.conversations.utils.Validator; +import android.net.Uri; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +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.annotation.SuppressLint; +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; + +public class NewConversationActivity extends XmppActivity { + + protected List phoneContacts = new ArrayList(); + protected List rosterContacts = new ArrayList(); + protected List aggregatedContacts = new ArrayList(); + protected ListView contactsView; + protected ArrayAdapter contactsAdapter; + + protected EditText search; + protected String searchString = ""; + private TextView contactsHeader; + private List accounts; + + protected void updateAggregatedContacts() { + + aggregatedContacts.clear(); + for (Contact contact : rosterContacts) { + if (contact.match(searchString)) + aggregatedContacts.add(contact); + } + + Collections.sort(aggregatedContacts, new Comparator() { + + @SuppressLint("DefaultLocale") + @Override + public int compare(Contact lhs, Contact rhs) { + return lhs.getDisplayName().toLowerCase() + .compareTo(rhs.getDisplayName().toLowerCase()); + } + }); + + if (aggregatedContacts.size() == 0) { + + if (Validator.isValidJid(searchString)) { + String name = searchString.split("@")[0]; + Contact newContact = new Contact(null, name, searchString, null); + newContact.flagAsNotInRoster(); + aggregatedContacts.add(newContact); + contactsHeader.setText("Create new contact"); + } else { + contactsHeader.setText("Contacts"); + } + } else { + contactsHeader.setText("Contacts"); + } + + contactsAdapter.notifyDataSetChanged(); + contactsView.setScrollX(0); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_new_conversation); + + contactsHeader = (TextView) findViewById(R.id.contacts_header); + + search = (EditText) findViewById(R.id.new_conversation_search); + search.addTextChangedListener(new TextWatcher() { + + @Override + public void onTextChanged(CharSequence s, int start, int before, + int count) { + searchString = search.getText().toString(); + updateAggregatedContacts(); + } + + @Override + public void afterTextChanged(Editable s) { + // TODO Auto-generated method stub + + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, + int after) { + // TODO Auto-generated method stub + + } + }); + + contactsView = (ListView) findViewById(R.id.contactList); + contactsAdapter = new ArrayAdapter(getApplicationContext(), + R.layout.contact, aggregatedContacts) { + @Override + public View getView(int position, View view, ViewGroup parent) { + LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); + Contact contact = getItem(position); + if (view == null) { + view = (View) inflater.inflate(R.layout.contact, null); + } + + ((TextView) view.findViewById(R.id.contact_display_name)) + .setText(getItem(position).getDisplayName()); + TextView contactJid = (TextView) view + .findViewById(R.id.contact_jid); + contactJid.setText(contact.getJid()); + String profilePhoto = getItem(position).getProfilePhoto(); + ImageView imageView = (ImageView) view + .findViewById(R.id.contact_photo); + if (profilePhoto != null) { + imageView.setImageURI(Uri.parse(profilePhoto)); + } else { + imageView.setImageBitmap(UIHelper.getUnknownContactPicture( + getItem(position).getDisplayName(), 90)); + } + return view; + } + }; + contactsView.setAdapter(contactsAdapter); + final Activity activity = this; + contactsView.setOnItemClickListener(new OnItemClickListener() { + + @Override + public void onItemClick(AdapterView arg0, final View view, + int pos, long arg3) { + final Contact clickedContact = aggregatedContacts.get(pos); + + if ((clickedContact.getAccount()==null)&&(accounts.size()>1)) { + String[] accountList = new String[accounts.size()]; + for (int i = 0; i < accounts.size(); ++i) { + accountList[i] = accounts.get(i).getJid(); + } + + AlertDialog.Builder accountChooser = new AlertDialog.Builder( + activity); + accountChooser.setTitle("Choose account"); + accountChooser.setItems(accountList, new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + clickedContact.setAccount(accounts.get(which)); + showIsMucDialogIfNeeded(clickedContact); + } + }); + accountChooser.create().show(); + } else { + if (clickedContact.getAccount()==null) { + clickedContact.setAccount(accounts.get(0)); + } + showIsMucDialogIfNeeded(clickedContact); + } + } + }); + contactsView.setOnItemLongClickListener(new OnItemLongClickListener() { + + @Override + public boolean onItemLongClick(AdapterView arg0, View arg1, + int pos, long arg3) { + Contact clickedContact = aggregatedContacts.get(pos); + DialogContactDetails dialog = new DialogContactDetails(); + dialog.setContact(clickedContact); + dialog.show(getFragmentManager(), "details"); + return true; + } + }); + } + + public void showIsMucDialogIfNeeded(final Contact clickedContact) { + 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() { + + @Override + public void onClick(DialogInterface dialog, int which) { + startConversation(clickedContact, clickedContact.getAccount(),true); + } + }); + dialog.setNegativeButton("No", new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + startConversation(clickedContact, clickedContact.getAccount(),false); + } + }); + dialog.create().show(); + } else { + startConversation(clickedContact, clickedContact.getAccount(),false); + } + } + + public void startConversation(Contact contact, Account account, boolean muc) { + if (!contact.isInRoster()) { + xmppConnectionService.createContact(contact); + } + Conversation conversation = xmppConnectionService + .findOrCreateConversation(account, contact.getJid(), muc); + + Intent viewConversationIntent = new Intent(this, + ConversationActivity.class); + viewConversationIntent.setAction(Intent.ACTION_VIEW); + viewConversationIntent.putExtra(ConversationActivity.CONVERSATION, + conversation.getUuid()); + viewConversationIntent.setType(ConversationActivity.VIEW_CONVERSATION); + viewConversationIntent.setFlags(viewConversationIntent.getFlags() + | Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(viewConversationIntent); + } + + @Override + void onBackendConnected() { + if (xmppConnectionService.getConversationCount() == 0) { + getActionBar().setDisplayHomeAsUpEnabled(false); + getActionBar().setHomeButtonEnabled(false); + } + this.accounts = xmppConnectionService.getAccounts(); + this.rosterContacts.clear(); + for (int i = 0; i < accounts.size(); ++i) { + xmppConnectionService.getRoster(accounts.get(i), + new OnRosterFetchedListener() { + + @Override + public void onRosterFetched(List roster) { + rosterContacts.addAll(roster); + runOnUiThread(new Runnable() { + + @Override + public void run() { + updateAggregatedContacts(); + } + }); + + } + }); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.newconversation, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_settings: + startActivity(new Intent(this, SettingsActivity.class)); + break; + case R.id.action_accounts: + startActivity(new Intent(this, ManageAccountActivity.class)); + break; + case 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 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(); + } + }); + } + }); + } + } + } +} diff --git a/src/eu/siacs/conversations/ui/OnAccountListChangedListener.java b/src/eu/siacs/conversations/ui/OnAccountListChangedListener.java new file mode 100644 index 00000000..98ef445e --- /dev/null +++ b/src/eu/siacs/conversations/ui/OnAccountListChangedListener.java @@ -0,0 +1,5 @@ +package eu.siacs.conversations.ui; + +public interface OnAccountListChangedListener { + public void onAccountListChangedListener(); +} diff --git a/src/eu/siacs/conversations/ui/OnConversationListChangedListener.java b/src/eu/siacs/conversations/ui/OnConversationListChangedListener.java new file mode 100644 index 00000000..2a922e21 --- /dev/null +++ b/src/eu/siacs/conversations/ui/OnConversationListChangedListener.java @@ -0,0 +1,5 @@ +package eu.siacs.conversations.ui; + +public interface OnConversationListChangedListener { + public void onConversationListChanged(); +} diff --git a/src/eu/siacs/conversations/ui/OnRosterFetchedListener.java b/src/eu/siacs/conversations/ui/OnRosterFetchedListener.java new file mode 100644 index 00000000..d69ce35b --- /dev/null +++ b/src/eu/siacs/conversations/ui/OnRosterFetchedListener.java @@ -0,0 +1,9 @@ +package eu.siacs.conversations.ui; + +import java.util.List; + +import eu.siacs.conversations.entities.Contact; + +public interface OnRosterFetchedListener { + public void onRosterFetched(List roster); +} diff --git a/src/eu/siacs/conversations/ui/SettingsActivity.java b/src/eu/siacs/conversations/ui/SettingsActivity.java new file mode 100644 index 00000000..abaf8c68 --- /dev/null +++ b/src/eu/siacs/conversations/ui/SettingsActivity.java @@ -0,0 +1,16 @@ +package eu.siacs.conversations.ui; + +import android.app.Activity; +import android.os.Bundle; + +public class SettingsActivity extends Activity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Display the fragment as the main content. + getFragmentManager().beginTransaction() + .replace(android.R.id.content, new SettingsFragment()).commit(); + } + +} diff --git a/src/eu/siacs/conversations/ui/SettingsFragment.java b/src/eu/siacs/conversations/ui/SettingsFragment.java new file mode 100644 index 00000000..7e1c3698 --- /dev/null +++ b/src/eu/siacs/conversations/ui/SettingsFragment.java @@ -0,0 +1,15 @@ +package eu.siacs.conversations.ui; + +import eu.siacs.conversations.R; +import android.os.Bundle; +import android.preference.PreferenceFragment; + +public class SettingsFragment extends PreferenceFragment { + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Load the preferences from an XML resource + addPreferencesFromResource(R.xml.preferences); + } +} diff --git a/src/eu/siacs/conversations/ui/XmppActivity.java b/src/eu/siacs/conversations/ui/XmppActivity.java new file mode 100644 index 00000000..5114e640 --- /dev/null +++ b/src/eu/siacs/conversations/ui/XmppActivity.java @@ -0,0 +1,52 @@ +package eu.siacs.conversations.ui; + +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder; +import android.app.Activity; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.IBinder; + +public abstract class XmppActivity extends Activity { + public XmppConnectionService xmppConnectionService; + public boolean xmppConnectionServiceBound = false; + protected boolean handledViewIntent = false; + protected ServiceConnection mConnection = new ServiceConnection() { + + @Override + public void onServiceConnected(ComponentName className, IBinder service) { + XmppConnectionBinder binder = (XmppConnectionBinder) service; + xmppConnectionService = binder.getService(); + xmppConnectionServiceBound = true; + onBackendConnected(); + } + + @Override + public void onServiceDisconnected(ComponentName arg0) { + xmppConnectionServiceBound = false; + } + }; + + @Override + protected void onStart() { + startService(new Intent(this, XmppConnectionService.class)); + super.onStart(); + if (!xmppConnectionServiceBound) { + Intent intent = new Intent(this, XmppConnectionService.class); + bindService(intent, mConnection, Context.BIND_AUTO_CREATE); + } + } + + @Override + protected void onStop() { + super.onStop(); + if (xmppConnectionServiceBound) { + unbindService(mConnection); + xmppConnectionServiceBound = false; + } + } + + abstract void onBackendConnected(); +} diff --git a/src/eu/siacs/conversations/utils/DNSHelper.java b/src/eu/siacs/conversations/utils/DNSHelper.java new file mode 100644 index 00000000..46fd6928 --- /dev/null +++ b/src/eu/siacs/conversations/utils/DNSHelper.java @@ -0,0 +1,93 @@ +package eu.siacs.conversations.utils; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.util.Random; + +import android.os.Bundle; +import android.util.Log; + +public class DNSHelper { + public static Bundle getSRVRecord(String host) { + Bundle namePort = new Bundle(); + try { + String[] hostParts = host.split("\\."); + byte[] transId = new byte[2]; + Random random = new Random(); + random.nextBytes(transId); + byte[] header = { 0x01, 0x20, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x0c, 0x5f, 0x78, 0x6d, 0x70, 0x70, 0x2d, 0x63, + 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x04, 0x5f, 0x74, 0x63, 0x70 }; + byte[] rest = { 0x00, 0x00, 0x21, 0x00, 0x01, 0x00, 0x00, 0x29, + 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + ByteArrayOutputStream output = new ByteArrayOutputStream(); + output.write(transId); + output.write(header); + for (int i = 0; i < hostParts.length; ++i) { + char[] tmpChars = hostParts[i].toCharArray(); + byte[] tmp = new byte[tmpChars.length]; + for (int j = 0; j < tmpChars.length; ++j) { + tmp[j] = (byte) tmpChars[j]; + } + output.write(tmp.length); + output.write(tmp); + } + output.write(rest); + byte[] sendPaket = output.toByteArray(); + byte[] addr = { 0x8, 0x8, 0x8, 0x8 }; + int realLenght = sendPaket.length - 11; + DatagramPacket packet = new DatagramPacket(sendPaket, + sendPaket.length, InetAddress.getByAddress(addr), 53); + DatagramSocket datagramSocket = new DatagramSocket(); + datagramSocket.send(packet); + byte[] receiveData = new byte[1024]; + + DatagramPacket receivePacket = new DatagramPacket(receiveData, + receiveData.length); + datagramSocket.setSoTimeout(2000); + datagramSocket.receive(receivePacket); + if (receiveData[3]!=-128) { + namePort.putString("error", "nosrv"); + return namePort; + } + namePort.putInt("port",calcPort(receiveData[realLenght + 16], + receiveData[realLenght + 17])); + int i = realLenght + 18; + int wordLenght = 0; + StringBuilder builder = new StringBuilder(); + while (receiveData[i] != 0) { + if (wordLenght > 0) { + builder.append((char) receiveData[i]); + --wordLenght; + } else { + wordLenght = receiveData[i]; + builder.append("."); + } + ++i; + } + builder.replace(0, 1, ""); + namePort.putString("name",builder.toString()); + } catch (IOException e) { + Log.d("xmppService","gut" + e.getMessage()); + } + return namePort; + } + + static int calcPort(byte hb, byte lb) { + return ((int) hb << 8) | ((int) lb & 0xFF); + } + + final protected static char[] hexArray = "0123456789ABCDEF".toCharArray(); + public static String bytesToHex(byte[] bytes) { + char[] hexChars = new char[bytes.length * 2]; + for ( int j = 0; j < bytes.length; j++ ) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = hexArray[v >>> 4]; + hexChars[j * 2 + 1] = hexArray[v & 0x0F]; + } + return new String(hexChars); + } +} diff --git a/src/eu/siacs/conversations/utils/MessageParser.java b/src/eu/siacs/conversations/utils/MessageParser.java new file mode 100644 index 00000000..dc0cd35c --- /dev/null +++ b/src/eu/siacs/conversations/utils/MessageParser.java @@ -0,0 +1,149 @@ +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.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) { + String[] fromParts = packet.getFrom().split("/"); + Conversation conversation = service.findOrCreateConversation(account, fromParts[0],false); + String body = packet.getBody(); + if (!conversation.hasValidOtrSession()) { + conversation.startOtrSession(service.getApplicationContext(), fromParts[1]); + } + 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 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); + } + } + if (service.convChangedListener!=null) { + service.convChangedListener.onConversationListChanged(); + } + } else if ((before != after) && (after == SessionStatus.FINISHED)) { + conversation.resetOtrSession(); + Log.d(LOGTAG,"otr session stoped"); + } + } catch (Exception e) { + Log.d(LOGTAG, "error receiving otr. resetting"); + conversation.resetOtrSession(); + return null; + } + if (body == null) { + return null; + } + return new Message(conversation, packet.getFrom(), body, Message.ENCRYPTION_OTR,Message.STATUS_RECIEVED); + } + + 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 ((fromParts.length == 1) || (packet.hasChild("subject"))) { + return null; + } + String counterPart = fromParts[1]; + if (counterPart.equals(account.getUsername())) { + 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) { + // TODO Auto-generated method stub + 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; + } + 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 Message parseError(MessagePacket packet, Account account, XmppConnectionService service) { + + String[] fromParts = packet.getFrom().split("/"); + Conversation conversation = service.findOrCreateConversation(account, fromParts[0],false); + Element error = packet.findChild("error"); + String errorName = error.getChildren().get(0).getName(); + String displayError; + if (errorName.equals("service-unavailable")) { + displayError = "Contact is offline and does not have offline storage"; + } else { + displayError = errorName.replace("-", " "); + } + return new Message(conversation, packet.getFrom(), displayError, Message.ENCRYPTION_NONE, Message.STATUS_ERROR); + } + + 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 new file mode 100644 index 00000000..fa8cea04 --- /dev/null +++ b/src/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java @@ -0,0 +1,9 @@ +package eu.siacs.conversations.utils; + +import java.util.Hashtable; + +import android.os.Bundle; + +public interface OnPhoneContactsLoadedListener { + public void onPhoneContactsLoaded(Hashtable phoneContacts); +} diff --git a/src/eu/siacs/conversations/utils/PhoneHelper.java b/src/eu/siacs/conversations/utils/PhoneHelper.java new file mode 100644 index 00000000..e28f817e --- /dev/null +++ b/src/eu/siacs/conversations/utils/PhoneHelper.java @@ -0,0 +1,87 @@ +package eu.siacs.conversations.utils; + +import java.util.Hashtable; + +import android.app.Activity; +import android.content.Context; +import android.content.CursorLoader; +import android.content.Loader; +import android.content.Loader.OnLoadCompleteListener; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.os.Looper; +import android.provider.ContactsContract; +import android.provider.ContactsContract.Profile; + +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 phoneContacts = new Hashtable(); + + final String[] PROJECTION = new String[] { + ContactsContract.Data._ID, + ContactsContract.Data.DISPLAY_NAME, + ContactsContract.Data.PHOTO_THUMBNAIL_URI, + ContactsContract.Data.LOOKUP_KEY, + ContactsContract.CommonDataKinds.Im.DATA }; + + final String SELECTION = "(" + ContactsContract.Data.MIMETYPE + "=\"" + + ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE + + "\") AND (" + ContactsContract.CommonDataKinds.Im.PROTOCOL + + "=\"" + ContactsContract.CommonDataKinds.Im.PROTOCOL_JABBER + + "\")"; + + CursorLoader mCursorLoader = new CursorLoader(context, + ContactsContract.Data.CONTENT_URI, PROJECTION, SELECTION, null, + null); + mCursorLoader.registerListener(0, new OnLoadCompleteListener() { + + @Override + public void onLoadComplete(Loader arg0, Cursor cursor) { + while (cursor.moveToNext()) { + Bundle contact = new Bundle(); + contact.putInt("phoneid", cursor.getInt(cursor + .getColumnIndex(ContactsContract.Data._ID))); + contact.putString( + "displayname", + cursor.getString(cursor + .getColumnIndex(ContactsContract.Data.DISPLAY_NAME))); + contact.putString( + "photouri", + cursor.getString(cursor + .getColumnIndex(ContactsContract.Data.PHOTO_THUMBNAIL_URI))); + contact.putString("lookup",cursor.getString(cursor + .getColumnIndex(ContactsContract.Data.LOOKUP_KEY))); + phoneContacts.put( + cursor.getString(cursor + .getColumnIndex(ContactsContract.CommonDataKinds.Im.DATA)), + contact); + } + if (listener!=null) { + listener.onPhoneContactsLoaded(phoneContacts); + } + mLooper.quit(); + } + }); + mCursorLoader.startLoading(); + } + + public static Uri getSefliUri(Activity activity) { + String[] mProjection = new String[] { Profile._ID, + Profile.PHOTO_THUMBNAIL_URI }; + Cursor mProfileCursor = activity.getContentResolver().query( + Profile.CONTENT_URI, mProjection, null, null, null); + + if (mProfileCursor.getCount()==0) { + return null; + } else { + mProfileCursor.moveToFirst(); + return Uri.parse(mProfileCursor.getString(1)); + } + } +} diff --git a/src/eu/siacs/conversations/utils/SASL.java b/src/eu/siacs/conversations/utils/SASL.java new file mode 100644 index 00000000..cda1f97b --- /dev/null +++ b/src/eu/siacs/conversations/utils/SASL.java @@ -0,0 +1,24 @@ +package eu.siacs.conversations.utils; + +import android.util.Base64; + +public class SASL { + public static String plain(String username, String password) { + byte[] userBytes = username.getBytes(); + int userLenght = userBytes.length; + byte[] passwordBytes = password.getBytes(); + byte[] saslBytes = new byte[userBytes.length+passwordBytes.length+2]; + saslBytes[0] = 0x0; + for(int i = 1; i < saslBytes.length; ++i) { + if (i<=userLenght) { + saslBytes[i] = userBytes[i-1]; + } else if (i==userLenght+1) { + saslBytes[i] = 0x0; + } else { + saslBytes[i] = passwordBytes[i-(userLenght+2)]; + } + } + + return Base64.encodeToString(saslBytes, Base64.DEFAULT); + } +} diff --git a/src/eu/siacs/conversations/utils/UIHelper.java b/src/eu/siacs/conversations/utils/UIHelper.java new file mode 100644 index 00000000..f79fe14c --- /dev/null +++ b/src/eu/siacs/conversations/utils/UIHelper.java @@ -0,0 +1,210 @@ +package eu.siacs.conversations.utils; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; + +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.ui.ConversationActivity; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Notification; +import android.app.PendingIntent; +import android.content.Context; +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.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.net.Uri; +import android.preference.PreferenceManager; +import android.provider.ContactsContract.Contacts; +import android.support.v4.app.NotificationCompat; +import android.support.v4.app.TaskStackBuilder; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.QuickContactBadge; +import android.widget.TextView; + +public class UIHelper { + public static String readableTimeDifference(long time) { + if (time == 0) { + return "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"); + return sdf.format(date); + } else { + SimpleDateFormat sdf = new SimpleDateFormat("MM/dd"); + return sdf.format(date); + } + } + + public static Bitmap getUnknownContactPicture(String name, int size) { + String firstLetter = name.substring(0, 1).toUpperCase(); + + int holoColors[] = { 0xFF1da9da, 0xFFb368d9, 0xFF83b600, 0xFFffa713, + 0xFFe92727 }; + + int color = holoColors[Math.abs(name.hashCode()) % holoColors.length]; + + Bitmap bitmap = Bitmap + .createBitmap(size, size, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + + bitmap.eraseColor(color); + + Paint paint = new Paint(); + paint.setColor(0xffe5e5e5); + paint.setTextSize((float) (size * 0.9)); + paint.setAntiAlias(true); + Rect rect = new Rect(); + paint.getTextBounds(firstLetter, 0, 1, rect); + float width = paint.measureText(firstLetter); + canvas.drawText(firstLetter, (size / 2) - (width / 2), (size / 2) + + (rect.height() / 2), paint); + + return bitmap; + } + + public static Bitmap getErrorPicture(int size) { + Bitmap bitmap = Bitmap + .createBitmap(size, size, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + + bitmap.eraseColor(0xFFe92727); + + Paint paint = new Paint(); + paint.setColor(0xffe5e5e5); + paint.setTextSize((float) (size * 0.9)); + paint.setAntiAlias(true); + Rect rect = new Rect(); + paint.getTextBounds("!", 0, 1, rect); + float width = paint.measureText("!"); + canvas.drawText("!", (size / 2) - (width / 2), (size / 2) + + (rect.height() / 2), paint); + + return bitmap; + } + + public static Notification getUnreadMessageNotification(Context context, + Conversation conversation) { + + SharedPreferences sharedPref = PreferenceManager + .getDefaultSharedPreferences(context); + String ringtone = sharedPref.getString("notification_ringtone", null); + + Resources res = context.getResources(); + NotificationCompat.Builder mBuilder = new NotificationCompat.Builder( + context); + mBuilder.setLargeIcon(UIHelper.getUnknownContactPicture(conversation + .getName(), (int) res + .getDimension(android.R.dimen.notification_large_icon_width))); + mBuilder.setContentTitle(conversation.getName()); + mBuilder.setTicker(conversation.getLatestMessage().getBody().trim()); + StringBuilder bigText = new StringBuilder(); + List messages = conversation.getMessages(); + String firstLine = ""; + for(int i = messages.size() -1; i >= 0; --i) { + if (!messages.get(i).isRead()) { + if (i == messages.size() -1 ) { + firstLine = messages.get(i).getBody().trim(); + bigText.append(firstLine); + } else { + firstLine = messages.get(i).getBody().trim(); + bigText.insert(0, firstLine+"\n"); + } + } else { + break; + } + } + mBuilder.setContentText(firstLine); + mBuilder.setStyle(new NotificationCompat.BigTextStyle().bigText(bigText.toString())); + mBuilder.setSmallIcon(R.drawable.notification); + mBuilder.setLights(0xffffffff, 2000, 4000); + if (ringtone != null) { + mBuilder.setSound(Uri.parse(ringtone)); + } + + TaskStackBuilder stackBuilder = TaskStackBuilder.create(context); + stackBuilder.addParentStack(ConversationActivity.class); + + Intent viewConversationIntent = new Intent(context, + ConversationActivity.class); + viewConversationIntent.setAction(Intent.ACTION_VIEW); + viewConversationIntent.putExtra(ConversationActivity.CONVERSATION, + conversation.getUuid()); + viewConversationIntent.setType(ConversationActivity.VIEW_CONVERSATION); + + stackBuilder.addNextIntent(viewConversationIntent); + + PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0, + PendingIntent.FLAG_UPDATE_CURRENT); + + mBuilder.setContentIntent(resultPendingIntent); + return mBuilder.build(); + } + + public static void prepareContactBadge(final Activity activity, + QuickContactBadge badge, final Contact contact) { + if (contact.getSystemAccount()!=null) { + String[] systemAccount = contact.getSystemAccount().split("#"); + long id = Long.parseLong(systemAccount[0]); + badge.assignContactUri(Contacts.getLookupUri(id, systemAccount[1])); + + if (contact.getProfilePhoto() != null) { + badge.setImageURI(Uri.parse(contact.getProfilePhoto())); + } else { + badge.setImageBitmap(UIHelper.getUnknownContactPicture(contact.getDisplayName(), 400)); + } + } else { + badge.setImageBitmap(UIHelper.getUnknownContactPicture(contact.getDisplayName(), 400)); + } + + } + + public static AlertDialog getVerifyFingerprintDialog(final ConversationActivity activity,final Conversation conversation, final LinearLayout msg) { + final Contact contact = conversation.getContact(); + final Account account = conversation.getAccount(); + + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setTitle("Verify fingerprint"); + LayoutInflater inflater = activity.getLayoutInflater(); + View view = inflater.inflate(R.layout.dialog_verify_otr, null); + TextView jid = (TextView) view.findViewById(R.id.verify_otr_jid); + TextView fingerprint = (TextView) view.findViewById(R.id.verify_otr_fingerprint); + TextView yourprint = (TextView) view.findViewById(R.id.verify_otr_yourprint); + + jid.setText(contact.getJid()); + fingerprint.setText(conversation.getOtrFingerprint()); + yourprint.setText(account.getOtrFingerprint()); + builder.setNegativeButton("Cancel", null); + builder.setPositiveButton("Verify", new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + contact.addOtrFingerprint(conversation.getOtrFingerprint()); + msg.setVisibility(View.GONE); + activity.xmppConnectionService.updateContact(contact); + } + }); + builder.setView(view); + return builder.create(); + } +} diff --git a/src/eu/siacs/conversations/utils/Validator.java b/src/eu/siacs/conversations/utils/Validator.java new file mode 100644 index 00000000..fce953ae --- /dev/null +++ b/src/eu/siacs/conversations/utils/Validator.java @@ -0,0 +1,14 @@ +package eu.siacs.conversations.utils; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class Validator { + public static final Pattern VALID_JID = + Pattern.compile("^[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); + return matcher.find(); + } +} diff --git a/src/eu/siacs/conversations/xml/Element.java b/src/eu/siacs/conversations/xml/Element.java new file mode 100644 index 00000000..ad95ef9c --- /dev/null +++ b/src/eu/siacs/conversations/xml/Element.java @@ -0,0 +1,101 @@ +package eu.siacs.conversations.xml; + +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.List; + +import android.util.Log; + +public class Element { + protected String name; + protected Hashtable attributes = new Hashtable(); + protected String content; + protected List children = new ArrayList(); + + public Element(String name) { + this.name = name; + } + + public Element addChild(Element child) { + this.content = null; + children.add(child); + return this; + } + + public Element setContent(String content) { + this.content = content; + this.children.clear(); + return this; + } + + public Element findChild(String name) { + for(Element child : this.children) { + if (child.getName().equals(name)) { + return child; + } + } + return null; + } + + public boolean hasChild(String name) { + for(Element child : this.children) { + if (child.getName().equals(name)) { + return true; + } + } + return false; + } + + public List getChildren() { + return this.children; + } + + public String getContent() { + return content; + } + + public Element setAttribute(String name, String value) { + this.attributes.put(name, value); + return this; + } + + public Element setAttributes(Hashtable attributes) { + this.attributes = attributes; + return this; + } + + public String getAttribute(String name) { + if (this.attributes.containsKey(name)) { + return this.attributes.get(name); + } else { + return null; + } + } + + public String toString() { + StringBuilder elementOutput = new StringBuilder(); + if ((content==null)&&(children.size() == 0)) { + Tag emptyTag = Tag.empty(name); + emptyTag.setAtttributes(this.attributes); + elementOutput.append(emptyTag.toString()); + } else { + Tag startTag = Tag.start(name); + startTag.setAtttributes(this.attributes); + elementOutput.append(startTag); + if (content!=null) { + elementOutput.append(content); + } else { + for(Element child : children) { + elementOutput.append(child.toString()); + } + } + Tag endTag = Tag.end(name); + elementOutput.append(endTag); + } + return elementOutput.toString(); + } + + public String getName() { + return name; + } +} diff --git a/src/eu/siacs/conversations/xml/Tag.java b/src/eu/siacs/conversations/xml/Tag.java new file mode 100644 index 00000000..970cf0ae --- /dev/null +++ b/src/eu/siacs/conversations/xml/Tag.java @@ -0,0 +1,99 @@ +package eu.siacs.conversations.xml; + +import java.util.Hashtable; +import java.util.Iterator; +import java.util.Map.Entry; +import java.util.Set; + +public class Tag { + public static final int NO = -1; + public static final int START = 0; + public static final int END = 1; + public static final int EMPTY = 2; + + protected int type; + protected String name; + protected Hashtable attributes = new Hashtable(); + + protected Tag(int type, String name) { + this.type = type; + this.name = name; + } + + + public static Tag no(String text) { + return new Tag(NO,text); + } + + public static Tag start(String name) { + return new Tag(START,name); + } + + public static Tag end(String name) { + return new Tag(END,name); + } + + public static Tag empty(String name) { + return new Tag(EMPTY,name); + } + + public String getName() { + return name; + } + + public String getAttribute(String attrName) { + return this.attributes.get(attrName); + } + + public Tag setAttribute(String attrName, String attrValue) { + this.attributes.put(attrName, attrValue); + return this; + } + + public Tag setAtttributes(Hashtable attributes) { + this.attributes = attributes; + return this; + } + + public boolean isStart(String needle) { + return (this.type == START) && (this.name.equals(needle)); + } + + public boolean isEnd(String needle) { + return (this.type == END) && (this.name.equals(needle)); + } + + public boolean isNo() { + return (this.type == NO); + } + + public String toString() { + StringBuilder tagOutput = new StringBuilder(); + tagOutput.append('<'); + if (type==END) { + tagOutput.append('/'); + } + tagOutput.append(name); + if(type!=END) { + Set> attributeSet = attributes.entrySet(); + Iterator> it = attributeSet.iterator(); + while(it.hasNext()) { + Entry entry = it.next(); + tagOutput.append(' '); + tagOutput.append(entry.getKey()); + tagOutput.append("=\""); + tagOutput.append(entry.getValue()); + tagOutput.append('"'); + } + } + if (type==EMPTY) { + tagOutput.append('/'); + } + tagOutput.append('>'); + return tagOutput.toString(); + } + + public Hashtable getAttributes() { + return this.attributes; + } +} diff --git a/src/eu/siacs/conversations/xml/TagWriter.java b/src/eu/siacs/conversations/xml/TagWriter.java new file mode 100644 index 00000000..109078ca --- /dev/null +++ b/src/eu/siacs/conversations/xml/TagWriter.java @@ -0,0 +1,64 @@ +package eu.siacs.conversations.xml; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.util.concurrent.LinkedBlockingQueue; + +import android.util.Log; + +public class TagWriter { + + private OutputStreamWriter outputStream; + private LinkedBlockingQueue writeQueue = new LinkedBlockingQueue(); + private Thread writer = new Thread() { + public boolean shouldStop = false; + @Override + public void run() { + while(!shouldStop) { + try { + String output = writeQueue.take(); + outputStream.write(output); + outputStream.flush(); + } catch (IOException e) { + Log.d("xmppService", "error writing to stream"); + } catch (InterruptedException e) { + + } + } + } + }; + + + public TagWriter() { + + } + + public TagWriter(OutputStream out) { + this.setOutputStream(out); + writer.start(); + } + + public void setOutputStream(OutputStream out) { + this.outputStream = new OutputStreamWriter(out); + if (!writer.isAlive()) writer.start(); + } + + public TagWriter beginDocument() { + writeQueue.add(""); + return this; + } + + public TagWriter writeTag(Tag tag) { + writeQueue.add(tag.toString()); + return this; + } + + public void writeString(String string) { + writeQueue.add(string); + } + + public void writeElement(Element element) { + writeQueue.add(element.toString()); + } +} diff --git a/src/eu/siacs/conversations/xml/XmlReader.java b/src/eu/siacs/conversations/xml/XmlReader.java new file mode 100644 index 00000000..131141dd --- /dev/null +++ b/src/eu/siacs/conversations/xml/XmlReader.java @@ -0,0 +1,102 @@ +package eu.siacs.conversations.xml; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; +import android.util.Log; +import android.util.Xml; + +public class XmlReader { + private static final String LOGTAG = "xmppService"; + private XmlPullParser parser; + private PowerManager.WakeLock wakeLock; + private InputStream is; + + public XmlReader(WakeLock wakeLock) { + this.parser = Xml.newPullParser(); + try { + this.parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES,true); + } catch (XmlPullParserException e) { + Log.d(LOGTAG,"error setting namespace feature on parser"); + } + this.wakeLock = wakeLock; + } + + public void setInputStream(InputStream inputStream) { + this.is = inputStream; + try { + parser.setInput(new InputStreamReader(this.is)); + } catch (XmlPullParserException e) { + Log.d(LOGTAG,"error setting input stream"); + } + } + + public void reset() { + try { + parser.setInput(new InputStreamReader(this.is)); + } catch (XmlPullParserException e) { + Log.d(LOGTAG,"error resetting input stream"); + } + } + + public Tag readTag() throws XmlPullParserException, IOException { + if (wakeLock.isHeld()) { + //Log.d(LOGTAG,"there was a wake lock. releasing it till next event"); + wakeLock.release(); //release wake look while waiting on next parser event + } + //Log.d(LOGTAG,"waiting for new event..."); + while(parser.next() != XmlPullParser.END_DOCUMENT) { + //Log.d(LOGTAG,"found new event. acquiring wake lock"); + wakeLock.acquire(); + if (parser.getEventType() == XmlPullParser.START_TAG) { + Tag tag = Tag.start(parser.getName()); + for(int i = 0; i < parser.getAttributeCount(); ++i) { + tag.setAttribute(parser.getAttributeName(i), parser.getAttributeValue(i)); + } + String xmlns = parser.getNamespace(); + if (xmlns!=null) { + tag.setAttribute("xmlns",xmlns); + } + return tag; + } else if (parser.getEventType() == XmlPullParser.END_TAG) { + Tag tag = Tag.end(parser.getName()); + return tag; + } else if (parser.getEventType() == XmlPullParser.TEXT) { + Tag tag = Tag.no(parser.getText()); + return tag; + } + } + if (wakeLock.isHeld()) { + wakeLock.release(); + } + return null; //end document; + } + + 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(); + } + //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(); + } + //Log.d(LOGTAG,"return with element"+element); + return element; + } +} diff --git a/src/eu/siacs/conversations/xmpp/IqPacket.java b/src/eu/siacs/conversations/xmpp/IqPacket.java new file mode 100644 index 00000000..2319fd28 --- /dev/null +++ b/src/eu/siacs/conversations/xmpp/IqPacket.java @@ -0,0 +1,40 @@ +package eu.siacs.conversations.xmpp; + +import eu.siacs.conversations.xml.Element; + +public class IqPacket extends Element { + + public static final int TYPE_SET = 0; + public static final int TYPE_RESULT = 1; + public static final int TYPE_GET = 2; + + private IqPacket(String name) { + super(name); + } + + public IqPacket(int type) { + super("iq"); + switch (type) { + case TYPE_SET: + this.setAttribute("type", "set"); + break; + case TYPE_GET: + this.setAttribute("type", "get"); + break; + case TYPE_RESULT: + this.setAttribute("type", "result"); + break; + default: + break; + } + } + + public IqPacket() { + super("iq"); + } + + public String getId() { + return this.getAttribute("id"); + } + +} diff --git a/src/eu/siacs/conversations/xmpp/MessagePacket.java b/src/eu/siacs/conversations/xmpp/MessagePacket.java new file mode 100644 index 00000000..a014155f --- /dev/null +++ b/src/eu/siacs/conversations/xmpp/MessagePacket.java @@ -0,0 +1,81 @@ +package eu.siacs.conversations.xmpp; + +import eu.siacs.conversations.xml.Element; + +public class MessagePacket extends Element { + public static final int TYPE_CHAT = 0; + public static final int TYPE_UNKNOWN = 1; + public static final int TYPE_NO = 2; + public static final int TYPE_GROUPCHAT = 3; + public static final int TYPE_ERROR = 4; + + private MessagePacket(String name) { + super(name); + } + + public MessagePacket() { + super("message"); + } + + public String getTo() { + return getAttribute("to"); + } + + public String getFrom() { + return getAttribute("from"); + } + + public String getBody() { + Element body = this.findChild("body"); + if (body!=null) { + return body.getContent(); + } else { + return null; + } + } + + public void setTo(String to) { + setAttribute("to", to); + } + + public void setFrom(String from) { + setAttribute("from",from); + } + + public void setBody(String text) { + this.children.remove(findChild("body")); + Element body = new Element("body"); + body.setContent(text); + this.children.add(body); + } + + public void setType(int type) { + switch (type) { + case TYPE_CHAT: + this.setAttribute("type","chat"); + break; + case TYPE_GROUPCHAT: + this.setAttribute("type", "groupchat"); + break; + default: + this.setAttribute("type","chat"); + break; + } + } + + public int getType() { + String type = getAttribute("type"); + if (type==null) { + return TYPE_NO; + } + if (type.equals("chat")) { + return TYPE_CHAT; + } else if (type.equals("groupchat")) { + return TYPE_GROUPCHAT; + } else if (type.equals("error")) { + return TYPE_ERROR; + } else { + return TYPE_UNKNOWN; + } + } +} diff --git a/src/eu/siacs/conversations/xmpp/OnIqPacketReceived.java b/src/eu/siacs/conversations/xmpp/OnIqPacketReceived.java new file mode 100644 index 00000000..4e09282c --- /dev/null +++ b/src/eu/siacs/conversations/xmpp/OnIqPacketReceived.java @@ -0,0 +1,7 @@ +package eu.siacs.conversations.xmpp; + +import eu.siacs.conversations.entities.Account; + +public interface OnIqPacketReceived { + public void onIqPacketReceived(Account account, IqPacket packet); +} diff --git a/src/eu/siacs/conversations/xmpp/OnMessagePacketReceived.java b/src/eu/siacs/conversations/xmpp/OnMessagePacketReceived.java new file mode 100644 index 00000000..3d169300 --- /dev/null +++ b/src/eu/siacs/conversations/xmpp/OnMessagePacketReceived.java @@ -0,0 +1,7 @@ +package eu.siacs.conversations.xmpp; + +import eu.siacs.conversations.entities.Account; + +public interface OnMessagePacketReceived { + public void onMessagePacketReceived(Account account, MessagePacket packet); +} diff --git a/src/eu/siacs/conversations/xmpp/OnPresencePacketReceived.java b/src/eu/siacs/conversations/xmpp/OnPresencePacketReceived.java new file mode 100644 index 00000000..058d8be9 --- /dev/null +++ b/src/eu/siacs/conversations/xmpp/OnPresencePacketReceived.java @@ -0,0 +1,7 @@ +package eu.siacs.conversations.xmpp; + +import eu.siacs.conversations.entities.Account; + +public interface OnPresencePacketReceived { + public void onPresencePacketReceived(Account account, PresencePacket packet); +} diff --git a/src/eu/siacs/conversations/xmpp/OnStatusChanged.java b/src/eu/siacs/conversations/xmpp/OnStatusChanged.java new file mode 100644 index 00000000..ad1d98cb --- /dev/null +++ b/src/eu/siacs/conversations/xmpp/OnStatusChanged.java @@ -0,0 +1,7 @@ +package eu.siacs.conversations.xmpp; + +import eu.siacs.conversations.entities.Account; + +public interface OnStatusChanged { + public void onStatusChanged(Account account); +} diff --git a/src/eu/siacs/conversations/xmpp/PresencePacket.java b/src/eu/siacs/conversations/xmpp/PresencePacket.java new file mode 100644 index 00000000..3d77ce15 --- /dev/null +++ b/src/eu/siacs/conversations/xmpp/PresencePacket.java @@ -0,0 +1,13 @@ +package eu.siacs.conversations.xmpp; + +import eu.siacs.conversations.xml.Element; + +public class PresencePacket extends Element { + private PresencePacket(String name) { + super("presence"); + } + + public PresencePacket() { + super("presence"); + } +} diff --git a/src/eu/siacs/conversations/xmpp/XmppConnection.java b/src/eu/siacs/conversations/xmpp/XmppConnection.java new file mode 100644 index 00000000..4583c145 --- /dev/null +++ b/src/eu/siacs/conversations/xmpp/XmppConnection.java @@ -0,0 +1,446 @@ +package eu.siacs.conversations.xmpp; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.math.BigInteger; +import java.net.Socket; +import java.net.UnknownHostException; +import java.security.SecureRandom; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.List; + +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + +import org.xmlpull.v1.XmlPullParserException; + +import android.os.Bundle; +import android.os.PowerManager; +import android.util.Log; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.utils.DNSHelper; +import eu.siacs.conversations.utils.SASL; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xml.Tag; +import eu.siacs.conversations.xml.TagWriter; +import eu.siacs.conversations.xml.XmlReader; + +public class XmppConnection implements Runnable { + + protected Account account; + private static final String LOGTAG = "xmppService"; + + private PowerManager.WakeLock wakeLock; + + private SecureRandom random = new SecureRandom(); + + private Socket socket; + private XmlReader tagReader; + private TagWriter tagWriter; + + private boolean isTlsEncrypted = false; + private boolean isAuthenticated = false; + // private boolean shouldUseTLS = false; + private boolean shouldConnect = true; + private boolean shouldBind = true; + private boolean shouldAuthenticate = true; + private Element streamFeatures; + private HashSet discoFeatures = new HashSet(); + + private static final int PACKET_IQ = 0; + private static final int PACKET_MESSAGE = 1; + private static final int PACKET_PRESENCE = 2; + + private Hashtable iqPacketCallbacks = new Hashtable(); + private OnPresencePacketReceived presenceListener = null; + private OnIqPacketReceived unregisteredIqListener = null; + private OnMessagePacketReceived messageListener = null; + private OnStatusChanged statusListener = null; + + public XmppConnection(Account account, PowerManager pm) { + this.account = account; + wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, + "XmppConnection"); + tagReader = new XmlReader(wakeLock); + tagWriter = new TagWriter(); + } + + protected void connect() { + try { + Bundle namePort = DNSHelper.getSRVRecord(account.getServer()); + String srvRecordServer = namePort.getString("name"); + int srvRecordPort = namePort.getInt("port"); + if (srvRecordServer != null) { + Log.d(LOGTAG, account.getJid() + ": using values from dns " + + srvRecordServer + ":" + srvRecordPort); + socket = new Socket(srvRecordServer, srvRecordPort); + } else { + socket = new Socket(account.getServer(), 5222); + } + OutputStream out = socket.getOutputStream(); + tagWriter.setOutputStream(out); + InputStream in = socket.getInputStream(); + tagReader.setInputStream(in); + tagWriter.beginDocument(); + sendStartStream(); + Tag nextTag; + while ((nextTag = tagReader.readTag()) != null) { + if (nextTag.isStart("stream")) { + processStream(nextTag); + break; + } else { + Log.d(LOGTAG, "found unexpected tag: " + nextTag.getName()); + return; + } + } + if (socket.isConnected()) { + socket.close(); + } + } catch (UnknownHostException e) { + account.setStatus(Account.STATUS_SERVER_NOT_FOUND); + if (statusListener != null) { + statusListener.onStatusChanged(account); + } + return; + } catch (IOException e) { + Log.d(LOGTAG, "bla " + e.getMessage()); + if (shouldConnect) { + Log.d(LOGTAG, account.getJid() + ": connection lost"); + account.setStatus(Account.STATUS_OFFLINE); + if (statusListener != null) { + statusListener.onStatusChanged(account); + } + } + } catch (XmlPullParserException e) { + Log.d(LOGTAG, "xml exception " + e.getMessage()); + return; + } + + } + + @Override + public void run() { + shouldConnect = true; + while (shouldConnect) { + connect(); + try { + if (shouldConnect) { + Thread.sleep(30000); + } + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + Log.d(LOGTAG, "end run"); + } + + private void processStream(Tag currentTag) throws XmlPullParserException, + IOException { + Tag nextTag = tagReader.readTag(); + while ((nextTag != null) && (!nextTag.isEnd("stream"))) { + if (nextTag.isStart("error")) { + processStreamError(nextTag); + } else if (nextTag.isStart("features")) { + processStreamFeatures(nextTag); + } else if (nextTag.isStart("proceed")) { + switchOverToTls(nextTag); + } else if (nextTag.isStart("success")) { + isAuthenticated = true; + Log.d(LOGTAG, account.getJid() + + ": read success tag in stream. reset again"); + tagReader.readTag(); + tagReader.reset(); + sendStartStream(); + processStream(tagReader.readTag()); + break; + } else if (nextTag.isStart("failure")) { + Element failure = tagReader.readElement(nextTag); + Log.d(LOGTAG, "read failure element" + failure.toString()); + account.setStatus(Account.STATUS_UNAUTHORIZED); + if (statusListener != null) { + statusListener.onStatusChanged(account); + } + tagWriter.writeTag(Tag.end("stream")); + } else if (nextTag.isStart("iq")) { + processIq(nextTag); + } else if (nextTag.isStart("message")) { + processMessage(nextTag); + } else if (nextTag.isStart("presence")) { + processPresence(nextTag); + } else { + Log.d(LOGTAG, "found unexpected tag: " + nextTag.getName() + + " as child of " + currentTag.getName()); + } + nextTag = tagReader.readTag(); + } + if (account.getStatus() == Account.STATUS_ONLINE) { + account.setStatus(Account.STATUS_OFFLINE); + if (statusListener != null) { + statusListener.onStatusChanged(account); + } + } + } + + private Element processPacket(Tag currentTag, int packetType) + throws XmlPullParserException, IOException { + Element element; + switch (packetType) { + case PACKET_IQ: + element = new IqPacket(); + break; + case PACKET_MESSAGE: + element = new MessagePacket(); + break; + case PACKET_PRESENCE: + element = new PresencePacket(); + break; + default: + return null; + } + element.setAttributes(currentTag.getAttributes()); + Tag nextTag = tagReader.readTag(); + while (!nextTag.isEnd(element.getName())) { + if (!nextTag.isNo()) { + Element child = tagReader.readElement(nextTag); + element.addChild(child); + } + nextTag = tagReader.readTag(); + } + return element; + } + + private IqPacket processIq(Tag currentTag) throws XmlPullParserException, + IOException { + IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ); + if (iqPacketCallbacks.containsKey(packet.getId())) { + iqPacketCallbacks.get(packet.getId()).onIqPacketReceived(account, + packet); + iqPacketCallbacks.remove(packet.getId()); + } else if (this.unregisteredIqListener != null) { + this.unregisteredIqListener.onIqPacketReceived(account, packet); + } + return packet; + } + + private void processMessage(Tag currentTag) throws XmlPullParserException, + IOException { + MessagePacket packet = (MessagePacket) processPacket(currentTag, + PACKET_MESSAGE); + if (this.messageListener != null) { + this.messageListener.onMessagePacketReceived(account, packet); + } + } + + private void processPresence(Tag currentTag) throws XmlPullParserException, + IOException { + PresencePacket packet = (PresencePacket) processPacket(currentTag, + PACKET_PRESENCE); + if (this.presenceListener != null) { + this.presenceListener.onPresencePacketReceived(account, packet); + } + } + + private void sendStartTLS() { + Tag startTLS = Tag.empty("starttls"); + startTLS.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-tls"); + Log.d(LOGTAG, account.getJid() + ": sending starttls"); + tagWriter.writeTag(startTLS); + } + + private void switchOverToTls(Tag currentTag) throws XmlPullParserException, + IOException { + Tag nextTag = tagReader.readTag(); // should be proceed end tag + Log.d(LOGTAG, account.getJid() + ": now switch to ssl"); + SSLSocket sslSocket; + try { + sslSocket = (SSLSocket) ((SSLSocketFactory) SSLSocketFactory + .getDefault()).createSocket(socket, socket.getInetAddress() + .getHostAddress(), socket.getPort(), true); + tagReader.setInputStream(sslSocket.getInputStream()); + Log.d(LOGTAG, "reset inputstream"); + tagWriter.setOutputStream(sslSocket.getOutputStream()); + Log.d(LOGTAG, "switch over seemed to work"); + isTlsEncrypted = true; + sendStartStream(); + processStream(tagReader.readTag()); + sslSocket.close(); + } catch (IOException e) { + Log.d(LOGTAG, + account.getJid() + ": error on ssl '" + e.getMessage() + + "'"); + } + } + + private void sendSaslAuth() throws IOException, XmlPullParserException { + String saslString = SASL.plain(account.getUsername(), + account.getPassword()); + Element auth = new Element("auth"); + auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl"); + auth.setAttribute("mechanism", "PLAIN"); + auth.setContent(saslString); + Log.d(LOGTAG, account.getJid() + ": sending sasl " + auth.toString()); + tagWriter.writeElement(auth); + } + + private void processStreamFeatures(Tag currentTag) + throws XmlPullParserException, IOException { + this.streamFeatures = tagReader.readElement(currentTag); + Log.d(LOGTAG, account.getJid() + ": process stream features " + + streamFeatures); + if (this.streamFeatures.hasChild("starttls") + && account.isOptionSet(Account.OPTION_USETLS)) { + sendStartTLS(); + } else if (this.streamFeatures.hasChild("mechanisms") + && shouldAuthenticate) { + sendSaslAuth(); + } + if (this.streamFeatures.hasChild("bind") && shouldBind) { + sendBindRequest(); + if (this.streamFeatures.hasChild("session")) { + IqPacket startSession = new IqPacket(IqPacket.TYPE_SET); + Element session = new Element("session"); + session.setAttribute("xmlns", + "urn:ietf:params:xml:ns:xmpp-session"); + session.setContent(""); + startSession.addChild(session); + sendIqPacket(startSession, null); + tagWriter.writeElement(startSession); + } + Element presence = new Element("presence"); + + tagWriter.writeElement(presence); + } + } + + private void sendBindRequest() throws IOException { + IqPacket iq = new IqPacket(IqPacket.TYPE_SET); + Element bind = new Element("bind"); + bind.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-bind"); + iq.addChild(bind); + this.sendIqPacket(iq, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + String resource = packet.findChild("bind").findChild("jid") + .getContent().split("/")[1]; + account.setResource(resource); + account.setStatus(Account.STATUS_ONLINE); + if (statusListener != null) { + statusListener.onStatusChanged(account); + } + sendServiceDiscovery(); + } + }); + } + + private void sendServiceDiscovery() { + IqPacket iq = new IqPacket(IqPacket.TYPE_GET); + iq.setAttribute("to", account.getServer()); + Element query = new Element("query"); + query.setAttribute("xmlns", "http://jabber.org/protocol/disco#info"); + iq.addChild(query); + this.sendIqPacket(iq, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.hasChild("query")) { + List elements = packet.findChild("query") + .getChildren(); + for (int i = 0; i < elements.size(); ++i) { + if (elements.get(i).getName().equals("feature")) { + discoFeatures.add(elements.get(i).getAttribute( + "var")); + } + } + } + if (discoFeatures.contains("urn:xmpp:carbons:2")) { + sendEnableCarbons(); + } + } + }); + } + + private void sendEnableCarbons() { + Log.d(LOGTAG,account.getJid()+": enable carbons"); + IqPacket iq = new IqPacket(IqPacket.TYPE_SET); + Element enable = new Element("enable"); + enable.setAttribute("xmlns", "urn:xmpp:carbons:2"); + iq.addChild(enable); + this.sendIqPacket(iq, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (!packet.hasChild("error")) { + Log.d(LOGTAG,account.getJid()+": successfully enabled carbons"); + } else { + Log.d(LOGTAG,account.getJid()+": error enableing carbons "+packet.toString()); + } + } + }); + } + + private void processStreamError(Tag currentTag) { + Log.d(LOGTAG, "processStreamError"); + } + + private void sendStartStream() { + Tag stream = Tag.start("stream:stream"); + stream.setAttribute("from", account.getJid()); + stream.setAttribute("to", account.getServer()); + stream.setAttribute("version", "1.0"); + stream.setAttribute("xml:lang", "en"); + stream.setAttribute("xmlns", "jabber:client"); + stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams"); + tagWriter.writeTag(stream); + } + + private String nextRandomId() { + return new BigInteger(50, random).toString(32); + } + + public void sendIqPacket(IqPacket packet, OnIqPacketReceived callback) { + String id = nextRandomId(); + packet.setAttribute("id", id); + tagWriter.writeElement(packet); + if (callback != null) { + iqPacketCallbacks.put(id, callback); + } + //Log.d(LOGTAG, account.getJid() + ": sending: " + packet.toString()); + } + + public void sendMessagePacket(MessagePacket packet) { + Log.d(LOGTAG,"sending message packet "+packet.toString()); + tagWriter.writeElement(packet); + } + + public void sendPresencePacket(PresencePacket packet) { + tagWriter.writeElement(packet); + Log.d(LOGTAG, account.getJid() + ": sending: " + packet.toString()); + } + + public void setOnMessagePacketReceivedListener( + OnMessagePacketReceived listener) { + this.messageListener = listener; + } + + public void setOnUnregisteredIqPacketReceivedListener( + OnIqPacketReceived listener) { + this.unregisteredIqListener = listener; + } + + public void setOnPresencePacketReceivedListener( + OnPresencePacketReceived listener) { + this.presenceListener = listener; + } + + public void setOnStatusChangedListener(OnStatusChanged listener) { + this.statusListener = listener; + } + + public void disconnect() { + shouldConnect = false; + tagWriter.writeTag(Tag.end("stream:stream")); + } +} -- cgit v1.2.3