diff options
Diffstat (limited to 'src')
35 files changed, 2202 insertions, 384 deletions
diff --git a/src/eu/siacs/conversations/entities/Contact.java b/src/eu/siacs/conversations/entities/Contact.java index 4b97cc41..132b2717 100644 --- a/src/eu/siacs/conversations/entities/Contact.java +++ b/src/eu/siacs/conversations/entities/Contact.java @@ -13,6 +13,7 @@ import eu.siacs.conversations.xml.Element; import android.content.ContentValues; import android.database.Cursor; +import android.util.Log; public class Contact extends AbstractEntity implements Serializable { private static final long serialVersionUID = -4570817093119419962L; @@ -163,11 +164,16 @@ public class Contact extends AbstractEntity implements Serializable { public void updatePresence(String resource, int status) { this.presences.updatePresence(resource, status); + Log.d("xmppService","updatingPresence for contact="+this.jid+" resource="+resource+" num="+presences.size()); } public void removePresence(String resource) { this.presences.removePresence(resource); } + + public void clearPresences() { + this.presences.clearPresences(); + } public int getMostAvailableStatus() { return this.presences.getMostAvailableStatus(); diff --git a/src/eu/siacs/conversations/entities/Conversation.java b/src/eu/siacs/conversations/entities/Conversation.java index f8c6a2a6..6eb688ae 100644 --- a/src/eu/siacs/conversations/entities/Conversation.java +++ b/src/eu/siacs/conversations/entities/Conversation.java @@ -45,6 +45,8 @@ public class Conversation extends AbstractEntity { private int status; private long created; private int mode; + + private String nextPresence; private transient List<Message> messages = null; private transient Account account = null; @@ -308,4 +310,12 @@ public class Conversation extends AbstractEntity { public void setContactJid(String jid) { this.contactJid = jid; } + + public void setNextPresence(String presence) { + this.nextPresence = presence; + } + + public String getNextPresence() { + return this.nextPresence; + } } diff --git a/src/eu/siacs/conversations/entities/Message.java b/src/eu/siacs/conversations/entities/Message.java index b91d822d..e2b03967 100644 --- a/src/eu/siacs/conversations/entities/Message.java +++ b/src/eu/siacs/conversations/entities/Message.java @@ -9,15 +9,21 @@ public class Message extends AbstractEntity { public static final String TABLENAME = "messages"; + public static final int STATUS_RECIEVING = -1; 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 STATUS_SEND_FAILED = 3; + public static final int STATUS_SEND_REJECTED = 4; + public static final int STATUS_PREPARING = 5; public static final int ENCRYPTION_NONE = 0; public static final int ENCRYPTION_PGP = 1; public static final int ENCRYPTION_OTR = 2; public static final int ENCRYPTION_DECRYPTED = 3; + + public static final int TYPE_TEXT = 0; + public static final int TYPE_IMAGE = 1; public static String CONVERSATION = "conversationUuid"; public static String COUNTERPART = "counterpart"; @@ -25,6 +31,7 @@ public class Message extends AbstractEntity { public static String TIME_SENT = "timeSent"; public static String ENCRYPTION = "encryption"; public static String STATUS = "status"; + public static String TYPE = "type"; protected String conversationUuid; protected String counterpart; @@ -33,6 +40,7 @@ public class Message extends AbstractEntity { protected long timeSent; protected int encryption; protected int status; + protected int type; protected boolean read = true; protected transient Conversation conversation = null; @@ -40,17 +48,17 @@ public class Message extends AbstractEntity { 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); + Message.STATUS_UNSEND,TYPE_TEXT); 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(java.util.UUID.randomUUID().toString(), conversation.getUuid(),counterpart, body, System.currentTimeMillis(), encryption,status,TYPE_TEXT); this.conversation = conversation; } public Message(String uuid, String conversationUUid, String counterpart, - String body, long timeSent, int encryption, int status) { + String body, long timeSent, int encryption, int status, int type) { this.uuid = uuid; this.conversationUuid = conversationUUid; this.counterpart = counterpart; @@ -58,6 +66,7 @@ public class Message extends AbstractEntity { this.timeSent = timeSent; this.encryption = encryption; this.status = status; + this.type = type; } @Override @@ -70,6 +79,7 @@ public class Message extends AbstractEntity { values.put(TIME_SENT, timeSent); values.put(ENCRYPTION, encryption); values.put(STATUS, status); + values.put(TYPE, type); return values; } @@ -108,7 +118,8 @@ public class Message extends AbstractEntity { cursor.getString(cursor.getColumnIndex(BODY)), cursor.getLong(cursor.getColumnIndex(TIME_SENT)), cursor.getInt(cursor.getColumnIndex(ENCRYPTION)), - cursor.getInt(cursor.getColumnIndex(STATUS))); + cursor.getInt(cursor.getColumnIndex(STATUS)), + cursor.getInt(cursor.getColumnIndex(TYPE))); } public void setConversation(Conversation conv) { @@ -150,4 +161,16 @@ public class Message extends AbstractEntity { public void setEncryptedBody(String body) { this.encryptedBody = body; } + + public void setType(int type) { + this.type = type; + } + + public int getType() { + return this.type; + } + + public void setPresence(String presence) { + this.counterpart = this.counterpart.split("/")[0] + "/" + presence; + } } diff --git a/src/eu/siacs/conversations/entities/Presences.java b/src/eu/siacs/conversations/entities/Presences.java index 9a22e559..acbaafca 100644 --- a/src/eu/siacs/conversations/entities/Presences.java +++ b/src/eu/siacs/conversations/entities/Presences.java @@ -33,6 +33,10 @@ public class Presences { this.presences.remove(resource); } + public void clearPresences() { + this.presences.clear(); + } + public int getMostAvailableStatus() { int status = OFFLINE; Iterator<Entry<String, Integer>> it = presences.entrySet().iterator(); @@ -54,7 +58,7 @@ public class Presences { jObj.put("resource", entry.getKey()); jObj.put("status", entry.getValue()); } catch (JSONException e) { - + } json.put(jObj); } @@ -71,7 +75,7 @@ public class Presences { 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 index 4c0d6216..852a9315 100644 --- a/src/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -23,7 +23,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { private static DatabaseBackend instance = null; private static final String DATABASE_NAME = "history"; - private static final int DATABASE_VERSION = 2; + private static final int DATABASE_VERSION = 3; public DatabaseBackend(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); @@ -50,7 +50,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { + " TEXT PRIMARY KEY, " + Message.CONVERSATION + " TEXT, " + Message.TIME_SENT + " NUMBER, " + Message.COUNTERPART + " TEXT, " + Message.BODY + " TEXT, " + Message.ENCRYPTION - + " NUMBER, " + Message.STATUS + " NUMBER," + "FOREIGN KEY(" + + " NUMBER, " + Message.STATUS + " NUMBER," +Message.TYPE +" NUMBER, FOREIGN KEY(" + Message.CONVERSATION + ") REFERENCES " + Conversation.TABLENAME + "(" + Conversation.UUID + ") ON DELETE CASCADE);"); @@ -72,6 +72,10 @@ public class DatabaseBackend extends SQLiteOpenHelper { db.execSQL("update " + Account.TABLENAME + " set " + Account.OPTIONS + " = " + Account.OPTIONS + " | 8"); } + if (oldVersion < 3 && newVersion >= 3) { + //add field type to message + db.execSQL("ALTER TABLE "+Message.TABLENAME+" ADD COLUMN "+Message.TYPE+" NUMBER");; + } } public static synchronized DatabaseBackend getInstance(Context context) { diff --git a/src/eu/siacs/conversations/persistance/FileBackend.java b/src/eu/siacs/conversations/persistance/FileBackend.java new file mode 100644 index 00000000..f7f986f2 --- /dev/null +++ b/src/eu/siacs/conversations/persistance/FileBackend.java @@ -0,0 +1,113 @@ +package eu.siacs.conversations.persistance; + +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.util.LruCache; + +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.xmpp.jingle.JingleFile; + +public class FileBackend { + + private static int IMAGE_SIZE = 1920; + + private Context context; + private LruCache<String, Bitmap> thumbnailCache; + + public FileBackend(Context context) { + this.context = context; + + int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); + int cacheSize = maxMemory / 8; + thumbnailCache = new LruCache<String, Bitmap>(cacheSize) { + @Override + protected int sizeOf(String key, Bitmap bitmap) { + return bitmap.getByteCount() / 1024; + } + }; + + } + + public JingleFile getJingleFile(Message message) { + Conversation conversation = message.getConversation(); + String prefix = context.getFilesDir().getAbsolutePath(); + String path = prefix + "/" + conversation.getAccount().getJid() + "/" + + conversation.getContactJid(); + String filename = message.getUuid() + ".webp"; + return new JingleFile(path + "/" + filename); + } + + private Bitmap resize(Bitmap originalBitmap, int size) { + int w = originalBitmap.getWidth(); + int h = originalBitmap.getHeight(); + if (Math.max(w, h) > size) { + int scalledW; + int scalledH; + if (w <= h) { + scalledW = (int) (w / ((double) h / size)); + scalledH = size; + } else { + scalledW = size; + scalledH = (int) (h / ((double) w / size)); + } + Bitmap scalledBitmap = Bitmap.createScaledBitmap( + originalBitmap, scalledW, scalledH, true); + return scalledBitmap; + } else { + return originalBitmap; + } + } + + public JingleFile copyImageToPrivateStorage(Message message, Uri image) { + try { + InputStream is = context.getContentResolver() + .openInputStream(image); + JingleFile file = getJingleFile(message); + file.getParentFile().mkdirs(); + file.createNewFile(); + OutputStream os = new FileOutputStream(file); + Bitmap originalBitmap = BitmapFactory.decodeStream(is); + is.close(); + Bitmap scalledBitmap = resize(originalBitmap, IMAGE_SIZE); + boolean success = scalledBitmap.compress(Bitmap.CompressFormat.WEBP,75,os); + if (!success) { + //Log.d("xmppService", "couldnt compress"); + } + os.close(); + return file; + } catch (FileNotFoundException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + return null; + } + + public Bitmap getImageFromMessage(Message message) { + return BitmapFactory + .decodeFile(getJingleFile(message).getAbsolutePath()); + } + + public Bitmap getThumbnailFromMessage(Message message, int size) { + Bitmap thumbnail = thumbnailCache.get(message.getUuid()); + if (thumbnail==null) { + Bitmap fullsize = BitmapFactory.decodeFile(getJingleFile(message) + .getAbsolutePath()); + thumbnail = resize(fullsize, size); + this.thumbnailCache.put(message.getUuid(), thumbnail); + } + return thumbnail; + } +} diff --git a/src/eu/siacs/conversations/services/XmppConnectionService.java b/src/eu/siacs/conversations/services/XmppConnectionService.java index 9dbce675..0dea3731 100644 --- a/src/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/eu/siacs/conversations/services/XmppConnectionService.java @@ -27,6 +27,7 @@ import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.MucOptions.OnRenameListener; import eu.siacs.conversations.entities.Presences; import eu.siacs.conversations.persistance.DatabaseBackend; +import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.persistance.OnPhoneContactsMerged; import eu.siacs.conversations.ui.OnAccountListChangedListener; import eu.siacs.conversations.ui.OnConversationListChangedListener; @@ -39,16 +40,17 @@ import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.OnBindListener; import eu.siacs.conversations.xmpp.OnIqPacketReceived; -import eu.siacs.conversations.xmpp.OnJinglePacketReceived; import eu.siacs.conversations.xmpp.OnMessagePacketReceived; import eu.siacs.conversations.xmpp.OnPresencePacketReceived; import eu.siacs.conversations.xmpp.OnStatusChanged; import eu.siacs.conversations.xmpp.OnTLSExceptionReceived; import eu.siacs.conversations.xmpp.XmppConnection; +import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; +import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived; +import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; import eu.siacs.conversations.xmpp.stanzas.IqPacket; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; import eu.siacs.conversations.xmpp.stanzas.PresencePacket; -import eu.siacs.conversations.xmpp.stanzas.jingle.JinglePacket; import android.app.AlarmManager; import android.app.PendingIntent; import android.app.Service; @@ -59,6 +61,7 @@ import android.database.ContentObserver; import android.database.DatabaseUtils; import android.net.ConnectivityManager; import android.net.NetworkInfo; +import android.net.Uri; import android.os.Binder; import android.os.Bundle; import android.os.IBinder; @@ -73,6 +76,7 @@ public class XmppConnectionService extends Service { protected static final String LOGTAG = "xmppService"; public DatabaseBackend databaseBackend; + private FileBackend fileBackend; public long startDate; @@ -80,10 +84,12 @@ public class XmppConnectionService extends Service { private static final int PING_MIN_INTERVAL = 10; private static final int PING_TIMEOUT = 5; private static final int CONNECT_TIMEOUT = 60; + private static final long CARBON_GRACE_PERIOD = 60000L; private List<Account> accounts; private List<Conversation> conversations = null; - + private JingleConnectionManager mJingleConnectionManager = new JingleConnectionManager(this); + public OnConversationListChangedListener convChangedListener = null; private int convChangedListenerCount = 0; private OnAccountListChangedListener accountChangedListener = null; @@ -96,6 +102,8 @@ public class XmppConnectionService extends Service { private Random mRandom = new Random(System.currentTimeMillis()); + private long lastCarbonMessageReceived = -CARBON_GRACE_PERIOD; + private ContentObserver contactObserver = new ContentObserver(null) { @Override public void onChange(boolean selfChange) { @@ -115,14 +123,17 @@ public class XmppConnectionService extends Service { MessagePacket packet) { Message message = null; boolean notify = true; + if(getPreferences().getBoolean("notification_grace_period_after_carbon_received", true)){ + notify=(SystemClock.elapsedRealtime() - lastCarbonMessageReceived) > CARBON_GRACE_PERIOD; + } + if ((packet.getType() == MessagePacket.TYPE_CHAT)) { String pgpBody = MessageParser.getPgpBody(packet); if (pgpBody != null) { message = MessageParser.parsePgpChat(pgpBody, packet, account, service); message.markUnread(); - } else if (packet.hasChild("body") - && (packet.getBody().startsWith("?OTR"))) { + } else if ((packet.getBody()!=null) && (packet.getBody().startsWith("?OTR"))) { message = MessageParser.parseOtrChat(packet, account, service); if (message != null) { @@ -138,6 +149,7 @@ public class XmppConnectionService extends Service { service); if (message != null) { if (message.getStatus() == Message.STATUS_SEND) { + lastCarbonMessageReceived = SystemClock.elapsedRealtime(); notify = false; message.getConversation().markRead(); } else { @@ -158,7 +170,8 @@ public class XmppConnectionService extends Service { } } } else if (packet.getType() == MessagePacket.TYPE_ERROR) { - message = MessageParser.parseError(packet, account, service); + MessageParser.parseError(packet, account, service); + return; } else if (packet.getType() == MessagePacket.TYPE_NORMAL) { if (packet.hasChild("x")) { Element x = packet.findChild("x"); @@ -274,7 +287,11 @@ public class XmppConnectionService extends Service { } else { Contact contact = findContact(account, fromParts[0]); if (contact == null) { - // most likely roster not synced + if ("subscribe".equals(type)) { + account.getXmppConnection().addPendingSubscription(fromParts[0]); + } else { + Log.d(LOGTAG,packet.getFrom()+ " could not be found"); + } return; } if (type == null) { @@ -299,20 +316,24 @@ public class XmppConnectionService extends Service { } } } + replaceContactInConversation(contact.getJid(), contact); databaseBackend.updateContact(contact); } else { - // Log.d(LOGTAG,"presence without resource "+packet.toString()); + //Log.d(LOGTAG,"presence without resource "+packet.toString()); } } else if (type.equals("unavailable")) { if (fromParts.length != 2) { - // Log.d(LOGTAG,"received presence with no resource "+packet.toString()); + contact.clearPresences(); } else { contact.removePresence(fromParts[1]); - databaseBackend.updateContact(contact); } + replaceContactInConversation(contact.getJid(), contact); + databaseBackend.updateContact(contact); } else if (type.equals("subscribe")) { + Log.d(LOGTAG,"received subscribe packet from "+packet.getFrom()); if (contact .getSubscriptionOption(Contact.Subscription.PREEMPTIVE_GRANT)) { + Log.d(LOGTAG,"preemptive grant; granting"); sendPresenceUpdatesTo(contact); contact.setSubscriptionOption(Contact.Subscription.FROM); contact.resetSubscriptionOption(Contact.Subscription.PREEMPTIVE_GRANT); @@ -326,12 +347,11 @@ public class XmppConnectionService extends Service { requestPresenceUpdatesFrom(contact); } } else { - // TODO: ask user to handle it maybe + account.getXmppConnection().addPendingSubscription(fromParts[0]); } } else { //Log.d(LOGTAG, packet.toString()); } - replaceContactInConversation(contact.getJid(), contact); } } } @@ -348,6 +368,8 @@ public class XmppConnectionService extends Service { processRosterItems(account, query); mergePhoneContactsWithRoster(null); } + } else { + Log.d(LOGTAG,"iq packet arrived "+packet.toString()); } } }; @@ -356,7 +378,7 @@ public class XmppConnectionService extends Service { @Override public void onJinglePacketReceived(Account account, JinglePacket packet) { - Log.d(LOGTAG,account.getJid()+": jingle packet received"+packet.toString()); + mJingleConnectionManager.deliverPacket(account, packet); } }; @@ -381,6 +403,35 @@ public class XmppConnectionService extends Service { } + public FileBackend getFileBackend() { + return this.fileBackend; + } + + public void attachImageToConversation(final Conversation conversation, final String presence, final Uri uri) { + new Thread(new Runnable() { + + @Override + public void run() { + Message message = new Message(conversation, "", Message.ENCRYPTION_NONE); + message.setPresence(presence); + message.setType(Message.TYPE_IMAGE); + message.setStatus(Message.STATUS_PREPARING); + conversation.getMessages().add(message); + if (convChangedListener!=null) { + convChangedListener.onConversationListChanged(); + } + getFileBackend().copyImageToPrivateStorage(message, uri); + message.setStatus(Message.STATUS_UNSEND); + databaseBackend.createMessage(message); + if (convChangedListener!=null) { + convChangedListener.onConversationListChanged(); + } + sendMessage(message, null); + } + }).start(); + } + + protected Conversation findMuc(String name, Account account) { for (Conversation conversation : this.conversations) { if (conversation.getContactJid().split("/")[0].equals(name) @@ -522,7 +573,8 @@ public class XmppConnectionService extends Service { @Override public void onCreate() { ExceptionHelper.init(getApplicationContext()); - databaseBackend = DatabaseBackend.getInstance(getApplicationContext()); + this.databaseBackend = DatabaseBackend.getInstance(getApplicationContext()); + this.fileBackend = new FileBackend(getApplicationContext()); this.accounts = databaseBackend.getAccounts(); this.getConversations(); @@ -589,8 +641,7 @@ public class XmppConnectionService extends Service { } public XmppConnection createConnection(Account account) { - SharedPreferences sharedPref = PreferenceManager - .getDefaultSharedPreferences(getApplicationContext()); + SharedPreferences sharedPref = getPreferences(); account.setResource(sharedPref.getString("resource", "mobile").toLowerCase(Locale.getDefault())); XmppConnection connection = new XmppConnection(account, this.pm); connection.setOnMessagePacketReceivedListener(this.messageListener); @@ -633,51 +684,55 @@ public class XmppConnectionService extends Service { synchronized public void sendMessage(Message message, String presence) { Account account = message.getConversation().getAccount(); Conversation conv = message.getConversation(); + MessagePacket packet = null; boolean saveInDb = false; boolean addToConversation = false; + boolean send = 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,true); - } 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) { - message.getConversation().endOtrIfNeeded(); - 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"); - packet.addChild("x", "jabber:x:encrypted").setContent(message.getEncryptedBody()); - account.getXmppConnection().sendMessagePacket(packet); - message.setStatus(Message.STATUS_SEND); - message.setEncryption(Message.ENCRYPTION_DECRYPTED); - saveInDb = true; - addToConversation = true; + if (message.getType() == Message.TYPE_IMAGE) { + mJingleConnectionManager.createNewConnection(message); } else { - message.getConversation().endOtrIfNeeded(); - // don't encrypt - if (message.getConversation().getMode() == Conversation.MODE_SINGLE) { + if (message.getEncryption() == Message.ENCRYPTION_OTR) { + if (!conv.hasValidOtrSession()) { + // starting otr session. messages will be send later + conv.startOtrSession(getApplicationContext(), presence,true); + } else if (conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) { + // otr session aleary exists, creating message packet + // accordingly + packet = prepareMessagePacket(account, message, + conv.getOtrSession()); + send = true; + message.setStatus(Message.STATUS_SEND); + } + saveInDb = true; + addToConversation = true; + } else if (message.getEncryption() == Message.ENCRYPTION_PGP) { + message.getConversation().endOtrIfNeeded(); + 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"); + packet.addChild("x", "jabber:x:encrypted").setContent(message.getEncryptedBody()); message.setStatus(Message.STATUS_SEND); + message.setEncryption(Message.ENCRYPTION_DECRYPTED); saveInDb = true; addToConversation = true; + send = true; + } else { + message.getConversation().endOtrIfNeeded(); + // don't encrypt + if (message.getConversation().getMode() == Conversation.MODE_SINGLE) { + message.setStatus(Message.STATUS_SEND); + saveInDb = true; + addToConversation = true; + } + packet = prepareMessagePacket(account, message, null); + send = true; } - - packet = prepareMessagePacket(account, message, null); - account.getXmppConnection().sendMessagePacket(packet); } } else { // account is offline @@ -694,6 +749,9 @@ public class XmppConnectionService extends Service { convChangedListener.onConversationListChanged(); } } + if ((send)&&(packet!=null)) { + account.getXmppConnection().sendMessagePacket(packet); + } } @@ -748,6 +806,7 @@ public class XmppConnectionService extends Service { packet.setTo(message.getCounterpart().split("/")[0]); packet.setFrom(account.getJid()); } + packet.setId(message.getUuid()); return packet; } @@ -974,6 +1033,10 @@ public class XmppConnectionService extends Service { this.convChangedListener.onConversationListChanged(); } } + + public void clearConversationHistory(Conversation conversation) { + + } public int getConversationCount() { return this.databaseBackend.getConversationCount(); @@ -1018,12 +1081,10 @@ public class XmppConnectionService extends Service { OnConversationListChangedListener listener) { this.convChangedListener = listener; this.convChangedListenerCount++; - Log.d(LOGTAG,"registered on conv changed in backend ("+convChangedListenerCount+")"); } public void removeOnConversationListChangedListener() { this.convChangedListenerCount--; - Log.d(LOGTAG,"someone on conv changed listener removed listener ("+convChangedListenerCount+")"); if (this.convChangedListenerCount==0) { this.convChangedListener = null; } @@ -1160,8 +1221,7 @@ public class XmppConnectionService extends Service { } public void createContact(Contact contact) { - SharedPreferences sharedPref = PreferenceManager - .getDefaultSharedPreferences(getApplicationContext()); + SharedPreferences sharedPref = getPreferences(); boolean autoGrant = sharedPref.getBoolean("grant_new_contacts", true); if (autoGrant) { contact.setSubscriptionOption(Contact.Subscription.PREEMPTIVE_GRANT); @@ -1180,6 +1240,10 @@ public class XmppConnectionService extends Service { account.getXmppConnection().sendIqPacket(iq, null); if (autoGrant) { requestPresenceUpdatesFrom(contact); + if (account.getXmppConnection().hasPendingSubscription(contact.getJid())) { + Log.d("xmppService","contact had pending subscription"); + sendPresenceUpdatesTo(contact); + } } replaceContactInConversation(contact.getJid(), contact); } @@ -1253,9 +1317,11 @@ public class XmppConnectionService extends Service { public Contact findContact(String uuid) { Contact contact = this.databaseBackend.getContact(uuid); - for (Account account : getAccounts()) { - if (contact.getAccountUuid().equals(account.getUuid())) { - contact.setAccount(account); + if (contact!=null) { + for (Account account : getAccounts()) { + if (contact.getAccountUuid().equals(account.getUuid())) { + contact.setAccount(account); + } } } return contact; @@ -1325,4 +1391,33 @@ public class XmppConnectionService extends Service { } } -}
\ No newline at end of file + + public boolean markMessage(Account account, String recipient, String uuid, int status) { + boolean marked = false; + for(Conversation conversation : getConversations()) { + if (conversation.getContactJid().equals(recipient)&&conversation.getAccount().equals(account)) { + for(Message message : conversation.getMessages()) { + if (message.getUuid().equals(uuid)) { + markMessage(message, status); + marked = true; + break; + } + } + break; + } + } + return marked; + } + + public void markMessage(Message message, int status) { + message.setStatus(status); + databaseBackend.updateMessage(message); + if (convChangedListener!=null) { + convChangedListener.onConversationListChanged(); + } + } + + public SharedPreferences getPreferences() { + return PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); + } +} diff --git a/src/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/eu/siacs/conversations/ui/ContactDetailsActivity.java index 5dc6eb3b..1ed3fa13 100644 --- a/src/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -227,10 +227,14 @@ public class ContactDetailsActivity extends XmppActivity { status.setTextColor(0xFFe92727); break; } - contactJid.setText(contact.getJid()); + if (contact.getPresences().size() > 1) { + contactJid.setText(contact.getJid()+" ("+contact.getPresences().size()+")"); + } else { + contactJid.setText(contact.getJid()); + } accountJid.setText(contact.getAccount().getJid()); - UIHelper.prepareContactBadge(this, badge, contact); + UIHelper.prepareContactBadge(this, badge, contact, getApplicationContext()); if (contact.getSystemAccount() == null) { badge.setOnClickListener(onBadgeClick); diff --git a/src/eu/siacs/conversations/ui/ContactsActivity.java b/src/eu/siacs/conversations/ui/ContactsActivity.java index 2e9e55f5..e403450a 100644 --- a/src/eu/siacs/conversations/ui/ContactsActivity.java +++ b/src/eu/siacs/conversations/ui/ContactsActivity.java @@ -381,8 +381,7 @@ public class ContactsActivity extends XmppActivity { contactJid.setText(contact.getJid()); ImageView imageView = (ImageView) view .findViewById(R.id.contact_photo); - imageView.setImageBitmap(UIHelper.getContactPicture(contact, - null, 90, this.getContext())); + imageView.setImageBitmap(UIHelper.getContactPicture(contact, 48, this.getContext(), false)); return view; } }; diff --git a/src/eu/siacs/conversations/ui/ConversationActivity.java b/src/eu/siacs/conversations/ui/ConversationActivity.java index f47e8a8c..6e56d2b3 100644 --- a/src/eu/siacs/conversations/ui/ConversationActivity.java +++ b/src/eu/siacs/conversations/ui/ConversationActivity.java @@ -1,6 +1,7 @@ package eu.siacs.conversations.ui; import java.util.ArrayList; +import java.util.Hashtable; import java.util.List; import eu.siacs.conversations.R; @@ -13,16 +14,17 @@ import eu.siacs.conversations.utils.UIHelper; import android.os.Bundle; import android.preference.PreferenceManager; import android.app.AlertDialog; +import android.app.AlertDialog.Builder; import android.app.FragmentTransaction; import android.content.Context; import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; import android.content.Intent; import android.content.SharedPreferences; import android.graphics.Color; 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; @@ -43,39 +45,43 @@ public class ConversationActivity extends XmppActivity { public static final String VIEW_CONVERSATION = "viewConversation"; public static final String CONVERSATION = "conversationUuid"; public static final String TEXT = "text"; - + public static final String PRESENCE = "eu.siacs.conversations.presence"; + public static final int REQUEST_SEND_MESSAGE = 0x75441; public static final int REQUEST_DECRYPT_PGP = 0x76783; + private static final int ATTACH_FILE = 0x48502; protected SlidingPaneLayout spl; private List<Conversation> conversationList = new ArrayList<Conversation>(); private Conversation selectedConversation = null; private ListView listView; - + private boolean paneShouldBeOpen = true; private boolean useSubject = true; private ArrayAdapter<Conversation> listAdapter; - + private OnConversationListChangedListener onConvChanged = new OnConversationListChangedListener() { - + @Override public void onConversationListChanged() { runOnUiThread(new Runnable() { - + @Override - public void run() { + public void run() { updateConversationList(); - if(paneShouldBeOpen) { + if (paneShouldBeOpen) { if (conversationList.size() >= 1) { swapConversationFragment(); } else { - startActivity(new Intent(getApplicationContext(), ContactsActivity.class)); + startActivity(new Intent(getApplicationContext(), + ContactsActivity.class)); finish(); } } - ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager().findFragmentByTag("conversation"); - if (selectedFragment!=null) { + ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager() + .findFragmentByTag("conversation"); + if (selectedFragment != null) { selectedFragment.updateMessages(); } } @@ -83,19 +89,8 @@ public class ConversationActivity extends XmppActivity { } }; - 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); - } - }; protected ConversationActivity activity = this; - + public List<Conversation> getConversationList() { return this.conversationList; } @@ -103,19 +98,19 @@ public class ConversationActivity extends XmppActivity { public Conversation getSelectedConversation() { return this.selectedConversation; } - + public ListView getConversationListView() { return this.listView; } - + public SlidingPaneLayout getSlidingPaneLayout() { return this.spl; } - + public boolean shouldPaneBeOpen() { return paneShouldBeOpen; } - + @Override protected void onCreate(Bundle savedInstanceState) { @@ -141,7 +136,7 @@ public class ConversationActivity extends XmppActivity { return view; } if (!spl.isSlideable()) { - if (conv==getSelectedConversation()) { + if (conv == getSelectedConversation()) { view.setBackgroundColor(0xffdddddd); } else { view.setBackgroundColor(Color.TRANSPARENT); @@ -149,29 +144,34 @@ public class ConversationActivity extends XmppActivity { } else { view.setBackgroundColor(Color.TRANSPARENT); } - TextView convName = (TextView) view.findViewById(R.id.conversation_name); + TextView convName = (TextView) view + .findViewById(R.id.conversation_name); convName.setText(conv.getName(useSubject)); - TextView convLastMsg = (TextView) view.findViewById(R.id.conversation_lastmsg); + 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); + + if (!conv.isRead()) { + convName.setTypeface(null, Typeface.BOLD); + convLastMsg.setTypeface(null, Typeface.BOLD); } else { - convName.setTypeface(null,Typeface.NORMAL); - convLastMsg.setTypeface(null,Typeface.NORMAL); + convName.setTypeface(null, Typeface.NORMAL); + convLastMsg.setTypeface(null, Typeface.NORMAL); } - + ((TextView) view.findViewById(R.id.conversation_lastupdate)) - .setText(UIHelper.readableTimeDifference(conv.getLatestMessage().getTimeSent())); - - ImageView imageView = (ImageView) view.findViewById(R.id.conversation_image); - imageView.setImageBitmap(UIHelper.getContactPicture(conv.getContact(), conv.getName(useSubject),200, activity.getApplicationContext())); + .setText(UIHelper.readableTimeDifference(conv + .getLatestMessage().getTimeSent())); + + ImageView imageView = (ImageView) view + .findViewById(R.id.conversation_image); + imageView.setImageBitmap(UIHelper.getContactPicture( + conv, 56, activity.getApplicationContext(), false)); return view; } }; - + listView.setAdapter(this.listAdapter); listView.setOnItemClickListener(new OnItemClickListener() { @@ -182,7 +182,7 @@ public class ConversationActivity extends XmppActivity { paneShouldBeOpen = false; if (selectedConversation != conversationList.get(position)) { selectedConversation = conversationList.get(position); - swapConversationFragment(); //.onBackendConnected(conversationList.get(position)); + swapConversationFragment(); // .onBackendConnected(conversationList.get(position)); } else { spl.closePane(); } @@ -206,13 +206,16 @@ public class ConversationActivity extends XmppActivity { @Override public void onPanelClosed(View arg0) { paneShouldBeOpen = false; - if ((conversationList.size() > 0)&&(getSelectedConversation()!=null)) { + if ((conversationList.size() > 0) + && (getSelectedConversation() != null)) { getActionBar().setDisplayHomeAsUpEnabled(true); - getActionBar().setTitle(getSelectedConversation().getName(useSubject)); + getActionBar().setTitle( + getSelectedConversation().getName(useSubject)); invalidateOptionsMenu(); if (!getSelectedConversation().isRead()) { getSelectedConversation().markRead(); - UIHelper.updateNotification(getApplicationContext(), getConversationList(), null, false); + UIHelper.updateNotification(getApplicationContext(), + getConversationList(), null, false); listView.invalidateViews(); } } @@ -231,29 +234,36 @@ public class ConversationActivity extends XmppActivity { getMenuInflater().inflate(R.menu.conversations, menu); MenuItem menuSecure = (MenuItem) menu.findItem(R.id.action_security); MenuItem menuArchive = (MenuItem) menu.findItem(R.id.action_archive); - MenuItem menuMucDetails = (MenuItem) menu.findItem(R.id.action_muc_details); - MenuItem menuContactDetails = (MenuItem) menu.findItem(R.id.action_contact_details); - MenuItem menuInviteContacts = (MenuItem) menu.findItem(R.id.action_invite); - - if ((spl.isOpen()&&(spl.isSlideable()))) { + MenuItem menuMucDetails = (MenuItem) menu + .findItem(R.id.action_muc_details); + MenuItem menuContactDetails = (MenuItem) menu + .findItem(R.id.action_contact_details); + MenuItem menuInviteContacts = (MenuItem) menu + .findItem(R.id.action_invite); + MenuItem menuAttach = (MenuItem) menu.findItem(R.id.action_attach_file); + MenuItem menuClearHistory = (MenuItem) menu.findItem(R.id.action_clear_history); + + if ((spl.isOpen() && (spl.isSlideable()))) { menuArchive.setVisible(false); menuMucDetails.setVisible(false); menuContactDetails.setVisible(false); menuSecure.setVisible(false); menuInviteContacts.setVisible(false); + menuAttach.setVisible(false); + menuClearHistory.setVisible(false); } else { - ((MenuItem) menu.findItem(R.id.action_add)).setVisible(!spl.isSlideable()); - if (this.getSelectedConversation()!=null) { + ((MenuItem) menu.findItem(R.id.action_add)).setVisible(!spl + .isSlideable()); + if (this.getSelectedConversation() != null) { if (this.getSelectedConversation().getMode() == Conversation.MODE_MULTI) { - menuMucDetails.setVisible(true); menuContactDetails.setVisible(false); menuSecure.setVisible(false); - menuInviteContacts.setVisible(true); + menuAttach.setVisible(false); } else { - menuContactDetails.setVisible(true); menuMucDetails.setVisible(false); menuInviteContacts.setVisible(false); - if (this.getSelectedConversation().getLatestMessage().getEncryption() != Message.ENCRYPTION_NONE) { + if (this.getSelectedConversation().getLatestMessage() + .getEncryption() != Message.ENCRYPTION_NONE) { menuSecure.setIcon(R.drawable.ic_action_secure); } } @@ -268,6 +278,21 @@ public class ConversationActivity extends XmppActivity { case android.R.id.home: spl.openPane(); break; + case R.id.action_attach_file: + selectPresence(getSelectedConversation(), new OnPresenceSelected() { + + @Override + public void onPresenceSelected(boolean success, String presence) { + if (success) { + Intent attachFileIntent = new Intent(); + attachFileIntent.setType("image/*"); + attachFileIntent.setAction(Intent.ACTION_GET_CONTENT); + Intent chooser = Intent.createChooser(attachFileIntent, getString(R.string.attach_file)); + startActivityForResult(chooser, ATTACH_FILE); + } + } + }); + break; case R.id.action_add: startActivity(new Intent(this, ContactsActivity.class)); break; @@ -286,22 +311,16 @@ public class ConversationActivity extends XmppActivity { case R.id.action_contact_details: Contact contact = this.getSelectedConversation().getContact(); if (contact != null) { - Intent intent = new Intent(this,ContactDetailsActivity.class); + Intent intent = new Intent(this, ContactDetailsActivity.class); intent.setAction(ContactDetailsActivity.ACTION_VIEW_CONTACT); intent.putExtra("uuid", contact.getUuid()); startActivity(intent); } 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(); + showAddToRosterDialog(getSelectedConversation()); } break; case R.id.action_muc_details: - Intent intent = new Intent(this,MucDetailsActivity.class); + Intent intent = new Intent(this, MucDetailsActivity.class); intent.setAction(MucDetailsActivity.ACTION_VIEW_MUC); intent.putExtra("uuid", getSelectedConversation().getUuid()); startActivity(intent); @@ -310,17 +329,18 @@ public class ConversationActivity extends XmppActivity { Intent inviteIntent = new Intent(getApplicationContext(), ContactsActivity.class); inviteIntent.setAction("invite"); - inviteIntent.putExtra("uuid",selectedConversation.getUuid()); + inviteIntent.putExtra("uuid", selectedConversation.getUuid()); startActivity(inviteIntent); 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) { + final ConversationFragment fragment = (ConversationFragment) getFragmentManager() + .findFragmentByTag("conversation"); + if (fragment != null) { popup.setOnMenuItemClickListener(new OnMenuItemClickListener() { - + @Override public boolean onMenuItemClick(MenuItem item) { switch (item.getItemId()) { @@ -344,40 +364,66 @@ public class ConversationActivity extends XmppActivity { return true; } }); - popup.inflate(R.menu.encryption_choices); - switch (selConv.nextMessageEncryption) { + popup.inflate(R.menu.encryption_choices); + switch (selConv.nextMessageEncryption) { case Message.ENCRYPTION_NONE: - popup.getMenu().findItem(R.id.encryption_choice_none).setChecked(true); + 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); + 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); + 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); + popup.getMenu().findItem(R.id.encryption_choice_pgp) + .setChecked(true); break; default: - popup.getMenu().findItem(R.id.encryption_choice_none).setChecked(true); + popup.getMenu().findItem(R.id.encryption_choice_none) + .setChecked(true); break; } - popup.show(); - } + popup.show(); + } break; + case R.id.action_clear_history: + clearHistoryDialog(getSelectedConversation()); + break; default: break; } return super.onOptionsItemSelected(item); } + + protected void clearHistoryDialog(Conversation conversation) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.clear_conversation_history)); + View dialogView = getLayoutInflater().inflate(R.layout.dialog_clear_history, null); + builder.setView(dialogView); + builder.setNegativeButton(getString(R.string.cancel), null); + builder.setPositiveButton(getString(R.string.delete_messages), new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + // TODO Auto-generated method stub + + } + }); + builder.create().show(); + } protected ConversationFragment swapConversationFragment() { ConversationFragment selectedFragment = new ConversationFragment(); - + FragmentTransaction transaction = getFragmentManager() .beginTransaction(); - transaction.replace(R.id.selected_conversation, selectedFragment,"conversation"); + transaction.replace(R.id.selected_conversation, selectedFragment, + "conversation"); transaction.commit(); return selectedFragment; } @@ -392,24 +438,25 @@ public class ConversationActivity extends XmppActivity { } return super.onKeyDown(keyCode, event); } - + @Override public void onStart() { super.onStart(); - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); + SharedPreferences preferences = PreferenceManager + .getDefaultSharedPreferences(this); this.useSubject = preferences.getBoolean("use_subject_in_muc", true); if (this.xmppConnectionServiceBound) { this.onBackendConnected(); } - if (conversationList.size()>=1) { + if (conversationList.size() >= 1) { onConvChanged.onConversationListChanged(); } } - + @Override protected void onStop() { if (xmppConnectionServiceBound) { - xmppConnectionService.removeOnConversationListChangedListener(); + xmppConnectionService.removeOnConversationListChangedListener(); } super.onStop(); } @@ -417,18 +464,20 @@ public class ConversationActivity extends XmppActivity { @Override void onBackendConnected() { this.registerListener(); - if (conversationList.size()==0) { + if (conversationList.size() == 0) { updateConversationList(); } - if ((getIntent().getAction()!=null)&&(getIntent().getAction().equals(Intent.ACTION_VIEW) && (!handledViewIntent))) { + if ((getIntent().getAction() != null) + && (getIntent().getAction().equals(Intent.ACTION_VIEW) && (!handledViewIntent))) { if (getIntent().getType().equals( ConversationActivity.VIEW_CONVERSATION)) { handledViewIntent = true; - String convToView = (String) getIntent().getExtras().get(CONVERSATION); + String convToView = (String) getIntent().getExtras().get( + CONVERSATION); - for(int i = 0; i < conversationList.size(); ++i) { + for (int i = 0; i < conversationList.size(); ++i) { if (conversationList.get(i).getUuid().equals(convToView)) { selectedConversation = conversationList.get(i); } @@ -442,46 +491,108 @@ public class ConversationActivity extends XmppActivity { startActivity(new Intent(this, ManageAccountActivity.class)); finish(); } else if (conversationList.size() <= 0) { - //add no history + // add no history startActivity(new Intent(this, ContactsActivity.class)); finish(); } else { spl.openPane(); - //find currently loaded fragment - ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager().findFragmentByTag("conversation"); - if (selectedFragment!=null) { + // find currently loaded fragment + ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager() + .findFragmentByTag("conversation"); + if (selectedFragment != null) { selectedFragment.onBackendConnected(); } else { selectedConversation = conversationList.get(0); swapConversationFragment(); } - ExceptionHelper.checkForCrash(this,this.xmppConnectionService); + ExceptionHelper.checkForCrash(this, this.xmppConnectionService); } } } + public void registerListener() { - if (xmppConnectionServiceBound) { - xmppConnectionService.setOnConversationListChangedListener(this.onConvChanged); - } + if (xmppConnectionServiceBound) { + xmppConnectionService + .setOnConversationListChangedListener(this.onConvChanged); + } } @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (resultCode == RESULT_OK) { + protected void onActivityResult(int requestCode, int resultCode, final Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (resultCode == RESULT_OK) { if (requestCode == REQUEST_DECRYPT_PGP) { - ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager().findFragmentByTag("conversation"); - if (selectedFragment!=null) { + ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager() + .findFragmentByTag("conversation"); + if (selectedFragment != null) { selectedFragment.hidePgpPassphraseBox(); } + } else if (requestCode == ATTACH_FILE) { + Conversation conversation = getSelectedConversation(); + String presence = conversation.getNextPresence(); + xmppConnectionService.attachImageToConversation(conversation, presence, data.getData()); + } - } - } + } + } public void updateConversationList() { conversationList.clear(); - conversationList.addAll(xmppConnectionService - .getConversations()); + conversationList.addAll(xmppConnectionService.getConversations()); listView.invalidateViews(); } + + public void selectPresence(final Conversation conversation, final OnPresenceSelected listener) { + Contact contact = conversation.getContact(); + if (contact==null) { + showAddToRosterDialog(conversation); + listener.onPresenceSelected(false,null); + } else { + Hashtable<String, Integer> presences = contact.getPresences(); + if (presences.size() == 0) { + listener.onPresenceSelected(false, null); + } else if (presences.size() == 1) { + String presence = (String) presences.keySet().toArray()[0]; + conversation.setNextPresence(presence); + listener.onPresenceSelected(true, presence); + } else { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.choose_presence)); + final String[] presencesArray = new String[presences.size()]; + presences.keySet().toArray(presencesArray); + builder.setItems(presencesArray, + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, + int which) { + String presence = presencesArray[which]; + conversation.setNextPresence(presence); + listener.onPresenceSelected(true,presence); + } + }); + builder.create().show(); + } + } + } + + private void showAddToRosterDialog(final Conversation conversation) { + String jid = conversation.getContactJid(); + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(jid); + builder.setMessage(getString(R.string.not_in_roster)); + builder.setNegativeButton(getString(R.string.cancel), null); + builder.setPositiveButton(getString(R.string.add_contact), new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + String jid = conversation.getContactJid(); + Account account = getSelectedConversation().getAccount(); + String name = jid.split("@")[0]; + Contact contact = new Contact(account, name, jid, null); + xmppConnectionService.createContact(contact); + } + }); + builder.create().show(); + } } diff --git a/src/eu/siacs/conversations/ui/ConversationFragment.java b/src/eu/siacs/conversations/ui/ConversationFragment.java index 51caafbd..f43f37a3 100644 --- a/src/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/eu/siacs/conversations/ui/ConversationFragment.java @@ -33,6 +33,7 @@ import android.graphics.Typeface; import android.os.AsyncTask; import android.os.Bundle; import android.preference.PreferenceManager; +import android.util.DisplayMetrics; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -89,7 +90,6 @@ public class ConversationFragment extends Fragment { @Override public void onClick(View v) { - Log.d("gultsch", "clicked to decrypt"); if (askForPassphraseIntent != null) { try { getActivity().startIntentSenderForResult( @@ -97,7 +97,7 @@ public class ConversationFragment extends Fragment { ConversationActivity.REQUEST_DECRYPT_PGP, null, 0, 0, 0); } catch (SendIntentException e) { - Log.d("gultsch", "couldnt fire intent"); + Log.d("xmppService", "couldnt fire intent"); } } } @@ -124,20 +124,20 @@ public class ConversationFragment extends Fragment { public void updateChatMsgHint() { if (conversation.getMode() == Conversation.MODE_MULTI) { - chatMsg.setHint("Send message to conference"); + chatMsg.setHint(getString(R.string.send_message_to_conference)); } else { switch (conversation.nextMessageEncryption) { case Message.ENCRYPTION_NONE: - chatMsg.setHint("Send plain text message"); + chatMsg.setHint(getString(R.string.send_plain_text_message)); break; case Message.ENCRYPTION_OTR: - chatMsg.setHint("Send OTR encrypted message"); + chatMsg.setHint(getString(R.string.send_otr_message)); break; case Message.ENCRYPTION_PGP: - chatMsg.setHint("Send openPGP encryted messeage"); + chatMsg.setHint(getString(R.string.send_pgp_message)); break; case Message.ENCRYPTION_DECRYPTED: - chatMsg.setHint("Send openPGP encryted messeage"); + chatMsg.setHint(getString(R.string.send_pgp_message)); break; default: break; @@ -149,6 +149,8 @@ public class ConversationFragment extends Fragment { public View onCreateView(final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + final DisplayMetrics metrics = getResources().getDisplayMetrics(); + this.inflater = inflater; final View view = inflater.inflate(R.layout.fragment_conversation, @@ -177,19 +179,16 @@ public class ConversationFragment extends Fragment { private static final int SENT = 0; private static final int RECIEVED = 1; - private static final int ERROR = 2; @Override public int getViewTypeCount() { - return 3; + return 2; } @Override public int getItemViewType(int position) { - if (getItem(position).getStatus() == Message.STATUS_RECIEVED) { + if (getItem(position).getStatus() <= Message.STATUS_RECIEVED) { return RECIEVED; - } else if (getItem(position).getStatus() == Message.STATUS_ERROR) { - return ERROR; } else { return SENT; } @@ -206,20 +205,19 @@ public class ConversationFragment extends Fragment { case SENT: view = (View) inflater.inflate(R.layout.message_sent, null); - viewHolder.imageView = (ImageView) view + viewHolder.contact_picture = (ImageView) view .findViewById(R.id.message_photo); - viewHolder.imageView.setImageBitmap(selfBitmap); - viewHolder.indicator = (ImageView) view.findViewById(R.id.security_indicator); + viewHolder.contact_picture.setImageBitmap(selfBitmap); break; case RECIEVED: view = (View) inflater.inflate( R.layout.message_recieved, null); - viewHolder.imageView = (ImageView) view + viewHolder.contact_picture = (ImageView) view .findViewById(R.id.message_photo); - viewHolder.indicator = (ImageView) view.findViewById(R.id.security_indicator); + if (item.getConversation().getMode() == Conversation.MODE_SINGLE) { - viewHolder.imageView.setImageBitmap(mBitmapCache + viewHolder.contact_picture.setImageBitmap(mBitmapCache .get(item.getConversation().getName(useSubject), item .getConversation().getContact(), getActivity() @@ -227,18 +225,12 @@ public class ConversationFragment extends Fragment { } 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.indicator = (ImageView) view.findViewById(R.id.security_indicator); + viewHolder.image = (ImageView) view.findViewById(R.id.message_image); viewHolder.messageBody = (TextView) view .findViewById(R.id.message_body); viewHolder.time = (TextView) view @@ -250,52 +242,90 @@ public class ConversationFragment extends Fragment { if (type == RECIEVED) { if (item.getConversation().getMode() == Conversation.MODE_MULTI) { if (item.getCounterpart() != null) { - viewHolder.imageView.setImageBitmap(mBitmapCache + viewHolder.contact_picture.setImageBitmap(mBitmapCache .get(item.getCounterpart(), null, getActivity() .getApplicationContext())); } else { - viewHolder.imageView.setImageBitmap(mBitmapCache + viewHolder.contact_picture.setImageBitmap(mBitmapCache .get(item.getConversation().getName(useSubject), null, getActivity() .getApplicationContext())); } } } - String body = item.getBody(); - if (body != null) { - if (item.getEncryption() == Message.ENCRYPTION_PGP) { - viewHolder.messageBody - .setText(getString(R.string.encrypted_message)); + + if (item.getEncryption() == Message.ENCRYPTION_NONE) { + viewHolder.indicator.setVisibility(View.GONE); + } else { + viewHolder.indicator.setVisibility(View.VISIBLE); + } + + + if (item.getType() == Message.TYPE_IMAGE) { + if (item.getStatus() == Message.STATUS_PREPARING) { + viewHolder.image.setVisibility(View.GONE); + viewHolder.messageBody.setVisibility(View.VISIBLE); + viewHolder.messageBody.setText(getString(R.string.preparing_image)); + viewHolder.messageBody.setTextColor(0xff33B5E5); + viewHolder.messageBody.setTypeface(null,Typeface.ITALIC); + } else if (item.getStatus() == Message.STATUS_RECIEVING) { + viewHolder.image.setVisibility(View.GONE); + viewHolder.messageBody.setVisibility(View.GONE); + viewHolder.messageBody.setVisibility(View.VISIBLE); + viewHolder.messageBody.setText(getString(R.string.receiving_image)); viewHolder.messageBody.setTextColor(0xff33B5E5); - viewHolder.messageBody.setTypeface(null, - Typeface.ITALIC); - viewHolder.indicator.setVisibility(View.VISIBLE); - } else if ((item.getEncryption() == Message.ENCRYPTION_OTR)||(item.getEncryption() == Message.ENCRYPTION_DECRYPTED)) { - viewHolder.messageBody.setText(body.trim()); - viewHolder.messageBody.setTextColor(0xff000000); - viewHolder.messageBody.setTypeface(null, - Typeface.NORMAL); - viewHolder.indicator.setVisibility(View.VISIBLE); + 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_ERROR) { - viewHolder.indicator.setVisibility(View.GONE); - } + viewHolder.image.setImageBitmap(activity.xmppConnectionService.getFileBackend().getThumbnailFromMessage(item,(int) (metrics.density * 288))); + viewHolder.messageBody.setVisibility(View.GONE); + viewHolder.image.setVisibility(View.VISIBLE); } } else { - viewHolder.indicator.setVisibility(View.GONE); + viewHolder.image.setVisibility(View.GONE); + viewHolder.messageBody.setVisibility(View.VISIBLE); + 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 if ((item.getEncryption() == Message.ENCRYPTION_OTR)||(item.getEncryption() == Message.ENCRYPTION_DECRYPTED)) { + viewHolder.messageBody.setText(body.trim()); + viewHolder.messageBody.setTextColor(0xff333333); + viewHolder.messageBody.setTypeface(null, + Typeface.NORMAL); + } else { + viewHolder.messageBody.setText(body.trim()); + viewHolder.messageBody.setTextColor(0xff333333); + viewHolder.messageBody.setTypeface(null, + Typeface.NORMAL); + } + } } - if (item.getStatus() == Message.STATUS_UNSEND) { + switch (item.getStatus()) { + case Message.STATUS_UNSEND: viewHolder.time.setTypeface(null, Typeface.ITALIC); + viewHolder.time.setTextColor(0xFF8e8e8e); viewHolder.time.setText("sending\u2026"); - } else { + break; + case Message.STATUS_SEND_FAILED: + viewHolder.time.setText(getString(R.string.send_failed) + " \u00B7 " + UIHelper.readableTimeDifference(item + .getTimeSent())); + viewHolder.time.setTextColor(0xFFe92727); + viewHolder.time.setTypeface(null,Typeface.NORMAL); + break; + case Message.STATUS_SEND_REJECTED: + viewHolder.time.setText(getString(R.string.send_rejected)); + viewHolder.time.setTextColor(0xFFe92727); + viewHolder.time.setTypeface(null,Typeface.NORMAL); + break; + default: viewHolder.time.setTypeface(null, Typeface.NORMAL); - if ((item.getConversation().getMode() == Conversation.MODE_SINGLE) - || (type != RECIEVED)) { + viewHolder.time.setTextColor(0xFF8e8e8e); + if (item.getConversation().getMode() == Conversation.MODE_SINGLE) { viewHolder.time.setText(UIHelper .readableTimeDifference(item.getTimeSent())); } else { @@ -304,6 +334,7 @@ public class ConversationFragment extends Fragment { + UIHelper.readableTimeDifference(item .getTimeSent())); } + break; } return view; } @@ -320,7 +351,7 @@ public class ConversationFragment extends Fragment { boolean showPhoneSelfContactPicture = sharedPref.getBoolean( "show_phone_selfcontact_picture", true); - return UIHelper.getSelfContactPicture(conversation.getAccount(), 200, + return UIHelper.getSelfContactPicture(conversation.getAccount(), 48, showPhoneSelfContactPicture, getActivity()); } @@ -372,11 +403,11 @@ public class ConversationFragment extends Fragment { if (success) { Toast.makeText( getActivity(), - "Your nickname has been changed", + getString(R.string.your_nick_has_been_changed), Toast.LENGTH_SHORT).show(); } else { Toast.makeText(getActivity(), - "Nichname is already in use", + getString(R.string.nick_in_use), Toast.LENGTH_SHORT).show(); } } @@ -505,7 +536,7 @@ public class ConversationFragment extends Fragment { 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.setMessage("There is no openPGP key associated with this contact"); builder.setNegativeButton("Cancel", null); builder.setPositiveButton("Send plain text", new DialogInterface.OnClickListener() { @@ -585,10 +616,11 @@ public class ConversationFragment extends Fragment { private static class ViewHolder { + protected ImageView image; protected ImageView indicator; protected TextView time; protected TextView messageBody; - protected ImageView imageView; + protected ImageView contact_picture; } @@ -600,7 +632,12 @@ public class ConversationFragment extends Fragment { if (bitmaps.containsKey(name)) { return bitmaps.get(name); } else { - Bitmap bm = UIHelper.getContactPicture(contact, name, 200, context); + Bitmap bm; + if (contact != null){ + bm = UIHelper.getContactPicture(contact, 48, context, false); + } else { + bm = UIHelper.getContactPicture(name, 48, context, false); + } bitmaps.put(name, bm); return bm; } diff --git a/src/eu/siacs/conversations/ui/MucDetailsActivity.java b/src/eu/siacs/conversations/ui/MucDetailsActivity.java index bf36f156..c6807c61 100644 --- a/src/eu/siacs/conversations/ui/MucDetailsActivity.java +++ b/src/eu/siacs/conversations/ui/MucDetailsActivity.java @@ -179,7 +179,7 @@ public class MucDetailsActivity extends XmppActivity { role.setText(getReadableRole(contact.getRole())); ImageView imageView = (ImageView) view .findViewById(R.id.contact_photo); - imageView.setImageBitmap(UIHelper.getContactPicture(null,contact.getName(), 90,this.getApplicationContext())); + imageView.setImageBitmap(UIHelper.getContactPicture(contact.getName(), 48,this.getApplicationContext(), false)); membersView.addView(view); } } diff --git a/src/eu/siacs/conversations/ui/OnPresenceSelected.java b/src/eu/siacs/conversations/ui/OnPresenceSelected.java new file mode 100644 index 00000000..7e424b2e --- /dev/null +++ b/src/eu/siacs/conversations/ui/OnPresenceSelected.java @@ -0,0 +1,5 @@ +package eu.siacs.conversations.ui; + +public interface OnPresenceSelected { + public void onPresenceSelected(boolean success, String presence); +} diff --git a/src/eu/siacs/conversations/ui/ShareWithActivity.java b/src/eu/siacs/conversations/ui/ShareWithActivity.java index 51bad721..1bc9fc46 100644 --- a/src/eu/siacs/conversations/ui/ShareWithActivity.java +++ b/src/eu/siacs/conversations/ui/ShareWithActivity.java @@ -81,7 +81,10 @@ public class ShareWithActivity extends XmppActivity { } }); for(final Conversation conversation : convList) { - View view = createContactView(conversation.getName(useSubject), conversation.getLatestMessage().getBody().trim(), UIHelper.getContactPicture(conversation.getContact(),conversation.getName(useSubject), 90,this.getApplicationContext())); + View view = createContactView(conversation.getName(useSubject), + conversation.getLatestMessage().getBody().trim(), + UIHelper.getContactPicture(conversation, 48, + this.getApplicationContext(), false)); view.setOnClickListener(new OnClickListener() { @Override @@ -115,7 +118,8 @@ public class ShareWithActivity extends XmppActivity { for(int i = 0; i < contactsList.size(); ++i) { final Contact con = contactsList.get(i); - View view = createContactView(con.getDisplayName(), con.getJid(), UIHelper.getContactPicture(con,null, 90,this.getApplicationContext())); + View view = createContactView(con.getDisplayName(), con.getJid(), + UIHelper.getContactPicture(con, 48, this.getApplicationContext(), false)); view.setOnClickListener(new OnClickListener() { @Override diff --git a/src/eu/siacs/conversations/utils/ExceptionHelper.java b/src/eu/siacs/conversations/utils/ExceptionHelper.java index c6e857c0..f9cd375e 100644 --- a/src/eu/siacs/conversations/utils/ExceptionHelper.java +++ b/src/eu/siacs/conversations/utils/ExceptionHelper.java @@ -34,6 +34,18 @@ public class ExceptionHelper { if (neverSend) { return; } + List<Account> accounts = service.getAccounts(); + Account account = null; + for(int i = 0; i < accounts.size(); ++i) { + if (!accounts.get(i).isOptionSet(Account.OPTION_DISABLED)) { + account = accounts.get(i); + break; + } + } + if (account==null) { + return; + } + final Account finalAccount = account; FileInputStream file = context.openFileInput("stacktrace.txt"); InputStreamReader inputStreamReader = new InputStreamReader( file); @@ -54,20 +66,11 @@ public class ExceptionHelper { @Override public void onClick(DialogInterface dialog, int which) { - List<Account> accounts = service.getAccounts(); - Account account = null; - for(int i = 0; i < accounts.size(); ++i) { - if (!accounts.get(i).isOptionSet(Account.OPTION_DISABLED)) { - account = accounts.get(i); - break; - } - } - if (account!=null) { - Log.d("xmppService","using account="+account.getJid()+" to send in stack trace"); - Conversation conversation = service.findOrCreateConversation(account, "bugs@siacs.eu", false); + + Log.d("xmppService","using account="+finalAccount.getJid()+" to send in stack trace"); + Conversation conversation = service.findOrCreateConversation(finalAccount, "bugs@siacs.eu", false); Message message = new Message(conversation, stacktrace.toString(), Message.ENCRYPTION_NONE); service.sendMessage(message, null); - } } }); builder.setNegativeButton(context.getText(R.string.send_never),new OnClickListener() { diff --git a/src/eu/siacs/conversations/utils/MessageParser.java b/src/eu/siacs/conversations/utils/MessageParser.java index 936d0e9a..568386d5 100644 --- a/src/eu/siacs/conversations/utils/MessageParser.java +++ b/src/eu/siacs/conversations/utils/MessageParser.java @@ -149,19 +149,9 @@ public class MessageParser { 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 void parseError(MessagePacket packet, Account account, XmppConnectionService service) { + String[] fromParts = packet.getFrom().split("/"); + service.markMessage(account, fromParts[0], packet.getId(), Message.STATUS_SEND_FAILED); } public static String getPgpBody(MessagePacket packet) { diff --git a/src/eu/siacs/conversations/utils/PhoneHelper.java b/src/eu/siacs/conversations/utils/PhoneHelper.java index 5c652afe..6355e378 100644 --- a/src/eu/siacs/conversations/utils/PhoneHelper.java +++ b/src/eu/siacs/conversations/utils/PhoneHelper.java @@ -13,18 +13,19 @@ import android.os.Bundle; import android.os.Looper; import android.provider.ContactsContract; import android.provider.ContactsContract.Profile; +import android.provider.MediaStore; public class PhoneHelper { - - public static void loadPhoneContacts(Context context, final OnPhoneContactsLoadedListener listener) { - if (Looper.myLooper()==null) { + + public static void loadPhoneContacts(Context context, + final OnPhoneContactsLoadedListener listener) { + if (Looper.myLooper() == null) { Looper.prepare(); } final Looper mLooper = Looper.myLooper(); final Hashtable<String, Bundle> phoneContacts = new Hashtable<String, Bundle>(); - - final String[] PROJECTION = new String[] { - ContactsContract.Data._ID, + + final String[] PROJECTION = new String[] { ContactsContract.Data._ID, ContactsContract.Data.DISPLAY_NAME, ContactsContract.Data.PHOTO_THUMBNAIL_URI, ContactsContract.Data.LOOKUP_KEY, @@ -35,7 +36,7 @@ public class PhoneHelper { + "\") AND (" + ContactsContract.CommonDataKinds.Im.PROTOCOL + "=\"" + ContactsContract.CommonDataKinds.Im.PROTOCOL_JABBER + "\")"; - + CursorLoader mCursorLoader = new CursorLoader(context, ContactsContract.Data.CONTENT_URI, PROJECTION, SELECTION, null, null); @@ -55,14 +56,14 @@ public class PhoneHelper { "photouri", cursor.getString(cursor .getColumnIndex(ContactsContract.Data.PHOTO_THUMBNAIL_URI))); - contact.putString("lookup",cursor.getString(cursor + 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) { + if (listener != null) { listener.onPhoneContactsLoaded(phoneContacts); } mLooper.quit(); @@ -71,18 +72,18 @@ public class PhoneHelper { mCursorLoader.startLoading(); } - public static Uri getSefliUri(Activity activity) { + public static Uri getSefliUri(Context context) { String[] mProjection = new String[] { Profile._ID, Profile.PHOTO_THUMBNAIL_URI }; - Cursor mProfileCursor = activity.getContentResolver().query( + Cursor mProfileCursor = context.getContentResolver().query( Profile.CONTENT_URI, mProjection, null, null, null); - if (mProfileCursor.getCount()==0) { + if (mProfileCursor.getCount() == 0) { return null; } else { mProfileCursor.moveToFirst(); String uri = mProfileCursor.getString(1); - if (uri==null) { + if (uri == null) { return null; } else { return Uri.parse(uri); diff --git a/src/eu/siacs/conversations/utils/UIHelper.java b/src/eu/siacs/conversations/utils/UIHelper.java index 73a0494b..f058b9ee 100644 --- a/src/eu/siacs/conversations/utils/UIHelper.java +++ b/src/eu/siacs/conversations/utils/UIHelper.java @@ -12,6 +12,8 @@ import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.entities.MucOptions; +import eu.siacs.conversations.entities.MucOptions.User; import eu.siacs.conversations.ui.ConversationActivity; import eu.siacs.conversations.ui.ManageAccountActivity; @@ -31,6 +33,7 @@ import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; +import android.graphics.Typeface; import android.net.Uri; import android.preference.PreferenceManager; import android.provider.ContactsContract.Contacts; @@ -38,6 +41,7 @@ import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat.InboxStyle; import android.support.v4.app.TaskStackBuilder; import android.text.Html; +import android.util.DisplayMetrics; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -46,6 +50,10 @@ import android.widget.QuickContactBadge; import android.widget.TextView; public class UIHelper { + private static final int BG_COLOR = 0xFF181818; + private static final int FG_COLOR = 0xFFE5E5E5; + private static final int TRANSPARENT = 0x00000000; + public static String readableTimeDifference(long time) { if (time == 0) { return "just now"; @@ -65,49 +73,235 @@ public class UIHelper { } } - private static Bitmap getUnknownContactPicture(String name, int size) { - String firstLetter = (name.length() > 0) ? name.substring(0, 1).toUpperCase(Locale.US) : " "; + public static int getRealPx(int dp, Context context) { + final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); + return ((int) (dp * metrics.density)); + } + private static int getNameColor(String name) { int holoColors[] = { 0xFF1da9da, 0xFFb368d9, 0xFF83b600, 0xFFffa713, 0xFFe92727 }; - int color = holoColors[Math.abs(name.toLowerCase(Locale.getDefault()).hashCode()) % holoColors.length]; + return color; + } + private static Bitmap getUnknownContactPicture(String[] names, int size, int bgColor, int fgColor) { + int tiles = (names.length > 4)? 4 : + (names.length < 1)? 1 : + names.length; Bitmap bitmap = Bitmap .createBitmap(size, size, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); - bitmap.eraseColor(color); + String[] letters = new String[tiles]; + int[] colors = new int[tiles]; + if (names.length < 1) { + letters[0] = "?"; + colors[0] = 0xFFe92727; + } else { + for(int i = 0; i < tiles; ++i) { + letters[i] = (names[i].length() > 0) ? + names[i].substring(0, 1).toUpperCase(Locale.US) : " "; + colors[i] = getNameColor(names[i]); + } - 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); + if (names.length > 4) { + letters[3] = "\u2026"; // Unicode ellipsis + colors[3] = 0xFF444444; + } + } + Paint textPaint = new Paint(), tilePaint = new Paint(); + textPaint.setColor(fgColor); + textPaint.setTypeface(Typeface.create("sans-serif-light", Typeface.NORMAL)); + Rect rect, left, right, topLeft, bottomLeft, topRight, bottomRight; + float width; + + switch(tiles) { + case 1: + bitmap.eraseColor(colors[0]); + + textPaint.setTextSize((float) (size * 0.8)); + textPaint.setAntiAlias(true); + rect = new Rect(); + textPaint.getTextBounds(letters[0], 0, 1, rect); + width = textPaint.measureText(letters[0]); + canvas.drawText(letters[0], (size / 2) - (width / 2), (size / 2) + + (rect.height() / 2), textPaint); + break; + + case 2: + bitmap.eraseColor(bgColor); + + tilePaint.setColor(colors[0]); + left = new Rect(0, 0, (size/2)-1, size); + canvas.drawRect(left, tilePaint); + + tilePaint.setColor(colors[1]); + right = new Rect((size/2)+1, 0, size, size); + canvas.drawRect(right, tilePaint); + + textPaint.setTextSize((float) (size * 0.8*0.5)); + textPaint.setAntiAlias(true); + rect = new Rect(); + textPaint.getTextBounds(letters[0], 0, 1, rect); + width = textPaint.measureText(letters[0]); + canvas.drawText(letters[0], (size / 4) - (width / 2), (size / 2) + + (rect.height() / 2), textPaint); + textPaint.getTextBounds(letters[1], 0, 1, rect); + width = textPaint.measureText(letters[1]); + canvas.drawText(letters[1], (3 * size / 4) - (width / 2), (size / 2) + + (rect.height() / 2), textPaint); + break; + + case 3: + bitmap.eraseColor(bgColor); + + tilePaint.setColor(colors[0]); + left = new Rect(0, 0, (size/2)-1, size); + canvas.drawRect(left, tilePaint); + + tilePaint.setColor(colors[1]); + topRight = new Rect((size/2)+1, 0, size, (size/2 - 1)); + canvas.drawRect(topRight, tilePaint); + + tilePaint.setColor(colors[2]); + bottomRight = new Rect((size/2)+1, (size/2 + 1), size, size); + canvas.drawRect(bottomRight, tilePaint); + + textPaint.setTextSize((float) (size * 0.8*0.5)); + textPaint.setAntiAlias(true); + rect = new Rect(); + + textPaint.getTextBounds(letters[0], 0, 1, rect); + width = textPaint.measureText(letters[0]); + canvas.drawText(letters[0], (size / 4) - (width / 2), (size / 2) + + (rect.height() / 2), textPaint); + + textPaint.getTextBounds(letters[1], 0, 1, rect); + width = textPaint.measureText(letters[1]); + canvas.drawText(letters[1], (3 * size / 4) - (width / 2), (size / 4) + + (rect.height() / 2), textPaint); + + textPaint.getTextBounds(letters[2], 0, 1, rect); + width = textPaint.measureText(letters[2]); + canvas.drawText(letters[2], (3 * size / 4) - (width / 2), (3* size / 4) + + (rect.height() / 2), textPaint); + break; + + case 4: + bitmap.eraseColor(bgColor); + + tilePaint.setColor(colors[0]); + topLeft = new Rect(0, 0, (size/2)-1, (size/2)-1); + canvas.drawRect(topLeft, tilePaint); + + tilePaint.setColor(colors[1]); + bottomLeft = new Rect(0, (size/2)+1, (size/2)-1, size); + canvas.drawRect(bottomLeft, tilePaint); + tilePaint.setColor(colors[2]); + topRight = new Rect((size/2)+1, 0, size, (size/2 - 1)); + canvas.drawRect(topRight, tilePaint); + + tilePaint.setColor(colors[3]); + bottomRight = new Rect((size/2)+1, (size/2 + 1), size, size); + canvas.drawRect(bottomRight, tilePaint); + + textPaint.setTextSize((float) (size * 0.8*0.5)); + textPaint.setAntiAlias(true); + rect = new Rect(); + + textPaint.getTextBounds(letters[0], 0, 1, rect); + width = textPaint.measureText(letters[0]); + canvas.drawText(letters[0], (size / 4) - (width / 2), (size / 4) + + (rect.height() / 2), textPaint); + + textPaint.getTextBounds(letters[1], 0, 1, rect); + width = textPaint.measureText(letters[1]); + canvas.drawText(letters[1], (size / 4) - (width / 2), (3* size / 4) + + (rect.height() / 2), textPaint); + + textPaint.getTextBounds(letters[2], 0, 1, rect); + width = textPaint.measureText(letters[2]); + canvas.drawText(letters[2], (3 * size / 4) - (width / 2), (size / 4) + + (rect.height() / 2), textPaint); + + textPaint.getTextBounds(letters[3], 0, 1, rect); + width = textPaint.measureText(letters[3]); + canvas.drawText(letters[3], (3 * size / 4) - (width / 2), (3* size / 4) + + (rect.height() / 2), textPaint); + break; + } return bitmap; } - - public static Bitmap getContactPicture(Contact contact, String fallback, int size, Context context) { - if (contact==null) { - return getUnknownContactPicture(fallback, size); + + private static Bitmap getUnknownContactPicture(String[] names, int size) { + return getUnknownContactPicture(names, size, UIHelper.BG_COLOR, UIHelper.FG_COLOR); + } + + private static Bitmap getMucContactPicture(Conversation conversation, int size, int bgColor, int fgColor) { + List<User> members = conversation.getMucOptions().getUsers(); + if (members.size() == 0) { + return getUnknownContactPicture(new String[]{conversation.getName(false)}, size, bgColor, fgColor); + } + String[] names = new String[members.size()+1]; + names[0] = conversation.getMucOptions().getNick(); + for(int i = 0; i < members.size(); ++i) { + names[i+1] = members.get(i).getName(); + } + + return getUnknownContactPicture(names, size, bgColor, fgColor); + } + + public static Bitmap getContactPicture(Conversation conversation, int dpSize, Context context, boolean notification) { + if(conversation.getMode() == Conversation.MODE_SINGLE) { + if (conversation.getContact() != null){ + return getContactPicture(conversation.getContact(), dpSize, + context, notification); + } else { + return getContactPicture(conversation.getName(false), dpSize, + context, notification); + } + } else{ + int fgColor = UIHelper.FG_COLOR, + bgColor = (notification) ? + UIHelper.BG_COLOR : UIHelper.TRANSPARENT; + + return getMucContactPicture(conversation, getRealPx(dpSize, context), + bgColor, fgColor); } + } + + public static Bitmap getContactPicture(Contact contact, int dpSize, Context context, boolean notification) { + int fgColor = UIHelper.FG_COLOR, + bgColor = (notification) ? + UIHelper.BG_COLOR : UIHelper.TRANSPARENT; + String uri = contact.getProfilePhoto(); if (uri==null) { - return getUnknownContactPicture(contact.getDisplayName(), size); + return getContactPicture(contact.getDisplayName(), dpSize, + context, notification); } try { - Bitmap bm = BitmapFactory.decodeStream(context.getContentResolver().openInputStream(Uri.parse(uri))); - return Bitmap.createScaledBitmap(bm, size, size, false); + Bitmap bm = BitmapFactory.decodeStream(context.getContentResolver() + .openInputStream(Uri.parse(uri))); + return Bitmap.createScaledBitmap(bm, getRealPx(dpSize, context), + getRealPx(dpSize, context), false); } catch (FileNotFoundException e) { - return getUnknownContactPicture(contact.getDisplayName(), size); + return getContactPicture(contact.getDisplayName(), dpSize, + context, notification); } } + public static Bitmap getContactPicture(String name, int dpSize, Context context, boolean notification) { + int fgColor = UIHelper.FG_COLOR, + bgColor = (notification) ? + UIHelper.BG_COLOR : UIHelper.TRANSPARENT; + + return getUnknownContactPicture(new String[]{name}, getRealPx(dpSize, context), + bgColor, fgColor); + } + public static Bitmap getErrorPicture(int size) { Bitmap bitmap = Bitmap .createBitmap(size, size, Bitmap.Config.ARGB_8888); @@ -203,7 +397,6 @@ public class UIHelper { } String ringtone = preferences.getString("notification_ringtone", null); - Resources res = context.getResources(); NotificationCompat.Builder mBuilder = new NotificationCompat.Builder( context); if (unread.size() == 0) { @@ -212,8 +405,8 @@ public class UIHelper { } else if (unread.size() == 1) { Conversation conversation = unread.get(0); targetUuid = conversation.getUuid(); - mBuilder.setLargeIcon(UIHelper.getContactPicture(conversation.getContact(), conversation.getName(useSubject), (int) res - .getDimension(android.R.dimen.notification_large_icon_width), context)); + mBuilder.setLargeIcon(UIHelper.getContactPicture(conversation, 64, + context, true)); mBuilder.setContentTitle(conversation.getName(useSubject)); if (notify) { mBuilder.setTicker(conversation.getLatestMessage().getBody().trim()); @@ -255,12 +448,15 @@ public class UIHelper { mBuilder.setContentText(names.toString()); mBuilder.setStyle(style); } + if ((currentCon!=null)&&(notify)) { + targetUuid=currentCon.getUuid(); + } if (unread.size() != 0) { mBuilder.setSmallIcon(R.drawable.notification); if (notify) { if (vibrate) { int dat = 70; - long[] pattern = {0,3*dat,dat,dat,dat,3*dat,dat,dat}; + long[] pattern = {0,3*dat,dat,dat}; mBuilder.setVibrate(pattern); } mBuilder.setLights(0xffffffff, 2000, 4000); @@ -307,23 +503,13 @@ public class UIHelper { } public static void prepareContactBadge(final Activity activity, - QuickContactBadge badge, final Contact contact) { + QuickContactBadge badge, final Contact contact, Context context) { 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)); } - + badge.setImageBitmap(UIHelper.getContactPicture(contact, 72, context, false)); } public static AlertDialog getVerifyFingerprintDialog( @@ -359,20 +545,20 @@ public class UIHelper { return builder.create(); } - public static Bitmap getSelfContactPicture(Account account, int size, boolean showPhoneSelfContactPicture, Activity activity) { + public static Bitmap getSelfContactPicture(Account account, int size, boolean showPhoneSelfContactPicture, Context context) { if (showPhoneSelfContactPicture) { - Uri selfiUri = PhoneHelper.getSefliUri(activity); + Uri selfiUri = PhoneHelper.getSefliUri(context); if (selfiUri != null) { try { - return BitmapFactory.decodeStream(activity + return BitmapFactory.decodeStream(context .getContentResolver().openInputStream(selfiUri)); } catch (FileNotFoundException e) { - return getUnknownContactPicture(account.getJid(), size); + return getContactPicture(account.getJid(), size, context, false); } } - return getUnknownContactPicture(account.getJid(), size); + return getContactPicture(account.getJid(), size, context, false); } else { - return getUnknownContactPicture(account.getJid(), size); + return getContactPicture(account.getJid(), size, context, false); } } } diff --git a/src/eu/siacs/conversations/xml/Element.java b/src/eu/siacs/conversations/xml/Element.java index 2f1d7ad8..ce1d10ce 100644 --- a/src/eu/siacs/conversations/xml/Element.java +++ b/src/eu/siacs/conversations/xml/Element.java @@ -139,4 +139,8 @@ public class Element { content = content.replace("'","'"); return content; } + + public void clearChildren() { + this.children.clear(); + } } diff --git a/src/eu/siacs/conversations/xmpp/PacketReceived.java b/src/eu/siacs/conversations/xmpp/PacketReceived.java index d9f42459..d4502d73 100644 --- a/src/eu/siacs/conversations/xmpp/PacketReceived.java +++ b/src/eu/siacs/conversations/xmpp/PacketReceived.java @@ -1,5 +1,5 @@ package eu.siacs.conversations.xmpp; -abstract interface PacketReceived { +public abstract interface PacketReceived { } diff --git a/src/eu/siacs/conversations/xmpp/XmppConnection.java b/src/eu/siacs/conversations/xmpp/XmppConnection.java index 1ca7cd8b..adb27ec8 100644 --- a/src/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/eu/siacs/conversations/xmpp/XmppConnection.java @@ -16,9 +16,13 @@ import java.security.cert.CertPathValidatorException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.Hashtable; +import java.util.Iterator; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocket; @@ -44,11 +48,12 @@ import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Tag; import eu.siacs.conversations.xml.TagWriter; import eu.siacs.conversations.xml.XmlReader; +import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived; +import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; import eu.siacs.conversations.xmpp.stanzas.IqPacket; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; import eu.siacs.conversations.xmpp.stanzas.PresencePacket; -import eu.siacs.conversations.xmpp.stanzas.jingle.JinglePacket; import eu.siacs.conversations.xmpp.stanzas.streammgmt.AckPacket; import eu.siacs.conversations.xmpp.stanzas.streammgmt.EnablePacket; import eu.siacs.conversations.xmpp.stanzas.streammgmt.RequestPacket; @@ -70,8 +75,9 @@ public class XmppConnection implements Runnable { private boolean shouldBind = true; private boolean shouldAuthenticate = true; private Element streamFeatures; - private HashSet<String> discoFeatures = new HashSet<String>(); - private List<String> discoItems = new ArrayList<String>(); + private HashMap<String, List<String>> disco = new HashMap<String, List<String>>(); + + private HashSet<String> pendingSubscriptions = new HashSet<String>(); private String streamId = null; private int smVersion = 3; @@ -644,8 +650,8 @@ public class XmppConnection implements Runnable { tagWriter.writeStanzaAsync(enable); } sendInitialPresence(); - sendServiceDiscoveryInfo(); - sendServiceDiscoveryItems(); + sendServiceDiscoveryInfo(account.getServer()); + sendServiceDiscoveryItems(account.getServer()); if (bindListener !=null) { bindListener.onBind(account); } @@ -654,42 +660,53 @@ public class XmppConnection implements Runnable { }); } - private void sendServiceDiscoveryInfo() { + private void sendServiceDiscoveryInfo(final String server) { IqPacket iq = new IqPacket(IqPacket.TYPE_GET); - iq.setTo(account.getServer()); + iq.setTo(server); iq.query("http://jabber.org/protocol/disco#info"); this.sendIqPacket(iq, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { - List<Element> elements = packet.query().getChildren(); - for (int i = 0; i < elements.size(); ++i) { - if (elements.get(i).getName().equals("feature")) { - discoFeatures.add(elements.get(i).getAttribute( - "var")); - } + List<Element> elements = packet.query().getChildren(); + List<String> features = new ArrayList<String>(); + for (int i = 0; i < elements.size(); ++i) { + if (elements.get(i).getName().equals("feature")) { + features.add(elements.get(i).getAttribute( + "var")); } - if (discoFeatures.contains("urn:xmpp:carbons:2")) { - sendEnableCarbons(); + } + disco.put(server, features); + + if (account.getServer().equals(server)) { + enableAdvancedStreamFeatures(); } } }); } - private void sendServiceDiscoveryItems() { + + private void enableAdvancedStreamFeatures() { + if (hasFeaturesCarbon()) { + sendEnableCarbons(); + } + } + + private void sendServiceDiscoveryItems(final String server) { IqPacket iq = new IqPacket(IqPacket.TYPE_GET); - iq.setTo(account.getServer()); + iq.setTo(server); iq.query("http://jabber.org/protocol/disco#items"); this.sendIqPacket(iq, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { - List<Element> elements = packet.query().getChildren(); - for (int i = 0; i < elements.size(); ++i) { - if (elements.get(i).getName().equals("item")) { - discoItems.add(elements.get(i).getAttribute( - "jid")); - } + List<Element> elements = packet.query().getChildren(); + for (int i = 0; i < elements.size(); ++i) { + if (elements.get(i).getName().equals("item")) { + String jid = elements.get(i).getAttribute( + "jid"); + sendServiceDiscoveryInfo(jid); } + } } }); } @@ -732,8 +749,10 @@ public class XmppConnection implements Runnable { } public void sendIqPacket(IqPacket packet, OnIqPacketReceived callback) { - String id = nextRandomId(); - packet.setAttribute("id", id); + if (packet.getId()==null) { + String id = nextRandomId(); + packet.setAttribute("id", id); + } packet.setFrom(account.getFullJid()); this.sendPacket(packet, callback); } @@ -850,7 +869,26 @@ public class XmppConnection implements Runnable { } public boolean hasFeaturesCarbon() { - return discoFeatures.contains("urn:xmpp:carbons:2"); + return hasDiscoFeature(account.getServer(), "urn:xmpp:carbons:2"); + } + + public boolean hasDiscoFeature(String server, String feature) { + if (!disco.containsKey(server)) { + return false; + } + return disco.get(server).contains(feature); + } + + public String findDiscoItemByFeature(String feature) { + Iterator<Entry<String, List<String>>> it = this.disco.entrySet().iterator(); + while (it.hasNext()) { + Entry<String, List<String>> pairs = it.next(); + if (pairs.getValue().contains(feature)) { + return pairs.getKey(); + } + it.remove(); + } + return null; } public void r() { @@ -866,15 +904,15 @@ public class XmppConnection implements Runnable { } public String getMucServer() { - for(int i = 0; i < discoItems.size(); ++i) { - if (discoItems.get(i).contains("conference.")) { - return discoItems.get(i); - } else if (discoItems.get(i).contains("conf.")) { - return discoItems.get(i); - } else if (discoItems.get(i).contains("muc.")) { - return discoItems.get(i); - } - } - return null; + return findDiscoItemByFeature("http://jabber.org/protocol/muc"); + } + + public boolean hasPendingSubscription(String jid) { + return this.pendingSubscriptions.contains(jid); + } + + public void addPendingSubscription(String jid) { + Log.d(LOGTAG,"adding "+jid+" to pending subscriptions"); + this.pendingSubscriptions.add(jid); } } diff --git a/src/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java b/src/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java new file mode 100644 index 00000000..80ffeaaa --- /dev/null +++ b/src/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java @@ -0,0 +1,138 @@ +package eu.siacs.conversations.xmpp.jingle; + +import java.util.ArrayList; +import java.util.List; + +import eu.siacs.conversations.xml.Element; + +public class JingleCandidate { + + public static int TYPE_UNKNOWN; + public static int TYPE_DIRECT = 0; + public static int TYPE_PROXY = 1; + + private boolean ours; + private boolean usedByCounterpart = false; + private String cid; + private String host; + private int port; + private int type; + private String jid; + private int priority; + + public JingleCandidate(String cid,boolean ours) { + this.ours = ours; + this.cid = cid; + } + + public String getCid() { + return cid; + } + + public void setHost(String host) { + this.host = host; + } + + public String getHost() { + return this.host; + } + + public void setJid(String jid) { + this.jid = jid; + } + + public String getJid() { + return this.jid; + } + + public void setPort(int port) { + this.port = port; + } + + public int getPort() { + return this.port; + } + + public void setType(int type) { + this.type = type; + } + + public void setType(String type) { + if ("proxy".equals(type)) { + this.type = TYPE_PROXY; + } else if ("direct".equals(type)) { + this.type = TYPE_DIRECT; + } else { + this.type = TYPE_UNKNOWN; + } + } + + public void setPriority(int i) { + this.priority = i; + } + + public int getPriority() { + return this.priority; + } + + public boolean equals(JingleCandidate other) { + return this.getCid().equals(other.getCid()); + } + + public boolean equalValues(JingleCandidate other) { + return other.getHost().equals(this.getHost())&&(other.getPort()==this.getPort()); + } + + public boolean isOurs() { + return ours; + } + + public int getType() { + return this.type; + } + + public static List<JingleCandidate> parse(List<Element> canditates) { + List<JingleCandidate> parsedCandidates = new ArrayList<JingleCandidate>(); + for(Element c : canditates) { + parsedCandidates.add(JingleCandidate.parse(c)); + } + return parsedCandidates; + } + + public static JingleCandidate parse(Element candidate) { + JingleCandidate parsedCandidate = new JingleCandidate(candidate.getAttribute("cid"), false); + parsedCandidate.setHost(candidate.getAttribute("host")); + parsedCandidate.setJid(candidate.getAttribute("jid")); + parsedCandidate.setType(candidate.getAttribute("type")); + parsedCandidate.setPriority(Integer.parseInt(candidate.getAttribute("priority"))); + parsedCandidate.setPort(Integer.parseInt(candidate.getAttribute("port"))); + return parsedCandidate; + } + + public Element toElement() { + Element element = new Element("candidate"); + element.setAttribute("cid", this.getCid()); + element.setAttribute("host", this.getHost()); + element.setAttribute("port", ""+this.getPort()); + element.setAttribute("jid", this.getJid()); + element.setAttribute("priority",""+this.getPriority()); + if (this.getType()==TYPE_DIRECT) { + element.setAttribute("type","direct"); + } else if (this.getType()==TYPE_PROXY) { + element.setAttribute("type","proxy"); + } + return element; + } + + public void flagAsUsedByCounterpart() { + this.usedByCounterpart = true; + } + + public boolean isUsedByCounterpart() { + return this.usedByCounterpart; + } + + public String toString() { + return this.getHost()+":"+this.getPort()+" (prio="+this.getPriority()+")"; + } +} diff --git a/src/eu/siacs/conversations/xmpp/jingle/JingleConnection.java b/src/eu/siacs/conversations/xmpp/jingle/JingleConnection.java new file mode 100644 index 00000000..a7fd367d --- /dev/null +++ b/src/eu/siacs/conversations/xmpp/jingle/JingleConnection.java @@ -0,0 +1,494 @@ +package eu.siacs.conversations.xmpp.jingle; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map.Entry; + +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.OnIqPacketReceived; +import eu.siacs.conversations.xmpp.jingle.stanzas.Content; +import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; +import eu.siacs.conversations.xmpp.jingle.stanzas.Reason; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; + +public class JingleConnection { + + private JingleConnectionManager mJingleConnectionManager; + private XmppConnectionService mXmppConnectionService; + + public static final int STATUS_INITIATED = 0; + public static final int STATUS_ACCEPTED = 1; + public static final int STATUS_TERMINATED = 2; + public static final int STATUS_CANCELED = 3; + public static final int STATUS_FINISHED = 4; + public static final int STATUS_TRANSMITTING = 5; + public static final int STATUS_FAILED = 99; + + private int status = -1; + private Message message; + private String sessionId; + private Account account; + private String initiator; + private String responder; + private List<JingleCandidate> candidates = new ArrayList<JingleCandidate>(); + private HashMap<String, SocksConnection> connections = new HashMap<String, SocksConnection>(); + + private String transportId; + private Element fileOffer; + private JingleFile file = null; + + private boolean receivedCandidateError = false; + + private OnIqPacketReceived responseListener = new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() == IqPacket.TYPE_ERROR) { + mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED); + status = STATUS_FAILED; + } + } + }; + + public JingleConnection(JingleConnectionManager mJingleConnectionManager) { + this.mJingleConnectionManager = mJingleConnectionManager; + this.mXmppConnectionService = mJingleConnectionManager.getXmppConnectionService(); + } + + public String getSessionId() { + return this.sessionId; + } + + public String getAccountJid() { + return this.account.getFullJid(); + } + + public String getCounterPart() { + return this.message.getCounterpart(); + } + + public void deliverPacket(JinglePacket packet) { + + if (packet.isAction("session-terminate")) { + Reason reason = packet.getReason(); + if (reason!=null) { + if (reason.hasChild("cancel")) { + this.cancel(); + } else if (reason.hasChild("success")) { + this.finish(); + } + } else { + Log.d("xmppService","remote terminated for no reason"); + this.cancel(); + } + } else if (packet.isAction("session-accept")) { + accept(packet); + } else if (packet.isAction("transport-info")) { + transportInfo(packet); + } else { + Log.d("xmppService","packet arrived in connection. action was "+packet.getAction()); + } + } + + public void init(Message message) { + this.message = message; + this.account = message.getConversation().getAccount(); + this.initiator = this.account.getFullJid(); + this.responder = this.message.getCounterpart(); + this.sessionId = this.mJingleConnectionManager.nextRandomId(); + if (this.candidates.size() > 0) { + this.sendInitRequest(); + } else { + this.mJingleConnectionManager.getPrimaryCandidate(account, new OnPrimaryCandidateFound() { + + @Override + public void onPrimaryCandidateFound(boolean success, JingleCandidate candidate) { + if (success) { + mergeCandidate(candidate); + } + openOurCandidates(); + sendInitRequest(); + } + }); + } + + } + + public void init(Account account, JinglePacket packet) { + this.status = STATUS_INITIATED; + Conversation conversation = this.mXmppConnectionService.findOrCreateConversation(account, packet.getFrom().split("/")[0], false); + this.message = new Message(conversation, "receiving image file", Message.ENCRYPTION_NONE); + this.message.setType(Message.TYPE_IMAGE); + this.message.setStatus(Message.STATUS_RECIEVING); + String[] fromParts = packet.getFrom().split("/"); + this.message.setPresence(fromParts[1]); + this.account = account; + this.initiator = packet.getFrom(); + this.responder = this.account.getFullJid(); + this.sessionId = packet.getSessionId(); + Content content = packet.getJingleContent(); + this.transportId = content.getTransportId(); + this.mergeCandidates(JingleCandidate.parse(content.getCanditates())); + this.fileOffer = packet.getJingleContent().getFileOffer(); + if (fileOffer!=null) { + this.file = this.mXmppConnectionService.getFileBackend().getJingleFile(message); + Element fileSize = fileOffer.findChild("size"); + Element fileName = fileOffer.findChild("name"); + this.file.setExpectedSize(Long.parseLong(fileSize.getContent())); + conversation.getMessages().add(message); + this.mXmppConnectionService.databaseBackend.createMessage(message); + if (this.mXmppConnectionService.convChangedListener!=null) { + this.mXmppConnectionService.convChangedListener.onConversationListChanged(); + } + if (this.file.getExpectedSize()>=this.mJingleConnectionManager.getAutoAcceptFileSize()) { + Log.d("xmppService","auto accepting file from "+packet.getFrom()); + this.sendAccept(); + } else { + Log.d("xmppService","not auto accepting new file offer with size: "+this.file.getExpectedSize()+" allowed size:"+this.mJingleConnectionManager.getAutoAcceptFileSize()); + } + } else { + Log.d("xmppService","no file offer was attached. aborting"); + } + } + + private void sendInitRequest() { + JinglePacket packet = this.bootstrapPacket("session-initiate"); + Content content = new Content(); + if (message.getType() == Message.TYPE_IMAGE) { + content.setAttribute("creator", "initiator"); + content.setAttribute("name", "a-file-offer"); + this.file = this.mXmppConnectionService.getFileBackend().getJingleFile(message); + content.setFileOffer(this.file); + this.transportId = this.mJingleConnectionManager.nextRandomId(); + content.setCandidates(this.transportId,getCandidatesAsElements()); + packet.setContent(content); + Log.d("xmppService",packet.toString()); + account.getXmppConnection().sendIqPacket(packet, this.responseListener); + this.status = STATUS_INITIATED; + } + } + + private List<Element> getCandidatesAsElements() { + List<Element> elements = new ArrayList<Element>(); + for(JingleCandidate c : this.candidates) { + elements.add(c.toElement()); + } + return elements; + } + + private void sendAccept() { + this.mJingleConnectionManager.getPrimaryCandidate(this.account, new OnPrimaryCandidateFound() { + + @Override + public void onPrimaryCandidateFound(boolean success, JingleCandidate candidate) { + Content content = new Content(); + content.setFileOffer(fileOffer); + if (success) { + if (!equalCandidateExists(candidate)) { + mergeCandidate(candidate); + } + } + openOurCandidates(); + content.setCandidates(transportId, getCandidatesAsElements()); + JinglePacket packet = bootstrapPacket("session-accept"); + packet.setContent(content); + account.getXmppConnection().sendIqPacket(packet, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() != IqPacket.TYPE_ERROR) { + status = STATUS_ACCEPTED; + connectNextCandidate(); + } + } + }); + } + }); + + } + + private JinglePacket bootstrapPacket(String action) { + JinglePacket packet = new JinglePacket(); + packet.setAction(action); + packet.setFrom(account.getFullJid()); + packet.setTo(this.message.getCounterpart()); //fixme, not right in all cases; + packet.setSessionId(this.sessionId); + packet.setInitiator(this.initiator); + return packet; + } + + private void accept(JinglePacket packet) { + Log.d("xmppService","session-accept: "+packet.toString()); + Content content = packet.getJingleContent(); + mergeCandidates(JingleCandidate.parse(content.getCanditates())); + this.status = STATUS_ACCEPTED; + this.connectNextCandidate(); + IqPacket response = packet.generateRespone(IqPacket.TYPE_RESULT); + account.getXmppConnection().sendIqPacket(response, null); + } + + private void transportInfo(JinglePacket packet) { + Content content = packet.getJingleContent(); + String cid = content.getUsedCandidate(); + IqPacket response = packet.generateRespone(IqPacket.TYPE_RESULT); + if (cid!=null) { + Log.d("xmppService","candidate used by counterpart:"+cid); + JingleCandidate candidate = getCandidate(cid); + candidate.flagAsUsedByCounterpart(); + if (status == STATUS_ACCEPTED) { + this.connect(); + } else { + Log.d("xmppService","ignoring because file is already in transmission"); + } + } else if (content.hasCandidateError()) { + Log.d("xmppService","received candidate error"); + this.receivedCandidateError = true; + if (status == STATUS_ACCEPTED) { + this.connect(); + } + } + account.getXmppConnection().sendIqPacket(response, null); + } + + private void connect() { + final SocksConnection connection = chooseConnection(); + this.status = STATUS_TRANSMITTING; + final OnFileTransmitted callback = new OnFileTransmitted() { + + @Override + public void onFileTransmitted(JingleFile file) { + if (responder.equals(account.getFullJid())) { + sendSuccess(); + mXmppConnectionService.markMessage(message, Message.STATUS_SEND); + } + Log.d("xmppService","sucessfully transmitted file. sha1:"+file.getSha1Sum()); + } + }; + if (connection.isProxy()&&(connection.getCandidate().isOurs())) { + Log.d("xmppService","candidate "+connection.getCandidate().getCid()+" was our proxy and needs activation"); + IqPacket activation = new IqPacket(IqPacket.TYPE_SET); + activation.setTo(connection.getCandidate().getJid()); + activation.query("http://jabber.org/protocol/bytestreams").setAttribute("sid", this.getSessionId()); + activation.query().addChild("activate").setContent(this.getCounterPart()); + this.account.getXmppConnection().sendIqPacket(activation, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + Log.d("xmppService","activation result: "+packet.toString()); + if (initiator.equals(account.getFullJid())) { + Log.d("xmppService","we were initiating. sending file"); + connection.send(file,callback); + } else { + connection.receive(file,callback); + Log.d("xmppService","we were responding. receiving file"); + } + } + }); + } else { + if (initiator.equals(account.getFullJid())) { + Log.d("xmppService","we were initiating. sending file"); + connection.send(file,callback); + } else { + Log.d("xmppService","we were responding. receiving file"); + connection.receive(file,callback); + } + } + } + + private SocksConnection chooseConnection() { + Log.d("xmppService","choosing connection from "+this.connections.size()+" possibilties"); + SocksConnection connection = null; + Iterator<Entry<String, SocksConnection>> it = this.connections.entrySet().iterator(); + while (it.hasNext()) { + Entry<String, SocksConnection> pairs = it.next(); + SocksConnection currentConnection = pairs.getValue(); + Log.d("xmppService","comparing candidate: "+currentConnection.getCandidate().toString()); + if (currentConnection.isEstablished()&&(currentConnection.getCandidate().isUsedByCounterpart()||(!currentConnection.getCandidate().isOurs()))) { + Log.d("xmppService","is usable"); + if (connection==null) { + connection = currentConnection; + } else { + if (connection.getCandidate().getPriority()<currentConnection.getCandidate().getPriority()) { + connection = currentConnection; + } else if (connection.getCandidate().getPriority()==currentConnection.getCandidate().getPriority()) { + Log.d("xmppService","found two candidates with same priority"); + if (initiator.equals(account.getFullJid())) { + if (currentConnection.getCandidate().isOurs()) { + connection = currentConnection; + } + } else { + if (!currentConnection.getCandidate().isOurs()) { + connection = currentConnection; + } + } + } + } + } + it.remove(); + } + Log.d("xmppService","chose candidate: "+connection.getCandidate().getHost()); + return connection; + } + + private void sendSuccess() { + JinglePacket packet = bootstrapPacket("session-terminate"); + Reason reason = new Reason(); + reason.addChild("success"); + packet.setReason(reason); + Log.d("xmppService","sending success. "+packet.toString()); + this.account.getXmppConnection().sendIqPacket(packet, responseListener); + this.disconnect(); + this.status = STATUS_FINISHED; + this.mXmppConnectionService.markMessage(this.message, Message.STATUS_RECIEVED); + } + + private void finish() { + this.status = STATUS_FINISHED; + this.mXmppConnectionService.markMessage(this.message, Message.STATUS_SEND); + this.disconnect(); + } + + public void cancel() { + this.disconnect(); + this.status = STATUS_CANCELED; + this.mXmppConnectionService.markMessage(this.message, Message.STATUS_SEND_REJECTED); + } + + private void openOurCandidates() { + for(JingleCandidate candidate : this.candidates) { + if (candidate.isOurs()) { + final SocksConnection socksConnection = new SocksConnection(this,candidate); + connections.put(candidate.getCid(), socksConnection); + socksConnection.connect(new OnSocksConnection() { + + @Override + public void failed() { + Log.d("xmppService","connection to our candidate failed"); + } + + @Override + public void established() { + Log.d("xmppService","connection to our candidate was successful"); + } + }); + } + } + } + + private void connectNextCandidate() { + for(JingleCandidate candidate : this.candidates) { + if ((!connections.containsKey(candidate.getCid())&&(!candidate.isOurs()))) { + this.connectWithCandidate(candidate); + return; + } + } + this.sendCandidateError(); + } + + private void connectWithCandidate(final JingleCandidate candidate) { + final SocksConnection socksConnection = new SocksConnection(this,candidate); + connections.put(candidate.getCid(), socksConnection); + socksConnection.connect(new OnSocksConnection() { + + @Override + public void failed() { + connectNextCandidate(); + } + + @Override + public void established() { + sendCandidateUsed(candidate.getCid()); + if ((receivedCandidateError)&&(status == STATUS_ACCEPTED)) { + Log.d("xmppService","received candidate error before. trying to connect"); + connect(); + } + } + }); + } + + private void disconnect() { + Iterator<Entry<String, SocksConnection>> it = this.connections.entrySet().iterator(); + while (it.hasNext()) { + Entry<String, SocksConnection> pairs = it.next(); + pairs.getValue().disconnect(); + it.remove(); + } + } + + private void sendCandidateUsed(final String cid) { + JinglePacket packet = bootstrapPacket("transport-info"); + Content content = new Content(); + //TODO: put these into actual variables + content.setAttribute("creator", "initiator"); + content.setAttribute("name", "a-file-offer"); + content.setUsedCandidate(this.transportId, cid); + packet.setContent(content); + Log.d("xmppService","send using candidate: "+cid); + this.account.getXmppConnection().sendIqPacket(packet,responseListener); + } + + private void sendCandidateError() { + JinglePacket packet = bootstrapPacket("transport-info"); + Content content = new Content(); + //TODO: put these into actual variables + content.setAttribute("creator", "initiator"); + content.setAttribute("name", "a-file-offer"); + content.setCandidateError(this.transportId); + packet.setContent(content); + Log.d("xmppService","send candidate error"); + this.account.getXmppConnection().sendIqPacket(packet,responseListener); + } + + public String getInitiator() { + return this.initiator; + } + + public String getResponder() { + return this.responder; + } + + public int getStatus() { + return this.status; + } + + private boolean equalCandidateExists(JingleCandidate candidate) { + for(JingleCandidate c : this.candidates) { + if (c.equalValues(candidate)) { + return true; + } + } + return false; + } + + private void mergeCandidate(JingleCandidate candidate) { + for(JingleCandidate c : this.candidates) { + if (c.equals(candidate)) { + return; + } + } + this.candidates.add(candidate); + } + + private void mergeCandidates(List<JingleCandidate> candidates) { + for(JingleCandidate c : candidates) { + mergeCandidate(c); + } + } + + private JingleCandidate getCandidate(String cid) { + for(JingleCandidate c : this.candidates) { + if (c.getCid().equals(cid)) { + return c; + } + } + return null; + } +} diff --git a/src/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java new file mode 100644 index 00000000..44af51a3 --- /dev/null +++ b/src/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -0,0 +1,130 @@ +package eu.siacs.conversations.xmpp.jingle; + +import java.math.BigInteger; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +import android.util.Log; + +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.OnIqPacketReceived; +import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; + +public class JingleConnectionManager { + + private XmppConnectionService xmppConnectionService; + + private List<JingleConnection> connections = new ArrayList<JingleConnection>(); // make + // concurrent + + private ConcurrentHashMap<String, JingleCandidate> primaryCandidates = new ConcurrentHashMap<String, JingleCandidate>(); + + private SecureRandom random = new SecureRandom(); + + public JingleConnectionManager(XmppConnectionService service) { + this.xmppConnectionService = service; + } + + public void deliverPacket(Account account, JinglePacket packet) { + if (packet.isAction("session-initiate")) { + JingleConnection connection = new JingleConnection(this); + connection.init(account,packet); + connections.add(connection); + } else { + for (JingleConnection connection : connections) { + if (connection.getAccountJid().equals(account.getFullJid()) && connection + .getSessionId().equals(packet.getSessionId()) && connection + .getCounterPart().equals(packet.getFrom())) { + connection.deliverPacket(packet); + return; + } + } + Log.d("xmppService","delivering packet failed "+packet.toString()); + } + } + + public JingleConnection createNewConnection(Message message) { + JingleConnection connection = new JingleConnection(this); + connection.init(message); + connections.add(connection); + return connection; + } + + public JingleConnection createNewConnection(JinglePacket packet) { + JingleConnection connection = new JingleConnection(this); + connections.add(connection); + return connection; + } + + public XmppConnectionService getXmppConnectionService() { + return this.xmppConnectionService; + } + + public void getPrimaryCandidate(Account account, + final OnPrimaryCandidateFound listener) { + if (!this.primaryCandidates.containsKey(account.getJid())) { + String xmlns = "http://jabber.org/protocol/bytestreams"; + final String proxy = account.getXmppConnection() + .findDiscoItemByFeature(xmlns); + if (proxy != null) { + IqPacket iq = new IqPacket(IqPacket.TYPE_GET); + iq.setTo(proxy); + iq.query(xmlns); + account.getXmppConnection().sendIqPacket(iq, + new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, + IqPacket packet) { + Element streamhost = packet + .query() + .findChild("streamhost", + "http://jabber.org/protocol/bytestreams"); + if (streamhost != null) { + Log.d("xmppService", "streamhost found " + + streamhost.toString()); + JingleCandidate candidate = new JingleCandidate(nextRandomId(),true); + candidate.setHost(streamhost.getAttribute("host")); + candidate.setPort(Integer.parseInt(streamhost.getAttribute("port"))); + candidate.setType(JingleCandidate.TYPE_PROXY); + candidate.setJid(proxy); + candidate.setPriority(655360+65535); + primaryCandidates.put(account.getJid(), + candidate); + listener.onPrimaryCandidateFound(true, + candidate); + } else { + listener.onPrimaryCandidateFound(false, + null); + } + } + }); + } else { + listener.onPrimaryCandidateFound(false, null); + } + + } else { + listener.onPrimaryCandidateFound(true, + this.primaryCandidates.get(account.getJid())); + } + } + + public String nextRandomId() { + return new BigInteger(50, random).toString(32); + } + + public long getAutoAcceptFileSize() { + String config = this.xmppConnectionService.getPreferences().getString("auto_accept_file_size", "0"); + try { + return Long.parseLong(config); + } catch (NumberFormatException e) { + return 0; + } + } +} diff --git a/src/eu/siacs/conversations/xmpp/jingle/JingleFile.java b/src/eu/siacs/conversations/xmpp/jingle/JingleFile.java new file mode 100644 index 00000000..21cbd716 --- /dev/null +++ b/src/eu/siacs/conversations/xmpp/jingle/JingleFile.java @@ -0,0 +1,35 @@ +package eu.siacs.conversations.xmpp.jingle; + +import java.io.File; + +public class JingleFile extends File { + + private static final long serialVersionUID = 2247012619505115863L; + + private long expectedSize = 0; + private String sha1sum; + + public JingleFile(String path) { + super(path); + } + + public long getSize() { + return super.length(); + } + + public long getExpectedSize() { + return this.expectedSize; + } + + public void setExpectedSize(long size) { + this.expectedSize = size; + } + + public String getSha1Sum() { + return this.sha1sum; + } + + public void setSha1Sum(String sum) { + this.sha1sum = sum; + } +} diff --git a/src/eu/siacs/conversations/xmpp/jingle/OnFileTransmitted.java b/src/eu/siacs/conversations/xmpp/jingle/OnFileTransmitted.java new file mode 100644 index 00000000..fd5fd2f7 --- /dev/null +++ b/src/eu/siacs/conversations/xmpp/jingle/OnFileTransmitted.java @@ -0,0 +1,5 @@ +package eu.siacs.conversations.xmpp.jingle; + +public interface OnFileTransmitted { + public void onFileTransmitted(JingleFile file); +} diff --git a/src/eu/siacs/conversations/xmpp/OnJinglePacketReceived.java b/src/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java index 6705e309..2aaf62a1 100644 --- a/src/eu/siacs/conversations/xmpp/OnJinglePacketReceived.java +++ b/src/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java @@ -1,7 +1,8 @@ -package eu.siacs.conversations.xmpp; +package eu.siacs.conversations.xmpp.jingle; import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.xmpp.stanzas.jingle.JinglePacket; +import eu.siacs.conversations.xmpp.PacketReceived; +import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; public interface OnJinglePacketReceived extends PacketReceived { public void onJinglePacketReceived(Account account, JinglePacket packet); diff --git a/src/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java b/src/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java new file mode 100644 index 00000000..b91a90ff --- /dev/null +++ b/src/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java @@ -0,0 +1,5 @@ +package eu.siacs.conversations.xmpp.jingle; + +public interface OnPrimaryCandidateFound { + public void onPrimaryCandidateFound(boolean success, JingleCandidate canditate); +} diff --git a/src/eu/siacs/conversations/xmpp/jingle/OnSocksConnection.java b/src/eu/siacs/conversations/xmpp/jingle/OnSocksConnection.java new file mode 100644 index 00000000..88771997 --- /dev/null +++ b/src/eu/siacs/conversations/xmpp/jingle/OnSocksConnection.java @@ -0,0 +1,6 @@ +package eu.siacs.conversations.xmpp.jingle; + +public interface OnSocksConnection { + public void failed(); + public void established(); +} diff --git a/src/eu/siacs/conversations/xmpp/jingle/SocksConnection.java b/src/eu/siacs/conversations/xmpp/jingle/SocksConnection.java new file mode 100644 index 00000000..197b9424 --- /dev/null +++ b/src/eu/siacs/conversations/xmpp/jingle/SocksConnection.java @@ -0,0 +1,207 @@ +package eu.siacs.conversations.xmpp.jingle; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.net.UnknownHostException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; + +import eu.siacs.conversations.utils.CryptoHelper; +import eu.siacs.conversations.xml.Element; + +import android.util.Log; +import android.widget.Button; + +public class SocksConnection { + private JingleCandidate candidate; + private String destination; + private OutputStream outputStream; + private InputStream inputStream; + private boolean isEstablished = false; + protected Socket socket; + + public SocksConnection(JingleConnection jingleConnection, JingleCandidate candidate) { + this.candidate = candidate; + try { + MessageDigest mDigest = MessageDigest.getInstance("SHA-1"); + StringBuilder destBuilder = new StringBuilder(); + destBuilder.append(jingleConnection.getSessionId()); + if (candidate.isOurs()) { + destBuilder.append(jingleConnection.getAccountJid()); + destBuilder.append(jingleConnection.getCounterPart()); + } else { + destBuilder.append(jingleConnection.getCounterPart()); + destBuilder.append(jingleConnection.getAccountJid()); + } + mDigest.reset(); + this.destination = CryptoHelper.bytesToHex(mDigest + .digest(destBuilder.toString().getBytes())); + } catch (NoSuchAlgorithmException e) { + + } + } + + public void connect(final OnSocksConnection callback) { + new Thread(new Runnable() { + + @Override + public void run() { + try { + socket = new Socket(candidate.getHost(), candidate.getPort()); + inputStream = socket.getInputStream(); + outputStream = socket.getOutputStream(); + byte[] login = { 0x05, 0x01, 0x00 }; + byte[] expectedReply = { 0x05, 0x00 }; + byte[] reply = new byte[2]; + outputStream.write(login); + inputStream.read(reply); + if (Arrays.equals(reply, expectedReply)) { + String connect = "" + '\u0005' + '\u0001' + '\u0000' + '\u0003' + + '\u0028' + destination + '\u0000' + '\u0000'; + outputStream.write(connect.getBytes()); + byte[] result = new byte[2]; + inputStream.read(result); + int status = result[1]; + if (status == 0) { + Log.d("xmppService", "established connection with "+candidate.getHost()+":"+candidate.getPort()+ "/" + destination); + isEstablished = true; + callback.established(); + } else { + callback.failed(); + } + } else { + socket.close(); + callback.failed(); + } + } catch (UnknownHostException e) { + callback.failed(); + } catch (IOException e) { + callback.failed(); + } + } + }).start(); + + } + + public void send(final JingleFile file, final OnFileTransmitted callback) { + new Thread(new Runnable() { + + @Override + public void run() { + FileInputStream fileInputStream = null; + try { + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + digest.reset(); + fileInputStream = new FileInputStream(file); + int count; + byte[] buffer = new byte[8192]; + while ((count = fileInputStream.read(buffer)) > 0) { + outputStream.write(buffer, 0, count); + digest.update(buffer, 0, count); + } + outputStream.flush(); + file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest())); + if (callback!=null) { + callback.onFileTransmitted(file); + } + } catch (FileNotFoundException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (NoSuchAlgorithmException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } finally { + try { + if (fileInputStream != null) { + fileInputStream.close(); + } + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + } + }).start(); + + } + + public void receive(final JingleFile file, final OnFileTransmitted callback) { + new Thread(new Runnable() { + + @Override + public void run() { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + digest.reset(); + inputStream.skip(45); + file.getParentFile().mkdirs(); + file.createNewFile(); + FileOutputStream fileOutputStream = new FileOutputStream(file); + long remainingSize = file.getExpectedSize(); + byte[] buffer = new byte[8192]; + int count = buffer.length; + while(remainingSize > 0) { + Log.d("xmppService","remaning size:"+remainingSize); + if (remainingSize<=count) { + count = (int) remainingSize; + } + count = inputStream.read(buffer, 0, count); + if (count==-1) { + Log.d("xmppService","end of stream"); + } else { + fileOutputStream.write(buffer, 0, count); + digest.update(buffer, 0, count); + remainingSize-=count; + } + } + fileOutputStream.flush(); + fileOutputStream.close(); + file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest())); + Log.d("xmppService","transmitted filename was: "+file.getAbsolutePath()); + callback.onFileTransmitted(file); + } catch (FileNotFoundException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (NoSuchAlgorithmException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + }).start(); + } + + public boolean isProxy() { + return this.candidate.getType() == JingleCandidate.TYPE_PROXY; + } + + public void disconnect() { + if (this.socket!=null) { + try { + this.socket.close(); + Log.d("xmppService","cloesd socket with "+candidate.getHost()+":"+candidate.getPort()); + } catch (IOException e) { + Log.d("xmppService","error closing socket with "+candidate.getHost()+":"+candidate.getPort()); + } + } + } + + public boolean isEstablished() { + return this.isEstablished; + } + + public JingleCandidate getCandidate() { + return this.candidate; + } +} diff --git a/src/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java b/src/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java new file mode 100644 index 00000000..3cd30251 --- /dev/null +++ b/src/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java @@ -0,0 +1,124 @@ +package eu.siacs.conversations.xmpp.jingle.stanzas; + +import java.util.ArrayList; +import java.util.List; + +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.jingle.JingleFile; + +public class Content extends Element { + private Content(String name) { + super(name); + } + + public Content() { + super("content"); + } + + public void setFileOffer(JingleFile actualFile) { + Element description = this.addChild("description", "urn:xmpp:jingle:apps:file-transfer:3"); + Element offer = description.addChild("offer"); + Element file = offer.addChild("file"); + file.addChild("size").setContent(""+actualFile.getSize()); + file.addChild("name").setContent(actualFile.getName()); + } + + public Element getFileOffer() { + Element description = this.findChild("description", "urn:xmpp:jingle:apps:file-transfer:3"); + if (description==null) { + return null; + } + Element offer = description.findChild("offer"); + if (offer==null) { + return null; + } + return offer.findChild("file"); + } + + public void setCandidates(String transportId, List<Element> canditates) { + Element transport = this.findChild("transport", "urn:xmpp:jingle:transports:s5b:1"); + if (transport==null) { + transport = this.addChild("transport", "urn:xmpp:jingle:transports:s5b:1"); + } + transport.setAttribute("sid", transportId); + transport.clearChildren(); + for(Element canditate : canditates) { + transport.addChild(canditate); + } + } + + public List<Element> getCanditates() { + Element transport = this.findChild("transport", "urn:xmpp:jingle:transports:s5b:1"); + if (transport==null) { + return new ArrayList<Element>(); + } else { + return transport.getChildren(); + } + } + + public String getTransportId() { + Element transport = this.findChild("transport", "urn:xmpp:jingle:transports:s5b:1"); + if (transport==null) { + return null; + } + return transport.getAttribute("sid"); + } + + public String getUsedCandidate() { + Element transport = this.findChild("transport", "urn:xmpp:jingle:transports:s5b:1"); + if (transport==null) { + return null; + } + Element usedCandidate = transport.findChild("candidate-used"); + if (usedCandidate==null) { + return null; + } else { + return usedCandidate.getAttribute("cid"); + } + } + + public boolean hasCandidateError() { + Element transport = this.findChild("transport", "urn:xmpp:jingle:transports:s5b:1"); + if (transport==null) { + return false; + } + return transport.hasChild("candidate-error"); + } + + public void setUsedCandidate(String transportId, String cid) { + Element transport = this.findChild("transport", "urn:xmpp:jingle:transports:s5b:1"); + if (transport==null) { + transport = this.addChild("transport", "urn:xmpp:jingle:transports:s5b:1"); + } + transport.setAttribute("sid", transportId); + transport.clearChildren(); + Element usedCandidate = transport.addChild("candidate-used"); + usedCandidate.setAttribute("cid",cid); + } + + public void addCandidate(Element candidate) { + Element transport = this.findChild("transport", "urn:xmpp:jingle:transports:s5b:1"); + if (transport==null) { + transport = this.addChild("transport", "urn:xmpp:jingle:transports:s5b:1"); + } + transport.addChild(candidate); + } + + public void setFileOffer(Element fileOffer) { + Element description = this.findChild("description", "urn:xmpp:jingle:apps:file-transfer:3"); + if (description==null) { + description = this.addChild("description", "urn:xmpp:jingle:apps:file-transfer:3"); + } + description.addChild(fileOffer); + } + + public void setCandidateError(String transportId) { + Element transport = this.findChild("transport", "urn:xmpp:jingle:transports:s5b:1"); + if (transport==null) { + transport = this.addChild("transport", "urn:xmpp:jingle:transports:s5b:1"); + } + transport.setAttribute("sid", transportId); + transport.clearChildren(); + transport.addChild("candidate-error"); + } +} diff --git a/src/eu/siacs/conversations/xmpp/stanzas/jingle/JinglePacket.java b/src/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java index 51c60d1f..55700609 100644 --- a/src/eu/siacs/conversations/xmpp/stanzas/jingle/JinglePacket.java +++ b/src/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java @@ -1,4 +1,4 @@ -package eu.siacs.conversations.xmpp.stanzas.jingle; +package eu.siacs.conversations.xmpp.jingle.stanzas; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.stanzas.IqPacket; @@ -6,6 +6,7 @@ import eu.siacs.conversations.xmpp.stanzas.IqPacket; public class JinglePacket extends IqPacket { Content content = null; Reason reason = null; + Element jingle = new Element("jingle"); @Override public Element addChild(Element child) { @@ -22,27 +23,36 @@ public class JinglePacket extends IqPacket { this.reason.setChildren(reasonElement.getChildren()); this.reason.setAttributes(reasonElement.getAttributes()); } - this.build(); - this.findChild("jingle").setAttributes(child.getAttributes()); + this.jingle.setAttributes(child.getAttributes()); } return child; } public JinglePacket setContent(Content content) { this.content = content; - this.build(); return this; } + public Content getJingleContent() { + if (this.content==null) { + this.content = new Content(); + } + return this.content; + } + public JinglePacket setReason(Reason reason) { this.reason = reason; - this.build(); return this; } + public Reason getReason() { + return this.reason; + } + private void build() { this.children.clear(); - Element jingle = addChild("jingle", "urn:xmpp:jingle:1"); + this.jingle.clearChildren(); + this.jingle.setAttribute("xmlns", "urn:xmpp:jingle:1"); if (this.content!=null) { jingle.addChild(this.content); } @@ -50,5 +60,36 @@ public class JinglePacket extends IqPacket { jingle.addChild(this.reason); } this.children.add(jingle); + this.setAttribute("type", "set"); + } + + public String getSessionId() { + return this.jingle.getAttribute("sid"); + } + + public void setSessionId(String sid) { + this.jingle.setAttribute("sid", sid); + } + + @Override + public String toString() { + this.build(); + return super.toString(); + } + + public void setAction(String action) { + this.jingle.setAttribute("action", action); + } + + public String getAction() { + return this.jingle.getAttribute("action"); + } + + public void setInitiator(String initiator) { + this.jingle.setAttribute("initiator", initiator); + } + + public boolean isAction(String action) { + return action.equalsIgnoreCase(this.getAction()); } } diff --git a/src/eu/siacs/conversations/xmpp/stanzas/jingle/Reason.java b/src/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java index 35b81655..195e0db7 100644 --- a/src/eu/siacs/conversations/xmpp/stanzas/jingle/Reason.java +++ b/src/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java @@ -1,4 +1,4 @@ -package eu.siacs.conversations.xmpp.stanzas.jingle; +package eu.siacs.conversations.xmpp.jingle.stanzas; import eu.siacs.conversations.xml.Element; diff --git a/src/eu/siacs/conversations/xmpp/stanzas/IqPacket.java b/src/eu/siacs/conversations/xmpp/stanzas/IqPacket.java index 3ab3b6c3..9e288454 100644 --- a/src/eu/siacs/conversations/xmpp/stanzas/IqPacket.java +++ b/src/eu/siacs/conversations/xmpp/stanzas/IqPacket.java @@ -63,5 +63,13 @@ public class IqPacket extends AbstractStanza { return 1000; } } + + public IqPacket generateRespone(int type) { + IqPacket packet = new IqPacket(type); + packet.setFrom(this.getTo()); + packet.setTo(this.getFrom()); + packet.setId(this.getId()); + return packet; + } } diff --git a/src/eu/siacs/conversations/xmpp/stanzas/jingle/Content.java b/src/eu/siacs/conversations/xmpp/stanzas/jingle/Content.java deleted file mode 100644 index ebd212b8..00000000 --- a/src/eu/siacs/conversations/xmpp/stanzas/jingle/Content.java +++ /dev/null @@ -1,13 +0,0 @@ -package eu.siacs.conversations.xmpp.stanzas.jingle; - -import eu.siacs.conversations.xml.Element; - -public class Content extends Element { - private Content(String name) { - super(name); - } - - public Content() { - super("content"); - } -} |